#include <Arduino.h>
#include <TFT_eSPI.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <HTTPClient.h>
#include <WiFiClient.h>
#include <math.h>
// #include "audio/AudioPlayer.h" // ← Audio désactivé pour l'instant
#include "tools/BME280.h"
#include "drivers/PCF8574_Handler.h"
#include "network/NTP_Time.h"
#include "hardware/clocks.h"
#include "Theme.h"
#include "WeatherData.h"
#include "Widget.h"
#include "Widgets.h"
#include "TabManager.h"
#include "Tabs.h"
// ─── Données météo globales ────────────────────────────────────────────────
WeatherData gWeather = {
19, 17, 14, 21,
72, 18, "SO",
1013, 4, 42,
"Partiellement nuageux",
"Lorient, FR",
"Jeu. 29 mai 14:32",
"06:12", "21:47"
};
DayForecast gForecast[5] = {
{"Ven", 16, 2}, {"Sam", 20, 3}, {"Dim", 23, 0},
{"Lun", 18, 4}, {"Mar", 19, 1},
};
HourlyPoint gHourly[8] = {
{"00", 13}, {"03", 12}, {"06", 13}, {"09", 16},
{"12", 19}, {"15", 21}, {"18", 20}, {"21", 17}
};
// ─── Widgets météo — définis dans Tabs.cpp ───────────────────────────────────
extern UpdatableMainTempWidget* mainTempWidget;
extern UpdatableForecastWidget* forecastWidget;
extern UpdatableChartWidget* chartWidget;
extern UpdatableSunWidget* sunWidget;
extern UpdatableAQIWidget* aqiWidget;
// ─── Configuration Wi-Fi ─────────────────────────────────────────────────────
const char* ssid = "SFR_8A40";
const char* password = "biosdu56";
// ─── Pins SD (SPI0) ──────────────────────────────────────────────────────────
#define SD_CS_PIN 20
#define SD_MISO_PIN 16
#define SD_MOSI_PIN 19
#define SD_SCK_PIN 18
// ─── Pins I2S (audio désactivé pour l'instant) ───────────────────────────────
// #define AUDIO_DIN_PIN 26
// #define AUDIO_BCK_PIN 27
// #define AUDIO_LCK_PIN 28
// ─── Boutons PCF8574 ─────────────────────────────────────────────────────────
#define BUTTON_COUNT 5
#define INTERRUPT_BUTTON_PIN 21
#define BTN_LEFT 0
#define BTN_RIGHT 1
#define BTN_CENTER 4
#define BTN_UP 2
#define BTN_DOWN 3
using namespace crepp::tools;
// using namespace crepp::audio; // ← Audio désactivé pour l'instant
using namespace crepp::drivers;
using namespace crepp::network;
// AudioPlayer audio(AUDIO_DIN_PIN, AUDIO_BCK_PIN, AUDIO_LCK_PIN); // ← Audio désactivé pour l'instant
TFT_eSPI tft = TFT_eSPI();
PCF8574_Handler buttons;
// ─── NTP_Time ────────────────────────────────────────────────────────────────
NTP_Time* ntpTime = nullptr;
// ─── Synchronisation Core 0 → Core 1 ─────────────────────────────────────────
#include <atomic>
std::atomic<bool> audioReady(true); // ← true en dur : plus d'init audio à attendre
// std::atomic<bool> audioPlaying(false); // ← Audio désactivé pour l'instant
std::atomic<bool> timeInitialized(false);
// ─── Boutons ─────────────────────────────────────────────────────────────────
volatile bool interruptFlag = false;
unsigned long lastButtonState = 0xFF;
void onInterrupt() { interruptFlag = true; }
// ─── BME280 ──────────────────────────────────────────────────────────────────
BME280 bmeSensor;
bool bmeInitialized = false;
float bmeTemperature = 0.0f;
float bmeHumidity = 0.0f;
float bmePressure = 0.0f;
// ─── Mise à jour heure + date ────────────────────────────────────────────────
// Remplit gWeather.time avec "Jeu. 29 mai 14:32"
static const char* DAY_NAMES[] = {"Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam"};
static const char* MONTH_NAMES[] = {
"jan", "fev", "mar", "avr", "mai", "jun",
"jul", "aou", "sep", "oct", "nov", "dec"
};
void updateTime()
{
if (!ntpTime || !ntpTime->isTimeSynchronized()) {
Serial.println("[NTP] Non synchronise");
return;
}
DateTime now = ntpTime->getCurrentTime();
if (!now.isValid()) {
Serial.println("[NTP] DateTime invalide");
return;
}
// Calcul du jour de la semaine (algorithme de Tomohiko Sakamoto)
int y = now.year, m = now.month, d = now.day;
static const int t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
if (m < 3) y--;
int dow = (y + y/4 - y/100 + y/400 + t[m-1] + d) % 7;
static char timeBuffer[48];
snprintf(timeBuffer, sizeof(timeBuffer),
"%s. %d %s %02d:%02d",
DAY_NAMES[dow],
now.day,
MONTH_NAMES[now.month - 1],
now.hour,
now.minute);
gWeather.time = timeBuffer;
Serial.print("[NTP] Heure: ");
Serial.println(gWeather.time);
}
// ─── SD Card ─────────────────────────────────────────────────────────────────
void initSDCard()
{
Serial.println("\n=== Initialisation SD (SPI0) ===");
pinMode(SD_CS_PIN, OUTPUT);
digitalWrite(SD_CS_PIN, HIGH);
SPI.setRX(SD_MISO_PIN);
SPI.setTX(SD_MOSI_PIN);
SPI.setSCK(SD_SCK_PIN);
SPI.begin();
delay(100);
if (!SD.begin(SD_CS_PIN, SPI)) {
Serial.println("ERREUR: SD echouee!");
return;
}
Serial.println("SUCCES: SD initialisee sur SPI0");
File dir = SD.open("/");
while (true) {
File entry = dir.openNextFile();
if (!entry) break;
if (!entry.isDirectory()) {
String name = entry.name();
String nameLower = name;
nameLower.toLowerCase();
if (nameLower.endsWith(".wav") || nameLower.endsWith(".mp3")) {
Serial.print(" [audio] ");
Serial.print(name);
Serial.print(" (");
Serial.print(entry.size() / 1024);
Serial.println(" KB)");
}
}
entry.close();
}
dir.close();
}
// ─── Audio (désactivé pour l'instant) ────────────────────────────────────────
/*
void setupAndPlayAudio()
{
Serial.println("\n=== Initialisation Audio ===");
pinMode(22, OUTPUT);
digitalWrite(22, HIGH);
delay(100);
if (!audio.begin(0.03f)) {
Serial.println("ERREUR: Audio I2S echoue");
audioReady.store(true);
return;
}
Serial.println("Audio I2S OK");
const char* candidates[] = {
"/MUSIC.WAV", "/music.wav",
"/AUDIO.WAV", "/audio.wav",
"/MUSIC.MP3", "/music.mp3",
"/AUDIO.MP3", "/audio.mp3"
};
bool loaded = false;
for (const char* path : candidates) {
if (SD.exists(path)) {
loaded = audio.load(path);
if (loaded) {
Serial.print("[Audio] Charge: ");
Serial.println(path);
break;
}
}
}
if (!loaded) {
Serial.println("[Audio] Aucun fichier trouve");
audioReady.store(true);
return;
}
audio.play();
audioPlaying.store(true);
Serial.println("[Audio] Lecture demarree");
audioReady.store(true);
}
*/
// ─── BME280 ──────────────────────────────────────────────────────────────────
void initBME280()
{
if (bmeSensor.begin(0x76)) {
bmeInitialized = true;
bmeTemperature = bmeSensor.getTemperature();
bmeHumidity = bmeSensor.getHumidity();
bmePressure = bmeSensor.getPressure();
Serial.println("BME280 OK");
} else {
Serial.println("BME280 non trouve (0x76)");
}
}
// ─── Mise à jour données météo ────────────────────────────────────────────────
void randomizeData()
{
if (bmeInitialized) {
bmeTemperature = bmeSensor.getTemperature();
bmeHumidity = bmeSensor.getHumidity();
bmePressure = bmeSensor.getPressure();
gWeather.temp = (int)round(bmeTemperature);
gWeather.feelsLike = gWeather.temp - random(0, 2);
gWeather.tempMin = gWeather.temp - random(1, 3);
gWeather.tempMax = gWeather.temp + random(1, 4);
gWeather.humidity = (int)round(bmeHumidity);
gWeather.pressure = (int)round(bmePressure);
gWeather.condition = (bmeHumidity > 80) ? "Humide" :
(bmeHumidity < 40) ? "Sec" : "Agreable";
} else {
gWeather.temp = random(10, 28);
gWeather.feelsLike = gWeather.temp - random(1, 4);
gWeather.tempMin = gWeather.temp - random(3, 7);
gWeather.tempMax = gWeather.temp + random(1, 5);
gWeather.humidity = random(45, 95);
gWeather.pressure = random(1000, 1030);
gWeather.condition = "Simule";
}
gWeather.windSpeed = random(5, 40);
gWeather.uvIndex = random(0, 10);
gWeather.aqi = random(10, 120);
int base = gWeather.tempMin;
for (int i = 0; i < 8; i++) {
float angle = (i * 3 * PI) / 12.0f;
int variation = (int)(5 * sin(angle - PI / 2));
gHourly[i].temp = base + variation + random(-1, 2);
}
static unsigned long lastTimeUpdate = 0;
unsigned long now = millis();
if (now - lastTimeUpdate > 60000 && timeInitialized.load()) {
lastTimeUpdate = now;
updateTime();
}
}
// ─── Gestion boutons ──────────────────────────────────────────────────────────
void checkButtons()
{
if (!interruptFlag) return;
interruptFlag = false;
delay(20);
uint8_t currentButtonState = 0;
for (uint8_t i = 0; i < BUTTON_COUNT; i++) {
if (buttons.read(i) == LOW) currentButtonState &= ~(1 << i);
else currentButtonState |= (1 << i);
}
uint8_t pressedButtons = (~currentButtonState) & lastButtonState;
for (uint8_t i = 0; i < BUTTON_COUNT; i++) {
if (pressedButtons & (1 << i)) {
Serial.print("[BTN] ");
Serial.print(i);
Serial.println(" PRESSE");
if (i == BTN_CENTER) {
// Audio désactivé pour l'instant — le bouton central déclenche
// maintenant un rafraîchissement manuel de l'onglet courant
// (voir TabManager::handleButton, case CENTER).
/*
if (audioPlaying.load()) {
audio.pause();
audioPlaying.store(false);
Serial.println("[Audio] Pause");
} else {
audio.play();
audioPlaying.store(true);
Serial.println("[Audio] Play");
}
*/
TabManager::handleButton(BTN_CENTER, false);
TabManager::forceRedraw();
}
else if (i == BTN_LEFT) { TabManager::handleButton(BTN_LEFT, false); TabManager::forceRedraw(); }
else if (i == BTN_RIGHT) { TabManager::handleButton(BTN_RIGHT, false); TabManager::forceRedraw(); }
else if (i == BTN_UP) { TabManager::handleButton(BTN_UP, false); TabManager::forceRedraw(); }
else if (i == BTN_DOWN) { TabManager::handleButton(BTN_DOWN, false); TabManager::forceRedraw(); }
}
}
lastButtonState = currentButtonState;
}
// ─── Core 1 : TFT + Boutons ──────────────────────────────────────────────────
void setup1()
{
// Audio désactivé : audioReady vaut désormais true en dur, donc cette
// boucle ne bloque plus. Gardée en commentaire si l'audio est réactivé.
// while (!audioReady.load()) delay(10);
Serial.println("[Core1] Demarrage TFT + Boutons");
Wire.begin();
initBME280();
buttons.begin();
pinMode(INTERRUPT_BUTTON_PIN, INPUT_PULLUP);
for (uint8_t i = 0; i < BUTTON_COUNT; i++) {
if (buttons.read(i) == LOW) lastButtonState &= ~(1 << i);
else lastButtonState |= (1 << i);
}
attachInterrupt(digitalPinToInterrupt(INTERRUPT_BUTTON_PIN), onInterrupt, FALLING);
Serial.println("[Core1] Boutons OK");
tft.init();
tft.invertDisplay(true);
// ── Routine de défatigage LCD (anti-rémanence) ───────────────────────
// Sur les dalles TN/IPS bon marché type ILI9488, un contenu statique
// affiché longtemps (ex. une valeur figée) peut laisser une trace
// temporaire ("image sticking") même après un effacement logiciel
// complet. Quelques cycles rapides blanc/noir/inversé au démarrage
// aident les cristaux liquides à se rééquilibrer et accélèrent la
// disparition de ce type de rémanence.
tft.setRotation(1);
for (int i = 0; i < 5; i++) {
tft.fillScreen(TFT_WHITE);
delay(100);
tft.fillScreen(TFT_BLACK);
delay(100);
tft.fillScreen(TFT_WHITE);
delay(100);
tft.fillScreen(TFT_BLACK);
delay(100);
tft.invertDisplay(false);
tft.fillScreen(TFT_WHITE);
delay(100);
tft.fillScreen(TFT_BLACK);
delay(100);
tft.fillScreen(TFT_WHITE);
delay(100);
tft.invertDisplay(true);
tft.fillScreen(TFT_BLACK);
delay(100);
tft.fillScreen(TFT_WHITE);
delay(100);
tft.fillScreen(TFT_BLACK);
delay(100);
}
// ── Effacement complet et robuste ────────────────────────────────────
// Sur certains modules ILI9488, la fenêtre d'adressage utilisée par
// fillScreen() (basée sur tft.width()/height()) n'est pas toujours
// recalée correctement juste après un changement de rotation : une
// partie du panneau physique peut alors ne jamais être réécrite, d'où
// un résidu visible (ex. la courbe de l'ancien onglet "Marées" d'un
// firmware précédent) qui persiste même après un fillScreen() classique.
// On force ici un effacement avec les dimensions NATIVES explicites
// (celles du platformio.ini), en portrait ET en paysage, avant de fixer
// la rotation finale.
tft.setRotation(0);
tft.fillRect(0, 0, TFT_WIDTH, TFT_HEIGHT, TFT_BLACK); // 320x480 natif portrait
tft.setRotation(1);
tft.fillRect(0, 0, TFT_HEIGHT, TFT_WIDTH, TFT_BLACK); // 480x320 natif paysage
tft.fillScreen(C_BG);
tft.fillScreen(C_BG); // second passage de sécurité
TabManager::init();
initWeatherTab();
TabManager::draw(tft);
// L'écran est maintenant effacé et le premier onglet dessiné :
// on peut allumer le rétroéclairage sans montrer de résidu de l'ancien firmware.
digitalWrite(TFT_BL, HIGH);
Serial.println("[Core1] Pret");
}
void loop1()
{
checkButtons();
static unsigned long lastWeatherUpdate = 0;
static unsigned long lastTabUpdate = 0;
static unsigned long lastDraw = 0;
unsigned long now = millis();
int updateInterval = bmeInitialized ? 5000 : 10000;
if (now - lastWeatherUpdate > (unsigned long)updateInterval) {
lastWeatherUpdate = now;
randomizeData();
if (mainTempWidget) mainTempWidget->markDirty();
if (forecastWidget) forecastWidget->markDirty();
if (chartWidget) chartWidget->markDirty();
if (sunWidget) sunWidget->markDirty();
if (aqiWidget) aqiWidget->markDirty();
if (TabManager::getCurrentTab() == TAB_WEATHER)
TabManager::forceRedraw();
}
if (now - lastTabUpdate > 500) {
lastTabUpdate = now;
TabManager::update();
}
if (now - lastDraw > 50) {
lastDraw = now;
TabManager::draw(tft);
}
delay(10);
}
// ─── Fake NTP pour tests ──────────────────────────────────────────────────────
class FakeNTP_Time : public NTP_Time {
public:
FakeNTP_Time() : NTP_Time("", "") {}
bool begin(int timeout_ms = 30000) override { return true; }
bool isTimeSynchronized() const override { return true; }
bool maintainConnection() override { return true; }
DateTime getCurrentTime() override {
// ← Modifie cette ligne pour changer l'heure de test
return DateTime(2025, 6, 3, 14, 0, 0, 1); // mercredi 3 juin 2026, 14:00
}
};
// ─── Core 0 : SD + Audio + Wi-Fi + NTP ──────────────────────────────────────
void setup()
{
Serial.begin(115200);
delay(500);
Serial.println("\n=== CORE 0 : SD + Audio + Wi-Fi + NTP ===");
pinMode(TFT_BL, OUTPUT);
digitalWrite(TFT_BL, LOW); // reste éteint tant que l'écran n'est pas effacé (voir setup1())
initSDCard();
// ntpTime = new NTP_Time(ssid, password);
ntpTime = new FakeNTP_Time(); // ← heure bidon
if (ntpTime->begin()) {
updateTime();
timeInitialized.store(true);
} else {
timeInitialized.store(false);
}
if (ntpTime->begin(30000)) {
Serial.println("[NTP] Synchronise");
updateTime();
timeInitialized.store(true);
} else {
Serial.println("[NTP] Echec synchronisation");
timeInitialized.store(false);
}
// setupAndPlayAudio(); // ← Audio désactivé pour l'instant
Serial.println("\n=== CORE 0 PRET ===");
}
void loop()
{
// audio.loop(); // ← Audio désactivé pour l'instant
static unsigned long lastMaintenance = 0;
static unsigned long lastTimeUpdate = 0;
unsigned long now = millis();
// Maintenance NTP toutes les 30s
if (now - lastMaintenance > 30000) {
lastMaintenance = now;
if (ntpTime) {
ntpTime->maintainConnection();
if (ntpTime->isTimeSynchronized() && !timeInitialized.load()) {
timeInitialized.store(true);
updateTime();
TabManager::forceRedraw();
}
}
}
// Mise à jour heure toutes les minutes
if (now - lastTimeUpdate > 60000 && timeInitialized.load()) {
lastTimeUpdate = now;
updateTime();
}
}