// Tabs.cpp - Dessin des onglets de l'interface
#include "Tabs.h"
#include "WeatherData.h"
#include "Theme.h"
#include "Widget.h"
#include "Widgets.h"
#include "TabManager.h"
#include <stdarg.h>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <math.h>
// ─── Constantes layout ───────────────────────────────────────────────────────
#define TAB_BAR_HEIGHT 30
#define BME280_UPDATE_MS 60000
// ─── Données onglet Intérieur ────────────────────────────────────────────────
IndoorSensor gSensors[4] = {
{"Salon", 21.5, 85, true},
{"Chambre", 19.2, 72, true},
{"Cuisine", 22.8, 60, true},
{"Garage", 14.5, 45, false}
};
int gSensorCount = 4;
int gCurrentSensorPage = 0;
// ─── Données onglet Debug ────────────────────────────────────────────────────
char debugBuffer[2048];
int debugIndex = 0;
// ─── Gestionnaires de widgets ────────────────────────────────────────────────
WidgetManager weatherWidgetManager;
UpdatableMainTempWidget* mainTempWidget = nullptr;
UpdatableForecastWidget* forecastWidget = nullptr;
UpdatableChartWidget* chartWidget = nullptr;
UpdatableSunWidget* sunWidget = nullptr;
UpdatableAQIWidget* aqiWidget = nullptr;
// ─── Debug log ───────────────────────────────────────────────────────────────
void addDebugMessage(const char* format, ...)
{
char buffer[128];
va_list args;
va_start(args, format);
vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
int len = strlen(buffer);
if (debugIndex + len + 2 < (int)sizeof(debugBuffer)) {
if (debugIndex > 0) debugBuffer[debugIndex++] = '\n';
memcpy(&debugBuffer[debugIndex], buffer, len);
debugIndex += len;
debugBuffer[debugIndex] = '\0';
} else {
int half = sizeof(debugBuffer) / 2;
memmove(debugBuffer, debugBuffer + half, half);
debugIndex = half;
addDebugMessage(buffer);
}
}
// ─── Helpers ─────────────────────────────────────────────────────────────────
static void hline(TFT_eSPI& tft, int x, int y, int w, uint16_t c) {
tft.drawFastHLine(x, y, w, c);
}
static void vline(TFT_eSPI& tft, int x, int y, int h, uint16_t c) {
tft.drawFastVLine(x, y, h, c);
}
// Extrait "HH:MM" depuis "Jeu. 29 mai 14:32"
static void extractTime(const char* src, char* out5)
{
int len = src ? (int)strlen(src) : 0;
if (len >= 5) {
strncpy(out5, src + len - 5, 5);
out5[5] = '\0';
} else {
strcpy(out5, "--:--");
}
}
// Extrait la partie date "Jeu. 29 mai" depuis "Jeu. 29 mai 14:32"
static void extractDate(const char* src, char* outDate, int maxLen)
{
if (!src) { strncpy(outDate, "--", maxLen); return; }
int len = (int)strlen(src);
if (len > 8) {
int cutPos = len - 7; // " HH:MM" = 7 chars
if (cutPos > 0 && cutPos < maxLen) {
strncpy(outDate, src, cutPos);
while (cutPos > 0 && outDate[cutPos-1] == ' ') cutPos--;
outDate[cutPos] = '\0';
return;
}
}
strncpy(outDate, src, maxLen - 1);
outDate[maxLen - 1] = '\0';
}
// ─── Icône météo ──────────────────────────────────────────────────────────────
static void drawWeatherIconByType(TFT_eSPI& tft, int cx, int cy, int r, int iconType)
{
switch (iconType) {
case 0: // Soleil
tft.fillCircle(cx, cy, r - 2, C_ACCENT_GOLD);
for (int a = 0; a < 8; a++) {
float angle = a * PI / 4.0f;
tft.drawLine(cx + (int)((r) * cosf(angle)),
cy + (int)((r) * sinf(angle)),
cx + (int)((r + 5) * cosf(angle)),
cy + (int)((r + 5) * sinf(angle)),
C_ACCENT_GOLD);
}
break;
case 1: // Nuages
tft.fillCircle(cx - 5, cy + 2, r - 5, C_MUTED);
tft.fillCircle(cx + 5, cy + 2, r - 6, C_MUTED);
tft.fillCircle(cx, cy, r - 4, 0xCE79);
break;
case 2: // Pluie
tft.fillCircle(cx - 4, cy - 3, r - 5, C_MUTED);
tft.fillCircle(cx + 4, cy - 3, r - 6, C_MUTED);
tft.fillCircle(cx, cy - 5, r - 4, 0xCE79);
for (int d = 0; d < 3; d++) {
int lx = cx - 6 + d * 6;
tft.drawLine(lx, cy + 4, lx - 2, cy + 11, C_ACCENT_BLUE);
}
break;
case 3: // Soleil + nuage
tft.fillCircle(cx + r - 5, cy - r + 5, r - 7, C_ACCENT_GOLD);
tft.fillCircle(cx - 3, cy + 2, r - 5, C_MUTED);
tft.fillCircle(cx + 4, cy + 2, r - 6, 0x9CD3);
tft.fillCircle(cx, cy, r - 5, 0xCE79);
break;
default:
tft.fillCircle(cx, cy, r - 3, C_MUTED);
break;
}
}
static void drawWeatherIconByCondition(TFT_eSPI& tft, int cx, int cy, int r,
const char* condition)
{
int type = 1;
if (!condition) { drawWeatherIconByType(tft, cx, cy, r, type); return; }
if (strstr(condition, "Soleil") || strstr(condition, "Clair")
|| strstr(condition, "Agreable") || strstr(condition, "Sec")) type = 0;
else if (strstr(condition, "Pluie") || strstr(condition, "pluie")) type = 2;
else if (strstr(condition, "nuageux") || strstr(condition, "Nuageux")
|| strstr(condition, "Humide")) type = 1;
drawWeatherIconByType(tft, cx, cy, r, type);
}
// ─── Section SOLEIL ───────────────────────────────────────────────────────────
static void drawSunSection(TFT_eSPI& tft, int x, int y, int w, int h)
{
tft.fillRoundRect(x, y, w, h, RADIUS, C_PANEL);
tft.drawRoundRect(x, y, w, h, RADIUS, C_BORDER);
tft.setTextSize(1);
tft.setTextColor(C_MUTED, C_PANEL);
tft.setCursor(x + 6, y + 5);
tft.print("SOLEIL");
// Lever
tft.fillTriangle(x + 8, y + 28, x + 14, y + 20, x + 20, y + 28, C_ACCENT_GOLD);
tft.setTextColor(C_WHITE, C_PANEL);
tft.setCursor(x + 24, y + 20);
tft.print(gWeather.sunriseTime ? gWeather.sunriseTime : "--:--");
// Coucher
tft.fillTriangle(x + 8, y + 42, x + 14, y + 50, x + 20, y + 42, C_ACCENT_RED);
tft.setTextColor(C_WHITE, C_PANEL);
tft.setCursor(x + 24, y + 40);
tft.print(gWeather.sunsetTime ? gWeather.sunsetTime : "--:--");
}
// ─── Section AQI + UV ─────────────────────────────────────────────────────────
static void drawAqiUvSection(TFT_eSPI& tft, int x, int y, int w, int h)
{
tft.fillRoundRect(x, y, w, h, RADIUS, C_PANEL);
tft.drawRoundRect(x, y, w, h, RADIUS, C_BORDER);
tft.setTextSize(1);
tft.setTextColor(C_MUTED, C_PANEL);
tft.setCursor(x + 6, y + 5);
tft.print("AIR & UV");
uint16_t aqiColor = (gWeather.aqi < 50) ? C_ACCENT_GREEN :
(gWeather.aqi < 100) ? C_ACCENT_GOLD : C_ACCENT_RED;
tft.setTextSize(2);
tft.setTextColor(aqiColor, C_PANEL);
tft.setCursor(x + 6, y + 18);
tft.print(gWeather.aqi);
tft.setTextSize(1);
tft.setTextColor(C_MUTED, C_PANEL);
tft.print(" AQI");
uint16_t uvColor = (gWeather.uvIndex <= 2) ? C_ACCENT_GREEN :
(gWeather.uvIndex <= 5) ? C_ACCENT_GOLD : C_ACCENT_RED;
tft.setTextSize(2);
tft.setTextColor(uvColor, C_PANEL);
tft.setCursor(x + 6, y + 40);
tft.print(gWeather.uvIndex);
tft.setTextSize(1);
tft.setTextColor(C_MUTED, C_PANEL);
tft.print(" UV");
}
// ─── Mini graphique horaire ───────────────────────────────────────────────────
static void drawHourlyChart(TFT_eSPI& tft, int x, int y, int w, int h)
{
tft.fillRoundRect(x, y, w, h, RADIUS, C_PANEL);
tft.drawRoundRect(x, y, w, h, RADIUS, C_BORDER);
int tMin = gHourly[0].temp, tMax = gHourly[0].temp;
for (int i = 1; i < 8; i++) {
if (gHourly[i].temp < tMin) tMin = gHourly[i].temp;
if (gHourly[i].temp > tMax) tMax = gHourly[i].temp;
}
int range = max(1, tMax - tMin);
int chartX = x + 4, chartY = y + 6;
int chartW = w - 8, chartH = h - 18;
int stepX = chartW / 7;
for (int i = 0; i < 7; i++) {
int x1 = chartX + i * stepX;
int x2 = chartX + (i + 1) * stepX;
int y1 = chartY + chartH - (gHourly[i].temp - tMin) * chartH / range;
int y2 = chartY + chartH - (gHourly[i+1].temp - tMin) * chartH / range;
tft.drawLine(x1, y1, x2, y2, C_ACCENT_BLUE);
tft.fillCircle(x1, y1, 2, C_WHITE);
}
int xl = chartX + 7 * stepX;
int yl = chartY + chartH - (gHourly[7].temp - tMin) * chartH / range;
tft.fillCircle(xl, yl, 2, C_WHITE);
for (int i = 0; i < 8; i += 2) {
int lx = chartX + i * stepX - 4;
tft.setTextSize(1);
tft.setTextColor(C_DIM, C_PANEL);
tft.setCursor(lx, y + h - 10);
if (gHourly[i].label) tft.print(gHourly[i].label);
}
}
// ─── Prévisions 5 jours ───────────────────────────────────────────────────────
static void drawForecastBar(TFT_eSPI& tft, int x, int y, int w, int h)
{
tft.fillRoundRect(x, y, w, h, RADIUS, C_PANEL);
tft.drawRoundRect(x, y, w, h, RADIUS, C_BORDER);
int itemW = w / 5;
for (int i = 0; i < 5; i++) {
int ix = x + i * itemW;
uint16_t bg = (i == 0) ? 0x1082 : C_PANEL;
if (i == 0)
tft.fillRoundRect(ix + 2, y + 2, itemW - 4, h - 4, 3, bg);
tft.setTextSize(1);
tft.setTextColor(i == 0 ? C_ACCENT_BLUE : C_MUTED, bg);
const char* lbl = gForecast[i].label ? gForecast[i].label : "?";
tft.setCursor(ix + (itemW - (int)strlen(lbl) * 6) / 2, y + 5);
tft.print(lbl);
uint16_t dotColor;
switch (gForecast[i].iconType) {
case 0: dotColor = C_ACCENT_GOLD; break;
case 2: dotColor = C_ACCENT_BLUE; break;
default: dotColor = C_MUTED; break;
}
tft.fillCircle(ix + itemW / 2, y + h / 2 + 2, 4, dotColor);
char tStr[6]; itoa(gForecast[i].temp, tStr, 10);
tft.setTextSize(1);
tft.setTextColor(C_WHITE, bg);
tft.setCursor(ix + (itemW - (int)strlen(tStr) * 6) / 2, y + h - 12);
tft.print(tStr);
tft.setTextColor(C_DIM, bg);
tft.print("o");
if (i < 4) vline(tft, ix + itemW - 1, y + 4, h - 8, C_BORDER);
}
}
// ════════════════════════════════════════════════════════════════════════════
// ONGLET MÉTÉO
// ════════════════════════════════════════════════════════════════════════════
void drawWeatherTab(TFT_eSPI& tft)
{
const int PAD = 6;
const int TOP = TAB_BAR_HEIGHT + 2;
const int W = SCREEN_W;
// ── Zone 1 : HEURE (grand) + DATE + VILLE ───────────────────────────────
const int Z1H = 52;
tft.fillRect(0, TOP, W, Z1H, C_BG);
char heureStr[6];
extractTime(gWeather.time, heureStr);
tft.setTextSize(4);
int heureW = (int)strlen(heureStr) * 6 * 4;
tft.setTextColor(C_WHITE, C_BG);
tft.setCursor((W - heureW) / 2, TOP + 6);
tft.print(heureStr);
char dateStr[32] = "";
extractDate(gWeather.time, dateStr, sizeof(dateStr));
tft.setTextSize(1);
tft.setTextColor(C_ACCENT_GOLD, C_BG);
tft.setCursor(PAD, TOP + Z1H - 12);
tft.print(dateStr);
if (gWeather.city) {
tft.setTextColor(C_MUTED, C_BG);
int locW = (int)strlen(gWeather.city) * 6;
tft.setCursor(W - locW - PAD, TOP + Z1H - 12);
tft.print(gWeather.city);
}
hline(tft, PAD, TOP + Z1H, W - PAD * 2, C_BORDER);
int curY = TOP + Z1H + PAD;
// ── Zone 2 : TEMPÉRATURE + ICÔNE + CONDITION ────────────────────────────
const int Z2H = 58;
const int LW = W * 50 / 100;
const int RW = W - LW - PAD;
tft.fillRect(0, curY, W, Z2H, C_BG);
char bigTemp[8];
itoa(gWeather.temp, bigTemp, 10);
tft.setTextSize(5);
tft.setTextColor(C_WHITE, C_BG);
tft.setCursor(PAD, curY + 4);
tft.print(bigTemp);
tft.setTextSize(2);
tft.setTextColor(C_MUTED, C_BG);
tft.setCursor(PAD + (int)strlen(bigTemp) * 30, curY + 4);
tft.print("o");
tft.setTextSize(1);
tft.setTextColor(C_MUTED, C_BG);
tft.setCursor(PAD, curY + Z2H - 14);
tft.print("Ressenti "); tft.print(gWeather.feelsLike);
tft.print("C "); tft.print(gWeather.tempMin);
tft.print("-"); tft.print(gWeather.tempMax); tft.print("C");
int iconCX = LW + RW / 2;
drawWeatherIconByCondition(tft, iconCX, curY + 20, 18, gWeather.condition);
tft.setTextSize(1);
tft.setTextColor(C_MUTED, C_BG);
if (gWeather.condition) {
int condW = (int)strlen(gWeather.condition) * 6;
tft.setCursor(LW + max(0, (RW - condW) / 2), curY + Z2H - 14);
tft.print(gWeather.condition);
}
hline(tft, PAD, curY + Z2H, W - PAD * 2, C_BORDER);
curY += Z2H + PAD;
// ── Zone 3 : VENT + HUMIDITÉ + PRESSION ─────────────────────────────────
const int Z3H = 34;
const int C3W = (W - PAD * 4) / 3;
tft.fillRect(0, curY, W, Z3H, C_BG);
// Vent
tft.setTextSize(1);
tft.setTextColor(C_MUTED, C_BG);
tft.setCursor(PAD, curY + 2);
tft.print("VENT");
tft.setTextSize(2);
tft.setTextColor(C_ACCENT_GOLD, C_BG);
tft.setCursor(PAD, curY + 13);
tft.print(gWeather.windSpeed);
tft.setTextSize(1);
tft.setTextColor(C_MUTED, C_BG);
tft.print("km/h");
if (gWeather.windDir) {
tft.setTextColor(C_WHITE, C_BG);
tft.print(" ");
tft.print(gWeather.windDir);
}
// Humidité
int c2x = PAD * 2 + C3W;
tft.setTextSize(1);
tft.setTextColor(C_MUTED, C_BG);
tft.setCursor(c2x, curY + 2);
tft.print("HUMIDITE");
tft.setTextSize(2);
tft.setTextColor(C_ACCENT_BLUE, C_BG);
tft.setCursor(c2x, curY + 13);
tft.print(gWeather.humidity);
tft.setTextSize(1);
tft.setTextColor(C_MUTED, C_BG);
tft.print("%");
// Pression
int c3x = PAD * 3 + C3W * 2;
tft.setTextSize(1);
tft.setTextColor(C_MUTED, C_BG);
tft.setCursor(c3x, curY + 2);
tft.print("PRESSION");
tft.setTextSize(2);
tft.setTextColor(C_ACCENT_GOLD, C_BG);
tft.setCursor(c3x, curY + 13);
tft.print(gWeather.pressure);
tft.setTextSize(1);
tft.setTextColor(C_MUTED, C_BG);
tft.print("hPa");
hline(tft, PAD, curY + Z3H, W - PAD * 2, C_BORDER);
curY += Z3H + PAD;
// ── Zone 4 : SOLEIL | AQI+UV (2 panneaux) ──────────────────────────────
const int Z4H = 70;
const int HALF = (W - PAD * 3) / 2;
drawSunSection (tft, PAD, curY, HALF, Z4H);
drawAqiUvSection (tft, PAD * 2 + HALF, curY, HALF, Z4H);
curY += Z4H + PAD;
// ── Zone 5 : Graphique températures 24h ─────────────────────────────────
const int FORECAST_H = 38;
int chartH = SCREEN_H - curY - FORECAST_H - PAD - 2;
if (chartH >= 20) {
drawHourlyChart(tft, 0, curY, W, chartH);
curY += chartH + PAD;
}
// ── Zone 6 : Prévisions 5 jours ─────────────────────────────────────────
int z6H = SCREEN_H - curY;
if (z6H >= 18)
drawForecastBar(tft, 0, curY, W, z6H);
}
void initWeatherTab()
{
// Widgets en colonnes désactivés : ils dessinaient une disposition
// différente (colonne gauche/droite) par-dessus le layout plein-largeur
// de drawWeatherTab(), à des intervalles différents (10s/30s/60s),
// ce qui donnait une deuxième courbe fantôme en arrière-plan.
// drawWeatherTab() dessine désormais seul tout l'onglet Météo.
/*
int contentY = TAB_BAR_HEIGHT + 2;
int contentH = SCREEN_H - contentY;
mainTempWidget = new UpdatableMainTempWidget(0, contentY, LEFT_W, contentH);
forecastWidget = new UpdatableForecastWidget(RIGHT_X, contentY, RIGHT_W, 68);
chartWidget = new UpdatableChartWidget(RIGHT_X, contentY + 73, RIGHT_W, 130);
sunWidget = new UpdatableSunWidget(RIGHT_X, contentY + 208, RIGHT_W / 2 - 2, contentH - 208);
aqiWidget = new UpdatableAQIWidget(RIGHT_X + RIGHT_W / 2 + 2, contentY + 208,
RIGHT_W / 2 - 2, contentH - 208);
weatherWidgetManager.addWidget(mainTempWidget);
weatherWidgetManager.addWidget(forecastWidget);
weatherWidgetManager.addWidget(chartWidget);
weatherWidgetManager.addWidget(sunWidget);
weatherWidgetManager.addWidget(aqiWidget);
*/
}
void updateWeatherTab()
{
// Plus rien à mettre à jour ici : le rafraîchissement de l'onglet Météo
// passe uniquement par drawWeatherTab(), appelé via TabManager::forceRedraw()
// quand randomizeData() change les valeurs (voir main.cpp / loop1()).
/*
extern TFT_eSPI tft;
weatherWidgetManager.updateAll(tft, false);
*/
}
// ════════════════════════════════════════════════════════════════════════════
// ONGLET DEBUG
// ════════════════════════════════════════════════════════════════════════════
void drawDebugTab(TFT_eSPI& tft)
{
int contentY = TAB_BAR_HEIGHT + 2;
int startX = 5;
int width = SCREEN_W - 10;
int textH = 12;
// Efface toute la zone de contenu (pas seulement les cartes) pour ne
// jamais laisser de marge/interstice montrer un reliquat d'un autre onglet.
tft.fillRect(0, contentY - 2, SCREEN_W, SCREEN_H - contentY + 2, C_BG);
tft.fillRoundRect(startX, contentY, width, 50, 3, C_PANEL);
tft.drawRoundRect(startX, contentY, width, 50, 3, C_BORDER);
tft.setTextSize(1);
tft.setTextColor(C_ACCENT_BLUE, C_PANEL);
tft.setCursor(startX + 5, contentY + 5);
tft.print("SYSTEME: OK | Uptime: ");
tft.print(millis() / 60000);
tft.print(" min");
tft.setCursor(startX + 5, contentY + 18);
tft.print("Memoire libre: ");
tft.print(rp2040.getFreeHeap());
tft.print(" bytes");
tft.setCursor(startX + 5, contentY + 31);
tft.print("CPU: ");
tft.print(clock_get_hz(clk_sys) / 1000000);
tft.print(" MHz");
int logStartY = contentY + 60;
int logH = SCREEN_H - logStartY - 10;
tft.fillRoundRect(startX, logStartY, width, logH, 3, C_PANEL);
tft.drawRoundRect(startX, logStartY, width, logH, 3, C_BORDER);
tft.setTextColor(C_MUTED, C_PANEL);
tft.setCursor(startX + 5, logStartY + 5);
tft.print("LOGS:");
int lineY = logStartY + 18;
int lineNum = 0;
static char tmpBuf[2048];
strncpy(tmpBuf, debugBuffer, sizeof(tmpBuf) - 1);
tmpBuf[sizeof(tmpBuf) - 1] = '\0';
char* line = strtok(tmpBuf, "\n");
while (line && lineNum < 10) {
tft.setTextColor(C_WHITE, C_PANEL);
tft.setCursor(startX + 3, lineY);
tft.print(line);
lineY += textH;
lineNum++;
line = strtok(nullptr, "\n");
}
}
void updateDebugTab()
{
static unsigned long lastDebugUpdate = 0;
if (millis() - lastDebugUpdate > 5000) {
lastDebugUpdate = millis();
addDebugMessage("T=%dC H=%d%% P=%dhPa",
gWeather.temp, gWeather.humidity, gWeather.pressure);
TabManager::forceRedraw();
}
}
// ════════════════════════════════════════════════════════════════════════════
// ONGLET TEMPÉRATURE INTÉRIEURE
// ════════════════════════════════════════════════════════════════════════════
void drawIndoorTab(TFT_eSPI& tft)
{
int contentY = TAB_BAR_HEIGHT + 2;
int startX = 10;
int cardWidth = (SCREEN_W - 40) / 2;
int cardHeight = 110;
// Efface toute la zone de contenu (pas seulement les cartes) pour ne
// jamais laisser de marge/interstice montrer un reliquat d'un autre onglet.
tft.fillRect(0, contentY - 2, SCREEN_W, SCREEN_H - contentY + 2, C_BG);
tft.setTextSize(2);
tft.setTextColor(C_WHITE, C_BG);
tft.setCursor(startX, contentY + 5);
tft.print("CAPTEURS INTERIEURS");
tft.drawFastHLine(startX, contentY + 28, SCREEN_W - 20, C_BORDER);
for (int i = 0; i < gSensorCount && i < 4; i++) {
int col = i % 2, row = i / 2;
int cx = startX + col * (cardWidth + 10);
int cy = contentY + 40 + row * (cardHeight + 10);
uint16_t bg = gSensors[i].connected ? C_PANEL : 0x28A4; // #2a1620 — panneau "hors ligne"
tft.fillRoundRect(cx, cy, cardWidth, cardHeight, 5, bg);
tft.drawRoundRect(cx, cy, cardWidth, cardHeight, 5, C_BORDER);
tft.setTextSize(1);
tft.setTextColor(C_ACCENT_BLUE, bg);
tft.setCursor(cx + 10, cy + 8);
tft.print(gSensors[i].name);
tft.setTextSize(3);
uint16_t tc = (gSensors[i].temperature < 18) ? C_ACCENT_BLUE :
(gSensors[i].temperature > 25) ? C_ACCENT_RED : C_ACCENT_GOLD;
tft.setTextColor(tc, bg);
tft.setCursor(cx + 10, cy + 35);
char ts[10]; dtostrf(gSensors[i].temperature, 4, 1, ts);
tft.print(ts);
tft.setTextSize(1);
tft.print("C");
tft.setTextSize(1);
tft.setTextColor(C_MUTED, bg);
tft.setCursor(cx + 10, cy + 75);
tft.print("Batterie: "); tft.print(gSensors[i].battery); tft.print("%");
tft.fillCircle(cx + cardWidth - 15, cy + 15, 5,
gSensors[i].connected ? C_ACCENT_GREEN : C_ACCENT_RED);
}
tft.setTextSize(1);
tft.setTextColor(C_DIM, C_BG);
tft.setCursor(startX, SCREEN_H - 15);
tft.print("U/D: Vue | OK: Rafraichir");
}
void updateIndoorTab()
{
static unsigned long lastTempUpdate = 0;
extern TFT_eSPI tft;
if (millis() - lastTempUpdate > 3000) {
lastTempUpdate = millis();
for (int i = 0; i < gSensorCount; i++) {
if (gSensors[i].connected) {
float delta = (random(-30, 30)) / 100.0f;
gSensors[i].temperature = constrain(
gSensors[i].temperature + delta, 10.0f, 30.0f);
if (random(100) < 10 && gSensors[i].battery > 0)
gSensors[i].battery = max(0, gSensors[i].battery - 1);
}
}
TabManager::forceRedraw();
}
}
// ════════════════════════════════════════════════════════════════════════════
// ONGLET STATISTIQUES (capteur BME280)
// ════════════════════════════════════════════════════════════════════════════
void drawStatsTab(TFT_eSPI& tft)
{
int contentY = TAB_BAR_HEIGHT + 2;
int startX = MARGIN * 2;
int cardWidth = (SCREEN_W - MARGIN * 5) / 2;
int cardHeight = 70;
int cardSpacing = MARGIN;
tft.fillRect(0, contentY, SCREEN_W, SCREEN_H - contentY, C_BG);
tft.setTextSize(2);
if (bmeInitialized) {
tft.setTextColor(C_ACCENT_GREEN, C_BG);
tft.setCursor(startX, contentY + 5);
tft.print("CONNECTE");
} else {
tft.setTextColor(C_ACCENT_RED, C_BG);
tft.setCursor(startX, contentY + 5);
tft.print("DECONNECTE");
}
tft.drawFastHLine(startX, contentY + 28, SCREEN_W - MARGIN * 4, C_BORDER);
if (!bmeInitialized) {
tft.setTextSize(1);
tft.setTextColor(C_MUTED, C_BG);
tft.setCursor(startX, contentY + 50);
tft.print("Verifier cablage I2C:");
tft.setCursor(startX, contentY + 65);
tft.print("SDA=GPIO2 SCL=GPIO3");
return;
}
int row1Y = contentY + 42;
int row2Y = row1Y + cardHeight + cardSpacing;
tft.fillRoundRect(startX, row1Y, cardWidth, cardHeight, RADIUS, C_PANEL);
tft.drawRoundRect(startX, row1Y, cardWidth, cardHeight, RADIUS, C_BORDER);
tft.setTextSize(1);
tft.setTextColor(C_MUTED, C_PANEL);
tft.setCursor(startX + 8, row1Y + 8);
tft.print("TEMPERATURE");
uint16_t tc = (bmeTemperature > 25) ? C_ACCENT_RED :
(bmeTemperature < 15) ? C_ACCENT_BLUE : C_ACCENT_GREEN;
tft.setTextSize(3);
tft.setTextColor(tc, C_PANEL);
tft.setCursor(startX + 8, row1Y + 30);
char tempStr[10]; dtostrf(bmeTemperature, 4, 1, tempStr);
tft.print(tempStr); tft.setTextSize(1); tft.print("C");
int humX = startX + cardWidth + cardSpacing;
tft.fillRoundRect(humX, row1Y, cardWidth, cardHeight, RADIUS, C_PANEL);
tft.drawRoundRect(humX, row1Y, cardWidth, cardHeight, RADIUS, C_BORDER);
tft.setTextSize(1);
tft.setTextColor(C_MUTED, C_PANEL);
tft.setCursor(humX + 8, row1Y + 8);
tft.print("HUMIDITE");
uint16_t hc = (bmeHumidity > 70) ? C_ACCENT_BLUE :
(bmeHumidity < 30) ? C_ACCENT_GOLD : C_ACCENT_GREEN;
tft.setTextSize(3);
tft.setTextColor(hc, C_PANEL);
tft.setCursor(humX + 8, row1Y + 30);
char humStr[10]; dtostrf(bmeHumidity, 4, 0, humStr);
tft.print(humStr); tft.setTextSize(1); tft.print("%");
tft.fillRoundRect(startX, row2Y, cardWidth, cardHeight, RADIUS, C_PANEL);
tft.drawRoundRect(startX, row2Y, cardWidth, cardHeight, RADIUS, C_BORDER);
tft.setTextSize(1);
tft.setTextColor(C_MUTED, C_PANEL);
tft.setCursor(startX + 8, row2Y + 8);
tft.print("PRESSION");
tft.setTextSize(2);
tft.setTextColor(C_ACCENT_GOLD, C_PANEL);
tft.setCursor(startX + 8, row2Y + 30);
char pressStr[10]; dtostrf(bmePressure, 5, 1, pressStr);
tft.print(pressStr); tft.setTextSize(1); tft.print("hPa");
int altX = startX + cardWidth + cardSpacing;
tft.fillRoundRect(altX, row2Y, cardWidth, cardHeight, RADIUS, C_PANEL);
tft.drawRoundRect(altX, row2Y, cardWidth, cardHeight, RADIUS, C_BORDER);
tft.setTextSize(1);
tft.setTextColor(C_MUTED, C_PANEL);
tft.setCursor(altX + 8, row2Y + 8);
tft.print("ALTITUDE");
float altitude = 44330.0f * (1.0f - powf(bmePressure / 1013.25f, 0.1903f));
tft.setTextSize(2);
tft.setTextColor(C_ACCENT_GREEN, C_PANEL);
tft.setCursor(altX + 8, row2Y + 30);
tft.print((int)altitude); tft.setTextSize(1); tft.print("m");
tft.setTextSize(1);
tft.setTextColor(C_DIM, C_BG);
tft.setCursor(startX, SCREEN_H - 15);
tft.print("Mise a jour: "); tft.print(millis() / 1000); tft.print("s");
}
void updateStatsTab()
{
static unsigned long lastBMEUpdate = 0;
if (millis() - lastBMEUpdate > BME280_UPDATE_MS) {
lastBMEUpdate = millis();
if (bmeInitialized) TabManager::forceRedraw();
}
}
// ════════════════════════════════════════════════════════════════════════════
// BARRE D'ONGLETS
// ════════════════════════════════════════════════════════════════════════════
void drawTabBar(TFT_eSPI& tft, int currentTab)
{
const char* tabs[] = {"Meteo", "Debug", "Interieur", "Statistiques"};
const int numTabs = TAB_COUNT;
const int tabWidth = SCREEN_W / numTabs;
tft.fillRect(0, 0, SCREEN_W, 30, C_BG);
tft.drawFastHLine(0, 30, SCREEN_W, C_BORDER);
for (int i = 0; i < numTabs; i++) {
int tabX = i * tabWidth;
uint16_t color = (i == currentTab) ? C_ACCENT_BLUE : C_MUTED;
tft.setTextSize(1);
tft.setTextColor(color, C_BG);
// Centrer le label dans la tab
int labelW = (int)strlen(tabs[i]) * 6;
tft.setCursor(tabX + max(0, (tabWidth - labelW) / 2), 10);
tft.print(tabs[i]);
if (i == currentTab)
tft.drawFastHLine(tabX, 28, tabWidth, C_ACCENT_BLUE);
}
}