// Bird song wake-up for Cardputer
// string must be translated
// time zone idem
// SSID and Pass must be changed if you nuse one ssid #define MAX_ROUTERS 6 -> #define MAX_ROUTERS 1
// wav file must be put on SDcard of Cardputer
#include <ArduinoJson.h>
#include <LittleFS.h>
#include <M5Cardputer.h>
#include <M5GFX.h>
#include <WiFi.h>
#include <WiFiUdp.h>
#include <NTPClient.h>
#include <SD.h>
#include <SPI.h>
#include <HTTPClient.h>
#include <Wire.h>
#include <esp_log.h>
#include <TimeLib.h>
#include <Timezone.h>
TimeChangeRule* tcr;
// *******************************************************
// to be adjusted according to your criteria
// *******************************************************
#define MAX_ROUTERS 6
#define SCREEN_TIMEOUT 90000
#define MOVING_DISPLAY_DURATION 3000
#define TRIGGER_HOUR 6
#define WAV_FILENAME "/2380.wav"
// Règles pour l'heure d'été/d'hiver en Europe (UTC+1/UTC+2)
// *******************************************************
// to be adjusted according to your criteria
// *******************************************************
TimeChangeRule CEST = { "CEST", Last, Sun, Mar, 2, 120 }; // UTC+2
TimeChangeRule CET = { "CET", Last, Sun, Oct, 3, 60 }; // UTC+1
Timezone CE(CEST, CET);
int TRIGGER_MINUTE = 25;
// --- Variables globales ---
float batteryLevel;
unsigned long lastActivityTime = 0;
unsigned long lastSyncMinute = 9999;
static unsigned long lastNotificationTime = 0;
const unsigned long NOTIFICATION_DELAY = 5000;
int lastConnectedRouterIndex = -1;
int contrast = 33;
int volume = 22;
bool screenOn = true;
bool isWaitingForInput = false;
bool lowPowerMode = false;
bool screenInitDone = false;
static constexpr const char* files[] = {
"/2380.wav",
};
static constexpr const size_t buf_num = 3;
static constexpr const size_t buf_size = 1024;
static uint8_t wav_data[buf_num][buf_size];
struct attribute((packed)) wav_header_t {
char RIFF[4];
uint32_t chunk_size;
char WAVEfmt[8];
uint32_t fmt_chunk_size;
uint16_t audiofmt;
uint16_t channel;
uint32_t sample_rate;
uint32_t byte_per_sec;
uint16_t block_size;
uint16_t bit_per_sample;
};
struct attribute((packed)) sub_chunk_t {
char identifier[4];
uint32_t chunk_size;
uint8_t data[1];
};
struct Router {
char ssid[32];
char password[32];
};
Router routers[MAX_ROUTERS];
int routerCount = 0;
// --- Variables affichage ---
String TxtEnTete = "Bird-Alarm ";
String TxtCorps = "";
String TxtPied = "";
String TxtPiedII = "";
String data = "> ";
M5Canvas canvas(&M5Cardputer.Display);
// --- Variables NTP ---
const char* ntpServer = "pool.ntp.org";
const long utcOffsetSeconds = 0; // UTC
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, ntpServer, utcOffsetSeconds);
// --- Variables alarme ---
SemaphoreHandle_t resourceMutex = NULL;
TaskHandle_t audioTaskHandle = NULL;
volatile bool audioRequested = false;
volatile bool audioPlaying = false;
volatile bool triggeredToday = false;
// *******************************************
static bool playSdWav(const char* filename) {
File file = SD.open(filename);
wav_header_t wav_header;
file.read((uint8_t*)&wav_header, sizeof(wav_header_t));
// Vérifier la validité du fichier
if (memcmp(wav_header.RIFF, "RIFF", 4) || memcmp(wav_header.WAVEfmt, "WAVEfmt ", 8) || wav_header.audiofmt != 1 || wav_header.bit_per_sample < 8 || wav_header.bit_per_sample > 16 || wav_header.channel == 0 || wav_header.channel > 2) {
file.close();
Serial.println("[playSdWav] Invalid WAV file!");
return false;
}
file.seek(offsetof(wav_header_t, audiofmt) + wav_header.fmt_chunk_size);
sub_chunk_t sub_chunk;
file.read((uint8_t*)&sub_chunk, 8);
while (memcmp(sub_chunk.identifier, "data", 4)) {
file.seek(sub_chunk.chunk_size, SeekCur);
file.read((uint8_t*)&sub_chunk, 8);
}
bool flg_16bit = (wav_header.bit_per_sample >> 4);
int32_t data_len = sub_chunk.chunk_size;
size_t idx = 0;
M5Cardputer.Speaker.begin();
M5Cardputer.Speaker.setVolume(volume);
while (data_len > 0) {
size_t len = data_len < buf_size ? data_len : buf_size;
len = file.read(wav_data[idx], len);
data_len -= len;
if (flg_16bit) {
M5Cardputer.Speaker.playRaw((const int16_t*)wav_data[idx], len >> 1, wav_header.sample_rate, wav_header.channel > 1, 1, false);
} else {
M5Cardputer.Speaker.playRaw((const uint8_t*)wav_data[idx], len, wav_header.sample_rate, wav_header.channel > 1, 1, false);
}
idx = idx < (buf_num - 1) ? idx + 1 : 0;
}
M5Cardputer.Speaker.end();
file.close();
return true;
}
// *********************************************
void EnTete() {
ClearEnTete();
M5.Display.setCursor(5, 2);
M5.Display.setFont(&fonts::FreeMono9pt7b);
M5.Display.print(TxtEnTete);
batteryLevel = M5.Power.getBatteryLevel();
drawBattery(200, 5, batteryLevel);
M5.Display.drawLine(0, 19, M5.Display.width(), 19, YELLOW);
M5.Display.drawLine(0, 20, M5.Display.width(), 20, DARKGREY);
M5.Display.drawLine(0, 21, M5.Display.width(), 21, TFT_ORANGE);
}
// *********************************************
void ClearEnTete() {
M5.Display.setTextColor(WHITE, BLACK);
}
// *********************************************
void Corps() {
ClearCorps();
M5.Display.setTextSize(1);
M5.Display.setFont(&fonts::FreeMonoOblique9pt7b);
int largeurTexte = TxtCorps.length() * 10;
int x = (M5.Display.width() - largeurTexte) / 2;
M5.Display.setCursor(x, 25);
M5.Display.print(TxtCorps);
}
// *********************************************
void ClearCorps() {
M5.Display.fillRect(0, 22, 240, 91, BLACK);
}
// *********************************************
void Pied() {
ClearPied();
M5.Display.setTextSize(1);
M5.Display.setFont(&fonts::FreeMonoOblique9pt7b);
M5.Display.setTextColor(LIGHTGREY, BLACK);
M5.Display.setCursor(20, 107);
M5.Display.print(TxtPied);
}
// *********************************************
void ClearPied() {
M5.Display.fillRect(0, 107, 240, 14, BLACK);
}
// *********************************************
void PiedII() {
ClearPiedII();
M5.Display.setTextSize(1);
M5.Display.setFont(&fonts::FreeMonoOblique9pt7b);
M5.Display.setTextColor(LIGHTGREY, BLACK);
M5.Display.drawLine(0, 116, M5.Display.width(), 116, YELLOW);
char buffer[20];
snprintf(buffer, sizeof(buffer), " Wake-up @ %02dh%02dm | In: %s", TRIGGER_HOUR, TRIGGER_MINUTE, getTimeUntilAlarm().c_str());
TxtPiedII = buffer;
M5.Display.setCursor(3, 120);
M5.Display.print(TxtPiedII);
}
// *********************************************
void ClearPiedII() {
M5.Display.fillRect(0, 120, 240, 14, BLACK);
}
// *********************************************
void drawBattery(int x, int y, float percentage) {
int width = 30;
int height = 11;
int terminalWidth = 2;
uint16_t color = (percentage > 70) ? BLUE : (percentage > 30) ? YELLOW
: RED;
M5.Display.drawRect(x, y, width, height, WHITE);
M5.Display.fillRect(x + width, y + (height / 3), terminalWidth, height / 3, WHITE);
int chargeWidth = (int)((percentage / 100.0) * (width - 2));
M5.Display.fillRect(x + 1, y + 1, chargeWidth, height - 2, color);
}
// *********************************************
void drawProgressBar(int percentage) {
int barWidth = 180;
int barHeight = 10;
int x = 20;
int y = 50;
M5.Display.fillRect(x, y, barWidth, barHeight, BLACK);
M5.Display.drawRect(x, y, barWidth, barHeight, WHITE);
int fillWidth = (percentage * barWidth) / 100;
M5.Display.fillRect(x + 1, y + 1, fillWidth, barHeight - 2, GREEN);
M5.Display.setCursor(x + barWidth + 5, y);
M5.Display.printf("%d%%", percentage);
}
// *********************************************
void connectToStrongestWiFi() {
TxtCorps = " ";
Corps();
M5.Display.setTextColor(LIGHTGREY, BLACK);
M5.Display.setCursor(10, 26);
M5.Display.println("Scanning Wi-Fi...");
drawProgressBar(0);
int numNetworks = WiFi.scanNetworks();
if (numNetworks <= 0) {
ClearCorps();
M5.Display.setTextColor(YELLOW, BLACK);
M5.Display.setCursor(10, 40);
M5.Display.println("No Wi-Fi found");
return;
}
for (int i = 0; i <= 100; i += 5) {
drawProgressBar(i);
delay(28);
}
struct Network {
String ssid;
int rssi;
};
Network networks[numNetworks];
for (int i = 0; i < numNetworks; i++) {
networks[i].ssid = WiFi.SSID(i);
networks[i].rssi = WiFi.RSSI(i);
}
for (int i = 0; i < numNetworks - 1; i++) {
for (int j = i + 1; j < numNetworks; j++) {
if (networks[i].rssi < networks[j].rssi) {
Network temp = networks[i];
networks[i] = networks[j];
networks[j] = temp;
}
}
}
TxtCorps = "SSID found (" + String(numNetworks) + "):\n";
for (int i = 0; i < min(4, numNetworks); i++) {
TxtCorps += String(i + 1) + ". " + networks[i].ssid + "\n";
}
delay(9);
Corps();
for (int n = 0; n < numNetworks; n++) {
String candidateSSID = networks[n].ssid;
int foundIndex = -1;
for (int i = 0; i < routerCount; i++) {
if (String(routers[i].ssid) == candidateSSID) {
foundIndex = i;
break;
}
}
if (foundIndex == -1) {
continue;
}
ClearCorps();
M5.Display.setCursor(10, 26);
M5.Display.setTextColor(LIGHTGREY, BLACK);
M5.Display.println("Connecting to:");
M5.Display.setCursor(10, 46);
M5.Display.setTextColor(SKYBLUE, BLACK);
M5.Display.println(routers[foundIndex].ssid);
// Démarrer la connexion
WiFi.begin(routers[foundIndex].ssid, routers[foundIndex].password);
unsigned long start = millis();
bool connected = false;
while (millis() - start < 10000) {
delay(194);
M5.Display.print(".");
if (WiFi.status() == WL_CONNECTED) {
connected = true;
break;
}
}
ClearCorps();
if (connected) {
M5.Display.setCursor(10, 66);
M5.Display.setTextColor(GREEN, BLACK);
M5.Display.println("Connected @ ");
M5.Display.setCursor(10, 86);
M5.Display.print("IP: ");
M5.Display.println(WiFi.localIP());
lastConnectedRouterIndex = foundIndex;
return;
} else {
M5.Display.setCursor(10, 66);
M5.Display.setTextColor(RED, BLACK);
M5.Display.println("Connection failed");
delay(1940);
ClearCorps();
}
}
// Aucun réseau connu trouvé ou aucune connexion réussie
M5.Display.setCursor(10, 26);
M5.Display.setTextColor(YELLOW, BLACK);
M5.Display.println("No SSID found");
}
// *********************************************
void audioTask(void* param) {
(void)param;
while (true) {
if (!audioRequested) {
vTaskDelay(pdMS_TO_TICKS(200));
continue;
}
xSemaphoreTake(resourceMutex, portMAX_DELAY);
audioPlaying = true;
Serial.println("[AudioTask] Playing WAV file...");
if (!playSdWav(WAV_FILENAME)) {
M5.Display.fillRect(0, 22, 240, 91, BLACK);
M5.Display.setCursor(10, 26);
M5.Display.setTextColor(RED, BLACK);
M5.Display.println("Playback failed!");
} else {
M5.Display.fillRect(0, 22, 240, 91, BLACK);
M5.Display.setCursor(10, 26);
M5.Display.setTextColor(GREEN, BLACK);
M5.Display.println("Alarm ended !");
}
audioRequested = false;
audioPlaying = false;
xSemaphoreGive(resourceMutex);
}
}
// *********************************************
String Clavier(String TxtClavier) {
M5Cardputer.update();
M5.Display.fillRect(0, 120, 240, 14, BLACK);
data = TxtClavier + "> ";
M5.Display.drawString(data, 3, 120);
unsigned long startTime = millis();
const unsigned long TIMEOUT = 30000;
while (millis() - startTime < TIMEOUT) {
M5Cardputer.update();
if (M5Cardputer.Keyboard.isChange() && M5Cardputer.Keyboard.isPressed()) {
Keyboard_Class::KeysState status = M5Cardputer.Keyboard.keysState();
for (auto i : status.word) {
data += i;
}
if (status.del && data.length() > (TxtClavier.length() + 2)) {
data.remove(data.length() - 1);
}
if (status.enter) {
data.remove(0, TxtClavier.length() + 2);
String result = data;
data = "";
return result;
}
M5.Display.fillRect(0, 120, 240, 14, BLACK);
M5.Display.drawString(data, 3, 120);
}
delay(10);
}
return "";
}
// *********************************************
void saveSettingsToLittleFS() {
DynamicJsonDocument doc(256);
doc["contrast"] = contrast;
doc["volume"] = volume;
doc["minutes"] = TRIGGER_MINUTE;
File file = LittleFS.open("/settings.json", "w");
if (!file) {
Serial.println("Échec sauvegarde sur LittleFS.");
return;
}
serializeJson(doc, file);
file.close();
Serial.println("[LittleFS] Paramètres sauvegardés :");
Serial.print(" - Contraste: ");
Serial.println(contrast);
Serial.print(" - Volume: ");
Serial.println(volume);
Serial.print(" - Minutes de réveil: ");
Serial.println(TRIGGER_MINUTE);
}
// *********************************************
void handleButtonA() {
if (!isWaitingForInput && M5Cardputer.BtnA.wasPressed()) {
isWaitingForInput = true;
ClearCorps();
M5.Display.setTextColor(WHITE, BLACK);
M5.Display.setCursor(10, 27);
M5.Display.println("1. Contrast 0-255");
M5.Display.setCursor(10, 44);
M5.Display.println("2. Volume. 0-255");
M5.Display.setCursor(10, 61);
M5.Display.println("3. Test WAV");
M5.Display.setCursor(10, 78);
M5.Display.println("4. Rv: 6h 0-59m.");
M5.Display.setCursor(10, 95);
M5.Display.println("CR = Cancel");
String choice = Clavier("Selection ");
choice.trim();
if (choice == "1") {
ClearCorps();
M5.Display.setCursor(10, 30);
M5.Display.println("Contrast " + String(contrast));
M5.Display.setCursor(10, 50);
M5.Display.println("Input 0 -> 255");
M5.Display.setCursor(10, 90);
M5.Display.println("CR => Cancel");
String contrastStr = Clavier("Contrast: ");
if (contrastStr.length() > 0) {
bool isNumeric = true;
for (int i = 0; i < contrastStr.length(); i++) {
if (!isdigit(contrastStr.charAt(i))) {
isNumeric = false;
break;
}
}
if (isNumeric) {
int newContrast = contrastStr.toInt();
if (newContrast >= 0 && newContrast <= 255) {
contrast = newContrast;
M5.Display.setBrightness(contrast);
saveSettingsToLittleFS();
M5.Display.setCursor(10, 90);
M5.Display.println("Updated ! ");
delay(1000);
} else {
M5.Display.setCursor(10, 90);
M5.Display.println("Out of range ! ");
delay(1000);
}
} else {
M5.Display.setCursor(10, 90);
M5.Display.println("Wrong input ! ");
delay(1000);
}
} else {
M5.Display.setCursor(10, 90);
M5.Display.println("Canceled ! ");
delay(1000);
}
} else if (choice == "2") {
ClearCorps();
M5.Display.setCursor(10, 30);
M5.Display.println("Volume actuel: " + String(M5Cardputer.Speaker.getVolume()));
M5.Display.setCursor(10, 50);
M5.Display.println("Input 0 -> 255");
M5.Display.setCursor(10, 90);
M5.Display.println("CR => Cancel");
String volumeStr = Clavier("Volume: ");
if (volumeStr.length() > 0) {
bool isNumeric = true;
for (int i = 0; i < volumeStr.length(); i++) {
if (!isdigit(volumeStr.charAt(i))) {
isNumeric = false;
break;
}
}
if (isNumeric) {
int inputVolume = volumeStr.toInt();
if (inputVolume >= 0 && inputVolume <= 255) {
volume = inputVolume;
M5Cardputer.Speaker.setVolume(volume);
saveSettingsToLittleFS();
M5.Display.setCursor(10, 90);
M5.Display.println("Updated ! ");
delay(1000);
} else {
M5.Display.setCursor(10, 90);
M5.Display.println("Out of range ! ");
delay(1000);
}
} else {
M5.Display.setCursor(10, 90);
M5.Display.println("Wrong input ! ");
delay(1000);
}
} else {
M5.Display.setCursor(10, 90);
M5.Display.println("Canceled ! ");
delay(1000);
}
} else if (choice == "3") {
ClearCorps();
M5.Display.setCursor(10, 30);
M5.Display.println("Test WAV file...");
M5.Display.setCursor(10, 50);
M5.Display.println("Playing: " + String(WAV_FILENAME));
if (SD.exists(WAV_FILENAME)) {
M5Cardputer.Speaker.setVolume(volume);
audioRequested = true;
} else {
M5.Display.setCursor(10, 70);
M5.Display.setTextColor(RED, BLACK);
M5.Display.println("WAV not found ! ");
delay(1500);
}
} else if (choice == "4") {
int newMinutes;
if (newMinutes >= 0 && newMinutes <= 59) {
TRIGGER_MINUTE = newMinutes;
saveSettingsToLittleFS();
PiedII();
}
ClearCorps();
M5.Display.setCursor(10, 30);
char buffer[20];
snprintf(buffer, sizeof(buffer), "Wake-up @ %02dh %02dm", TRIGGER_HOUR, TRIGGER_MINUTE);
M5.Display.println(buffer);
M5.Display.setCursor(10, 50);
M5.Display.println("Input 00' -> 59'");
M5.Display.setCursor(10, 90);
M5.Display.println("CR => Cancel");
String minutesStr = Clavier("Minutes: ");
if (minutesStr.length() > 0) {
bool isNumeric = true;
for (int i = 0; i < minutesStr.length(); i++) {
if (!isdigit(minutesStr.charAt(i))) {
isNumeric = false;
break;
}
}
if (isNumeric) {
int newMinutes = minutesStr.toInt();
if (newMinutes >= 0 && newMinutes <= 59) {
TRIGGER_MINUTE = newMinutes;
saveSettingsToLittleFS();
M5.Display.setCursor(10, 90);
M5.Display.println("Updated ! ");
delay(1000);
} else {
M5.Display.setCursor(10, 90);
M5.Display.println("Out of range !");
delay(1000);
}
} else {
M5.Display.setCursor(10, 90);
M5.Display.println("Wrong input ! ");
delay(1000);
}
} else {
M5.Display.setCursor(10, 90);
M5.Display.println("Canceled ! ");
delay(1000);
}
}
Corps();
Pied();
PiedII();
isWaitingForInput = false;
}
}
// ==============================================
void turnScreenOn() {
if (lowPowerMode || !screenOn) {
M5.Display.setBrightness(contrast);
M5.Display.clear();
EnTete();
Corps();
Pied();
PiedII();
lowPowerMode = false;
screenOn = true;
}
lastActivityTime = millis();
}
// ==============================================
void turnScreenToLowPower() {
if (!lowPowerMode) {
M5.Display.setBrightness(5);
lowPowerMode = true;
}
}
// ==============================================
void checkScreenTimeout() {
if (millis() - lastActivityTime > SCREEN_TIMEOUT) {
turnScreenToLowPower();
}
}
// ==============================================
String getTimeUntilAlarm() {
timeClient.update();
time_t utc = timeClient.getEpochTime();
time_t local = CE.toLocal(utc, &tcr);
int currentHour = hour(local);
int currentMinute = minute(local);
int currentSecond = second(local);
int alarmHour = TRIGGER_HOUR;
int alarmMinute = TRIGGER_MINUTE;
int hoursUntilAlarm = alarmHour - currentHour;
int minutesUntilAlarm = alarmMinute - currentMinute;
int secondsUntilAlarm = 0 - currentSecond;
if (hoursUntilAlarm < 0) {
hoursUntilAlarm += 24;
}
if (minutesUntilAlarm < 0) {
minutesUntilAlarm += 60;
hoursUntilAlarm--;
}
if (secondsUntilAlarm < 0) {
secondsUntilAlarm += 60;
minutesUntilAlarm--;
}
if (minutesUntilAlarm < 0) {
minutesUntilAlarm = 0;
}
char timeStr[20];
snprintf(timeStr, sizeof(timeStr), "%02dh%02dm", hoursUntilAlarm, minutesUntilAlarm);
return String(timeStr);
TxtPiedII = " Alarm in: " + getTimeUntilAlarm();
PiedII();
}
// ==============================================
void updateDisplay() {
if (!screenOn) return;
timeClient.update();
time_t utc = timeClient.getEpochTime();
time_t local = CE.toLocal(utc, &tcr);
int hh = hour(local);
int mm = minute(local);
int ss = second(local);
int currentDay = day(local);
int currentMonth = month(local);
int currentYear = year(local);
M5.Display.fillRect(0, 22, 240, 54, BLACK);
char timeBuf[16];
snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d:%02d", hh, mm, ss);
M5.Display.setFont(&fonts::FreeSansBold18pt7b);
M5.Display.setTextSize(1);
M5.Display.setTextColor(YELLOW, BLACK);
int tw = M5.Display.textWidth(timeBuf);
int tx = (M5.Display.width() - tw) / 2;
M5.Display.setCursor(tx, 25);
M5.Display.print(timeBuf);
char dateBuf[24];
snprintf(dateBuf, sizeof(dateBuf), "%02d/%02d/%04d", currentDay, currentMonth, currentYear);
M5.Display.setFont(&fonts::FreeMonoOblique9pt7b);
M5.Display.setTextSize(1);
M5.Display.setTextColor(LIGHTGREY, BLACK);
int dw = M5.Display.textWidth(dateBuf);
int dx = (M5.Display.width() - dw) / 2;
M5.Display.setCursor(dx, 58);
M5.Display.print(dateBuf);
M5.Display.setTextColor(LIGHTGREY, BLACK);
M5.Display.setCursor(2, 76);
M5.Display.print("Wi-Fi ");
if (WiFi.status() == WL_CONNECTED) {
M5.Display.setTextColor(SKYBLUE, BLACK);
M5.Display.setCursor(69, 76);
M5.Display.println(WiFi.SSID());
M5.Display.setTextColor(LIGHTGREY, BLACK);
M5.Display.setCursor(2, 96);
M5.Display.print("IP ");
M5.Display.setTextColor(SKYBLUE, BLACK);
M5.Display.setCursor(69, 96);
M5.Display.println(WiFi.localIP());
} else {
M5.Display.setTextColor(YELLOW, BLACK);
M5.Display.println("No Wifi");
}
}
// ==============================================
void loadSettingsFromLittleFS() {
if (!LittleFS.exists("/settings.json")) {
Serial.println("Fichier settings.json introuvable.");
return;
}
File file = LittleFS.open("/settings.json", "r");
if (!file) {
Serial.println("Échec ouverture fichier settings.json.");
return;
}
DynamicJsonDocument doc(256);
DeserializationError err = deserializeJson(doc, file);
file.close();
if (err) {
Serial.println("Erreur parsing JSON (settings).");
return;
}
if (doc.containsKey("contrast")) {
contrast = doc["contrast"];
Serial.print("[LittleFS] Contraste chargé: ");
Serial.println(contrast);
}
if (doc.containsKey("volume")) {
volume = doc["volume"];
Serial.print("[LittleFS] Volume chargé: ");
Serial.println(volume);
}
if (doc.containsKey("minutes")) {
TRIGGER_MINUTE = doc["minutes"];
Serial.print("[LittleFS] Minutes de réveil chargées: ");
Serial.println(TRIGGER_MINUTE);
}
M5.Display.setBrightness(contrast);
M5Cardputer.Speaker.setVolume(volume);
}
// ==============================================
void loadDefaultRouters() {
routerCount = 5;
routers[0] = { "SSID1", "Pass1" };
routers[1] = { "SSID2", "Pass2" };
routers[2] = { "SSID3", "Pass3" };
routers[3] = { "SSID4", "Pass4" };
routers[4] = { "SSID5", "Pass5" };
Serial.println("[LittleFS] Liste de routers par défaut chargée (5 entrées).");
}
// ==============================================
bool loadRoutersFromLittleFS() {
if (!LittleFS.exists("/routers.json")) {
Serial.println("Fichier routers.json introuvable.");
return false;
}
File file = LittleFS.open("/routers.json", "r");
if (!file) {
Serial.println("Échec ouverture fichier routers.json.");
return false;
}
DynamicJsonDocument doc(1024);
DeserializationError err = deserializeJson(doc, file);
file.close();
if (err) {
Serial.println("Erreur parsing JSON (routers).");
return false;
}
JsonArray arr = doc["routers"];
routerCount = 0;
Serial.println("[LittleFS] Liste des routers chargés :");
for (JsonObject o : arr) {
if (routerCount >= MAX_ROUTERS) break;
strncpy(routers[routerCount].ssid, o["ssid"], sizeof(routers[routerCount].ssid) - 1);
strncpy(routers[routerCount].password, o["pwd"], sizeof(routers[routerCount].password) - 1);
Serial.print(" - SSID: ");
Serial.print(routers[routerCount].ssid);
Serial.print(", Password: ");
Serial.println(routers[routerCount].password);
routerCount++;
}
return true;
}
// ==============================================
void setup() {
Serial.begin(115200);
Serial.println("Démarrage du M5Cardputer...");
auto cfg = M5.config();
M5Cardputer.begin(cfg, true);
M5.Display.setRotation(1);
M5Cardputer.Display.setTextSize(1);
M5.Display.setFont(&fonts::FreeMonoOblique9pt7b);
M5.Display.setBrightness(contrast);
M5.Display.clear();
canvas.setTextFont(&fonts::FreeMonoOblique9pt7b);
canvas.setTextSize(1);
canvas.pushSprite(3, 120);
if (!LittleFS.begin(true)) {
Serial.println("Erreur LittleFS");
} else {
Serial.println("LittleFS initialisé.");
delay(999);
loadSettingsFromLittleFS();
Serial.println("Chargement des paramètres depuis LittleFS terminé.");
if (!loadRoutersFromLittleFS()) {
loadDefaultRouters();
}
Serial.println("Chargement des routers terminé.");
}
EnTete();
Serial.println("OLED initialisé.");
M5Cardputer.Speaker.begin();
Serial.println("Speaker initialisé.");
if (!SD.begin()) {
Serial.println("SD init failed");
} else {
Serial.println("SD ready");
}
WiFi.mode(WIFI_STA);
connectToStrongestWiFi();
timeClient.begin();
timeClient.update();
setTime(timeClient.getEpochTime());
EnTete();
Corps();
Pied();
PiedII();
resourceMutex = xSemaphoreCreateMutex();
xTaskCreatePinnedToCore(audioTask, "audioTask", 4096, NULL, 2, &audioTaskHandle, 1);
lastActivityTime = millis();
}
// ==============================================
void loop() {
checkScreenTimeout();
handleButtonA();
timeClient.update();
time_t utc = timeClient.getEpochTime();
time_t local = CE.toLocal(utc, &tcr);
int hh = hour(local);
int mm = minute(local);
bool shouldSync = (mm == 0) && (hh == 9 || hh == 12 || hh == 15 || hh == 18);
if (shouldSync && lastSyncMinute != (hh * 60 + mm)) {
lastSyncMinute = hh * 60 + mm;
if (WiFi.status() == WL_CONNECTED) {
if (timeClient.forceUpdate()) {
unsigned long epoch = timeClient.getEpochTime();
time_t t = (time_t)epoch;
struct tm tm;
gmtime_r(&t, &tm);
M5.Rtc.setDateTime(&tm);
}
}
}
if (!triggeredToday && hh == TRIGGER_HOUR && mm == TRIGGER_MINUTE) {
if (!audioPlaying && SD.exists(WAV_FILENAME)) {
audioRequested = true;
triggeredToday = true;
}
}
if (hh == 0 && mm == 0) {
triggeredToday = false;
}
if (screenOn) {
updateDisplay();
}
M5Cardputer.update();
if (M5Cardputer.Keyboard.isChange() && M5Cardputer.Keyboard.isPressed()) {
lastActivityTime = millis();
turnScreenOn();
}
PiedII();
delay(940);
}