projet crepp_git/crepp-projects/station-meteo/firmware/receiver/v1.0 · branche main
#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();
    }
}