/*********************************************************************
* LUMO‑ESP32 – Menu multi‑apps avec connexion Wi‑Fi au démarrage *
*********************************************************************/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <WiFi.h>
#include <WebServer.h>
#include <DNSServer.h>
#include <Preferences.h>
#include <Adafruit_NeoPixel.h>
#include "intro.h"
#include "content.h" // Animation custom
/* --------------------------- DISPLAY --------------------------- */
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
/* --------------------------- PINS ----------------------------- */
const int potentiometerPin = A0;
const int buttonOkPin = 2;
const int buttonBackPin = 3;
/* --------------------------- MENU ------------------------------ */
const int numOptions = 5; // nombre d’options du menu
String menuItems[numOptions] = {
"Pomodoro",
"WiFi Scanner",
"Animation",
"Config",
"LED Control"
};
int selectedIndex = 0;
/* --------------------------- STATE ----------------------------- */
enum AppState {
MENU,
APP_POMODORO,
APP_WIFI,
APP_ANIMATION,
APP_CONFIG,
APP_LED
};
AppState currentState = MENU;
/* --------------------------- BUTTONS --------------------------- */
unsigned long lastButtonPress = 0;
bool isButtonPressed(int pin) {
if (digitalRead(pin) == LOW && millis() - lastButtonPress > 200) {
lastButtonPress = millis();
return true;
}
return false;
}
/* --------------------------- LED (WS2812) ---------------------- */
#define LED_PIN 5
#define LED_COUNT 1
Adafruit_NeoPixel led(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
void setLedColor(uint8_t r, uint8_t g, uint8_t b) {
led.setPixelColor(0, led.Color(r, g, b));
led.show();
}
/* --------------------------- POMODORO -------------------------- */
unsigned long pomodoroStart = 0;
bool pomodoroRunning = false;
bool pomodoroPaused = false;
unsigned long elapsedBeforePause = 0;
int pomodoroPhase = 0; // 0 = Travail, 1 = Pause, 2 = Long break
/* --------------------------- ANIMATION -------------------------- */
unsigned long lastAnimUpdate = 0;
int currentFrame = 0;
/* --------------------------- WIFI SCANNER ----------------------- */
int nNetworks = 0;
String wifiList[6]; // max 6 réseaux affichés
/* --------------------------- STORAGE ---------------------------- */
Preferences prefs;
const char* PREF_NAMESPACE = "esp_config";
const char* KEY_SSID = "ssid";
const char* KEY_PASS = "pass";
/* --------------------------- CAPTIVE PORTAL -------------------- */
const byte DNS_PORT = 53;
DNSServer dnsServer;
WebServer webServer(80);
const char* apSSID = "ESP-PROD";
const char* apPassword = "Sup2rMdppasseD3B1ta/rd";
bool captiveActive = false;
/* --------------------------- WIFI CONNECT (START) --------------- */
// SSID / password du réseau auquel on veut se connecter au démarrage
const char* STA_SSID = "Saturn-hotspot";
const char* STA_PASS = "84903334";
bool wifiConnected = false; // true → “wifi ok”, false → “wifi no”
bool connectToWifi() {
WiFi.mode(WIFI_STA);
WiFi.begin(STA_SSID, STA_PASS);
unsigned long startAttemptTime = millis();
while (WiFi.status() != WL_CONNECTED &&
millis() - startAttemptTime < 10000) { // 10 s max
delay(250);
}
return WiFi.status() == WL_CONNECTED;
}
/* --------------------------- INTRO ------------------------------ */
void playIntro() {
for (int i = 0; i < INTRO_FRAME_COUNT; i++) {
display.clearDisplay();
display.drawBitmap(0, 0, intro[i], INTRO_WIDTH, INTRO_HEIGHT, SSD1306_WHITE);
display.display();
delay(intro_delays[i]);
}
}
/* --------------------------- MENU DRAW -------------------------- */
void drawMenu() {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.println("== MENU ==");
// ----- statut Wi‑Fi à droite -----
const char* wifiStatus = wifiConnected ? "wifi ok" : "wifi no";
int16_t x1, y1;
uint16_t w, h;
display.getTextBounds(wifiStatus, 0, 0, &x1, &y1, &w, &h);
display.setCursor(SCREEN_WIDTH - w - 2, 0);
display.println(wifiStatus);
// ---------------------------------
// ----- liste des options -----
for (int i = 0; i < numOptions; i++) {
if (i == selectedIndex) {
display.fillRect(0, 16 + i * 10, SCREEN_WIDTH, 10, SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK);
display.setCursor(2, 16 + i * 10);
display.println(menuItems[i]);
display.setTextColor(SSD1306_WHITE);
} else {
display.setCursor(2, 16 + i * 10);
display.println(menuItems[i]);
}
}
// ------------------------------
display.display();
}
/* --------------------------- MENU NAVIGATION ------------------- */
void handleMenuNavigation() {
int potValue = analogRead(potentiometerPin);
int newIndex = map(potValue, 0, 1023, 0, numOptions);
if (newIndex != selectedIndex && newIndex < numOptions) {
selectedIndex = newIndex;
drawMenu();
}
if (isButtonPressed(buttonOkPin)) {
switch (selectedIndex) {
case 0: currentState = APP_POMODORO; pomodoroPhase = 0; startPomodoro(); break;
case 1: currentState = APP_WIFI; scanWifi(); break;
case 2: currentState = APP_ANIMATION; currentFrame = 0; break;
case 3: currentState = APP_CONFIG; startConfigApp(); break;
case 4: currentState = APP_LED; break;
}
display.clearDisplay();
}
}
/* --------------------------- POMODORO -------------------------- */
void startPomodoro() {
pomodoroStart = millis();
elapsedBeforePause = 0;
pomodoroRunning = true;
pomodoroPaused = false;
}
void runPomodoroApp() {
display.clearDisplay();
String phaseName;
unsigned long phaseDuration;
switch (pomodoroPhase) {
case 0: phaseName = "Travail"; phaseDuration = 25UL * 60UL * 1000UL; setLedColor(255, 0, 0); break;
case 1: phaseName = "Pause"; phaseDuration = 5UL * 60UL * 1000UL; setLedColor(0, 255, 0); break;
case 2: phaseName = "Long Break";phaseDuration = 15UL * 60UL * 1000UL; setLedColor(255, 0, 0); break;
}
display.setTextSize(1);
display.setCursor(0,0);
display.println("== " + phaseName + " ==");
if (pomodoroRunning && !pomodoroPaused) {
unsigned long elapsed = millis() - pomodoroStart + elapsedBeforePause;
unsigned long remaining = (phaseDuration > elapsed) ? (phaseDuration - elapsed) : 0;
int minutes = remaining / 60000;
int seconds = (remaining % 60000) / 1000;
display.setTextSize(3);
String timeStr = String(minutes) + ":" + (seconds < 10 ? "0" : "") + String(seconds);
int16_t x1, y1; uint16_t w, h;
display.getTextBounds(timeStr, 0, 0, &x1, &y1, &w, &h);
display.setCursor((SCREEN_WIDTH - w)/2, (SCREEN_HEIGHT - h)/2 - 10);
display.println(timeStr);
// barre de progression
display.drawRect(0, SCREEN_HEIGHT-10, SCREEN_WIDTH, 8, SSD1306_WHITE);
int barWidth = (SCREEN_WIDTH * (phaseDuration - remaining)) / phaseDuration;
display.fillRect(0, SCREEN_HEIGHT-10, barWidth, 8, SSD1306_WHITE);
if (remaining == 0) {
pomodoroRunning = false;
display.setTextSize(1);
display.setCursor(0, SCREEN_HEIGHT/2 + 20);
display.println("FIN! OK: Suivant");
}
} else if (pomodoroPaused) {
display.setTextSize(2);
display.setCursor((SCREEN_WIDTH/2)-10, (SCREEN_HEIGHT/2)-8);
display.println("||"); // icône pause
} else {
display.setTextSize(1);
display.setCursor(0, SCREEN_HEIGHT/2);
display.println("Appuyez OK pour demarrer");
}
display.display();
// ----- Gestion des boutons -----
if (isButtonPressed(buttonOkPin)) {
if (pomodoroRunning) {
if (!pomodoroPaused) {
elapsedBeforePause += millis() - pomodoroStart;
pomodoroPaused = true;
} else {
pomodoroStart = millis();
pomodoroPaused = false;
}
} else {
pomodoroPhase = (pomodoroPhase + 1) % 3;
startPomodoro();
}
}
if (isButtonPressed(buttonBackPin)) {
pomodoroRunning = false;
pomodoroPaused = false;
pomodoroPhase = 0;
pomodoroStart = 0;
elapsedBeforePause = 0;
currentState = MENU;
drawMenu();
}
}
/* --------------------------- WIFI SCANNER ---------------------- */
void scanWifi() {
display.clearDisplay();
display.setCursor(0, 0);
display.println("Scan WiFi...");
display.display();
nNetworks = WiFi.scanNetworks();
if (nNetworks > 6) nNetworks = 6;
for (int i = 0; i < nNetworks; i++) {
wifiList[i] = WiFi.SSID(i) + " (" + String(WiFi.RSSI(i)) + "dBm)";
}
}
void runWifiScanner() {
display.clearDisplay();
display.setCursor(0, 0);
if (nNetworks == 0) {
display.println("Aucun reseau");
} else {
for (int i = 0; i < nNetworks; i++) {
display.println(wifiList[i]);
}
}
display.display();
if (isButtonPressed(buttonOkPin)) scanWifi();
if (isButtonPressed(buttonBackPin)) {
nNetworks = 0;
currentState = MENU;
drawMenu();
}
}
/* --------------------------- ANIMATION ------------------------ */
void runAnimationApp() {
if (millis() - lastAnimUpdate > content_delays[currentFrame]) {
lastAnimUpdate = millis();
display.clearDisplay();
display.drawBitmap(0, 0, content[currentFrame],
CONTENT_WIDTH, CONTENT_HEIGHT, SSD1306_WHITE);
display.display();
currentFrame++;
if (currentFrame >= CONTENT_FRAME_COUNT) currentFrame = 0;
}
if (isButtonPressed(buttonBackPin)) {
currentState = MENU;
drawMenu();
}
}
/* --------------------------- CAPTIVE PORTAL ------------------- */
void startCaptivePortal() {
if (captiveActive) return;
WiFi.mode(WIFI_AP);
WiFi.softAP(apSSID, apPassword);
IPAddress apIP = WiFi.softAPIP();
dnsServer.start(DNS_PORT, "*", apIP);
webServer.on("/", HTTP_GET, []() {
String html = "<h2>ESP32 Config</h2><form action='/save' method='POST'>";
html += "SSID:<input type='text' name='ssid'><br>";
html += "Pass:<input type='password' name='pass'><br>";
html += "<input type='submit' value='Save'></form>";
webServer.send(200, "text/html", html);
});
webServer.on("/save", HTTP_POST, []() {
prefs.putString(KEY_SSID, webServer.arg("ssid"));
prefs.putString(KEY_PASS, webServer.arg("pass"));
webServer.send(200, "text/html", "Saved! Reboot to connect.");
});
webServer.begin();
captiveActive = true;
display.clearDisplay();
display.setCursor(0,0);
display.println("AP: ESP-PROD");
display.println("Pass:");
display.println(apPassword);
display.display();
}
void stopCaptivePortal() {
if (!captiveActive) return;
webServer.stop();
dnsServer.stop();
WiFi.softAPdisconnect(true);
captiveActive = false;
}
void startConfigApp() { startCaptivePortal(); }
void runConfigApp() {
dnsServer.processNextRequest();
webServer.handleClient();
if (isButtonPressed(buttonBackPin)) {
stopCaptivePortal();
currentState = MENU;
drawMenu();
}
}
/* --------------------------- LED CONTROL ---------------------- */
int ledHue = 160; // teinte 0‑255
int ledBrightness = 50; // luminosité 0‑255
void runLedApp() {
int potValue = analogRead(potentiometerPin);
ledHue = map(potValue, 0, 1023, 0, 255);
// Conversion HSV → RGB (simplifiée)
uint8_t r, g, b;
if (ledHue < 85) {
r = 255 - ledHue * 3;
g = ledHue * 3;
b = 0;
} else if (ledHue < 170) {
r = 0;
g = 255 - (ledHue - 85) * 3;
b = (ledHue - 85) * 3;
} else {
r = (ledHue - 170) * 3;
g = 0;
b = 255 - (ledHue - 170) * 3;
}
led.setBrightness(ledBrightness);
setLedColor(r, g, b);
display.clearDisplay();
display.setCursor(0,0);
display.println("== LED Control ==");
display.print("Hue: "); display.println(ledHue);
display.print("Lum: "); display.println(ledBrightness);
display.display();
if (isButtonPressed(buttonOkPin)) {
ledBrightness += 50;
if (ledBrightness > 255) ledBrightness = 50;
}
if (isButtonPressed(buttonBackPin)) {
currentState = MENU;
drawMenu();
}
}
/* --------------------------- SETUP ----------------------------- */
void setup() {
Serial.begin(115200);
Wire.begin(18, 19);
pinMode(buttonOkPin, INPUT_PULLUP);
pinMode(buttonBackPin, INPUT_PULLUP);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
while (1); // boucle infinie si l’écran ne démarre pas
}
display.clearDisplay();
display.display();
prefs.begin(PREF_NAMESPACE, false);
led.begin();
led.setBrightness(50);
setLedColor(0, 0, 255); // LED bleue au démarrage
// ----- connexion Wi‑Fi au démarrage -----
wifiConnected = connectToWifi(); // true → ok, false → no
// -----------------------------------------
playIntro();
drawMenu();
}
/* --------------------------- LOOP ------------------------------ */
void loop() {
switch (currentState) {
case MENU: handleMenuNavigation(); break;
case APP_POMODORO:runPomodoroApp(); break;
case APP_WIFI: runWifiScanner(); break;
case APP_ANIMATION:runAnimationApp(); break;
case APP_CONFIG: runConfigApp(); break;
case APP_LED: runLedApp(); break;
}
}