<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Cardputer Wake-up clock with wav sound from SD]]></title><description><![CDATA[<p dir="auto">// Bird song wake-up for Cardputer<br />
// string must be translated<br />
// time zone idem<br />
// SSID and Pass must be changed if you nuse one ssid #define MAX_ROUTERS 6 -&gt; #define MAX_ROUTERS 1<br />
// wav file must be put on SDcard of Cardputer</p>
<p dir="auto">#include &lt;ArduinoJson.h&gt;<br />
#include &lt;LittleFS.h&gt;<br />
#include &lt;M5Cardputer.h&gt;<br />
#include &lt;M5GFX.h&gt;<br />
#include &lt;WiFi.h&gt;<br />
#include &lt;WiFiUdp.h&gt;<br />
#include &lt;NTPClient.h&gt;<br />
#include &lt;SD.h&gt;<br />
#include &lt;SPI.h&gt;<br />
#include &lt;HTTPClient.h&gt;<br />
#include &lt;Wire.h&gt;<br />
#include &lt;esp_log.h&gt;</p>
<p dir="auto">#include &lt;TimeLib.h&gt;<br />
#include &lt;Timezone.h&gt;</p>
<p dir="auto">TimeChangeRule* tcr;</p>
<p dir="auto">// *******************************************************<br />
// to be adjusted according to your criteria<br />
// *******************************************************<br />
#define MAX_ROUTERS 6<br />
#define SCREEN_TIMEOUT 90000<br />
#define MOVING_DISPLAY_DURATION 3000<br />
#define TRIGGER_HOUR 6<br />
#define WAV_FILENAME "/2380.wav"</p>
<p dir="auto">// Règles pour l'heure d'été/d'hiver en Europe (UTC+1/UTC+2)<br />
// *******************************************************<br />
// to be adjusted according to your criteria<br />
// *******************************************************<br />
TimeChangeRule CEST = { "CEST", Last, Sun, Mar, 2, 120 };  // UTC+2<br />
TimeChangeRule CET = { "CET", Last, Sun, Oct, 3, 60 };     // UTC+1<br />
Timezone CE(CEST, CET);</p>
<p dir="auto">int TRIGGER_MINUTE = 25;</p>
<p dir="auto">// --- Variables globales ---<br />
float batteryLevel;<br />
unsigned long lastActivityTime = 0;<br />
unsigned long lastSyncMinute = 9999;<br />
static unsigned long lastNotificationTime = 0;<br />
const unsigned long NOTIFICATION_DELAY = 5000;<br />
int lastConnectedRouterIndex = -1;<br />
int contrast = 33;<br />
int volume = 22;<br />
bool screenOn = true;<br />
bool isWaitingForInput = false;<br />
bool lowPowerMode = false;<br />
bool screenInitDone = false;</p>
<p dir="auto">static constexpr const char* files[] = {<br />
"/2380.wav",<br />
};</p>
<p dir="auto">static constexpr const size_t buf_num = 3;<br />
static constexpr const size_t buf_size = 1024;<br />
static uint8_t wav_data[buf_num][buf_size];</p>
<p dir="auto">struct <strong>attribute</strong>((packed)) wav_header_t {<br />
char RIFF[4];<br />
uint32_t chunk_size;<br />
char WAVEfmt[8];<br />
uint32_t fmt_chunk_size;<br />
uint16_t audiofmt;<br />
uint16_t channel;<br />
uint32_t sample_rate;<br />
uint32_t byte_per_sec;<br />
uint16_t block_size;<br />
uint16_t bit_per_sample;<br />
};</p>
<p dir="auto">struct <strong>attribute</strong>((packed)) sub_chunk_t {<br />
char identifier[4];<br />
uint32_t chunk_size;<br />
uint8_t data[1];<br />
};</p>
<p dir="auto">struct Router {<br />
char ssid[32];<br />
char password[32];<br />
};</p>
<p dir="auto">Router routers[MAX_ROUTERS];<br />
int routerCount = 0;</p>
<p dir="auto">// --- Variables affichage ---<br />
String TxtEnTete = "Bird-Alarm  ";<br />
String TxtCorps = "";<br />
String TxtPied = "";<br />
String TxtPiedII = "";<br />
String data = "&gt; ";<br />
M5Canvas canvas(&amp;M5Cardputer.Display);</p>
<p dir="auto">// --- Variables NTP ---<br />
const char* ntpServer = "<a href="http://pool.ntp.org" target="_blank" rel="noopener noreferrer nofollow ugc">pool.ntp.org</a>";<br />
const long utcOffsetSeconds = 0;  // UTC</p>
<p dir="auto">WiFiUDP ntpUDP;<br />
NTPClient timeClient(ntpUDP, ntpServer, utcOffsetSeconds);</p>
<p dir="auto">// --- Variables alarme ---<br />
SemaphoreHandle_t resourceMutex = NULL;<br />
TaskHandle_t audioTaskHandle = NULL;<br />
volatile bool audioRequested = false;<br />
volatile bool audioPlaying = false;<br />
volatile bool triggeredToday = false;</p>
<p dir="auto">// *******************************************<br />
static bool playSdWav(const char* filename) {<br />
File file = SD.open(filename);</p>
<p dir="auto">wav_header_t wav_header;<br />
file.read((uint8_t*)&amp;wav_header, sizeof(wav_header_t));</p>
<p dir="auto">// Vérifier la validité du fichier<br />
if (memcmp(wav_header.RIFF, "RIFF", 4) || memcmp(wav_header.WAVEfmt, "WAVEfmt ", 8) || wav_header.audiofmt != 1 || wav_header.bit_per_sample &lt; 8 || wav_header.bit_per_sample &gt; 16 || wav_header.channel == 0 || wav_header.channel &gt; 2) {<br />
file.close();<br />
Serial.println("[playSdWav] Invalid WAV file!");<br />
return false;<br />
}</p>
<p dir="auto">file.seek(offsetof(wav_header_t, audiofmt) + wav_header.fmt_chunk_size);<br />
sub_chunk_t sub_chunk;<br />
file.read((uint8_t*)&amp;sub_chunk, 8);<br />
while (memcmp(sub_chunk.identifier, "data", 4)) {<br />
file.seek(sub_chunk.chunk_size, SeekCur);<br />
file.read((uint8_t*)&amp;sub_chunk, 8);<br />
}</p>
<p dir="auto">bool flg_16bit = (wav_header.bit_per_sample &gt;&gt; 4);<br />
int32_t data_len = sub_chunk.chunk_size;<br />
size_t idx = 0;</p>
<p dir="auto">M5Cardputer.Speaker.begin();<br />
M5Cardputer.Speaker.setVolume(volume);</p>
<p dir="auto">while (data_len &gt; 0) {<br />
size_t len = data_len &lt; buf_size ? data_len : buf_size;<br />
len = file.read(wav_data[idx], len);<br />
data_len -= len;</p>
<pre><code>if (flg_16bit) {
  M5Cardputer.Speaker.playRaw((const int16_t*)wav_data[idx], len &gt;&gt; 1, wav_header.sample_rate, wav_header.channel &gt; 1, 1, false);
} else {
  M5Cardputer.Speaker.playRaw((const uint8_t*)wav_data[idx], len, wav_header.sample_rate, wav_header.channel &gt; 1, 1, false);
}
idx = idx &lt; (buf_num - 1) ? idx + 1 : 0;
</code></pre>
<p dir="auto">}</p>
<p dir="auto">M5Cardputer.Speaker.end();<br />
file.close();<br />
return true;<br />
}</p>
<p dir="auto">// *********************************************<br />
void EnTete() {<br />
ClearEnTete();<br />
M5.Display.setCursor(5, 2);<br />
M5.Display.setFont(&amp;fonts::FreeMono9pt7b);<br />
M5.Display.print(TxtEnTete);<br />
batteryLevel = M5.Power.getBatteryLevel();<br />
drawBattery(200, 5, batteryLevel);<br />
M5.Display.drawLine(0, 19, M5.Display.width(), 19, YELLOW);<br />
M5.Display.drawLine(0, 20, M5.Display.width(), 20, DARKGREY);<br />
M5.Display.drawLine(0, 21, M5.Display.width(), 21, TFT_ORANGE);<br />
}<br />
// *********************************************<br />
void ClearEnTete() {<br />
M5.Display.setTextColor(WHITE, BLACK);<br />
}</p>
<p dir="auto">// *********************************************<br />
void Corps() {<br />
ClearCorps();<br />
M5.Display.setTextSize(1);<br />
M5.Display.setFont(&amp;fonts::FreeMonoOblique9pt7b);<br />
int largeurTexte = TxtCorps.length() * 10;<br />
int x = (M5.Display.width() - largeurTexte) / 2;<br />
M5.Display.setCursor(x, 25);<br />
M5.Display.print(TxtCorps);<br />
}<br />
// *********************************************<br />
void ClearCorps() {<br />
M5.Display.fillRect(0, 22, 240, 91, BLACK);<br />
}</p>
<p dir="auto">// *********************************************<br />
void Pied() {<br />
ClearPied();<br />
M5.Display.setTextSize(1);<br />
M5.Display.setFont(&amp;fonts::FreeMonoOblique9pt7b);<br />
M5.Display.setTextColor(LIGHTGREY, BLACK);<br />
M5.Display.setCursor(20, 107);<br />
M5.Display.print(TxtPied);<br />
}<br />
// *********************************************<br />
void ClearPied() {<br />
M5.Display.fillRect(0, 107, 240, 14, BLACK);<br />
}</p>
<p dir="auto">// *********************************************<br />
void PiedII() {<br />
ClearPiedII();<br />
M5.Display.setTextSize(1);<br />
M5.Display.setFont(&amp;fonts::FreeMonoOblique9pt7b);<br />
M5.Display.setTextColor(LIGHTGREY, BLACK);</p>
<p dir="auto">M5.Display.drawLine(0, 116, M5.Display.width(), 116, YELLOW);</p>
<p dir="auto">char buffer[20];<br />
snprintf(buffer, sizeof(buffer), "  Wake-up @ %02dh%02dm | In: %s", TRIGGER_HOUR, TRIGGER_MINUTE, getTimeUntilAlarm().c_str());<br />
TxtPiedII = buffer;</p>
<p dir="auto">M5.Display.setCursor(3, 120);<br />
M5.Display.print(TxtPiedII);<br />
}</p>
<p dir="auto">// *********************************************<br />
void ClearPiedII() {<br />
M5.Display.fillRect(0, 120, 240, 14, BLACK);<br />
}</p>
<p dir="auto">// *********************************************<br />
void drawBattery(int x, int y, float percentage) {<br />
int width = 30;<br />
int height = 11;<br />
int terminalWidth = 2;<br />
uint16_t color = (percentage &gt; 70) ? BLUE : (percentage &gt; 30) ? YELLOW<br />
: RED;<br />
M5.Display.drawRect(x, y, width, height, WHITE);<br />
M5.Display.fillRect(x + width, y + (height / 3), terminalWidth, height / 3, WHITE);<br />
int chargeWidth = (int)((percentage / 100.0) * (width - 2));<br />
M5.Display.fillRect(x + 1, y + 1, chargeWidth, height - 2, color);<br />
}</p>
<p dir="auto">// *********************************************<br />
void drawProgressBar(int percentage) {<br />
int barWidth = 180;<br />
int barHeight = 10;<br />
int x = 20;<br />
int y = 50;<br />
M5.Display.fillRect(x, y, barWidth, barHeight, BLACK);<br />
M5.Display.drawRect(x, y, barWidth, barHeight, WHITE);<br />
int fillWidth = (percentage * barWidth) / 100;<br />
M5.Display.fillRect(x + 1, y + 1, fillWidth, barHeight - 2, GREEN);<br />
M5.Display.setCursor(x + barWidth + 5, y);<br />
M5.Display.printf("%d%%", percentage);<br />
}</p>
<p dir="auto">// *********************************************<br />
void connectToStrongestWiFi() {<br />
TxtCorps = " ";<br />
Corps();<br />
M5.Display.setTextColor(LIGHTGREY, BLACK);<br />
M5.Display.setCursor(10, 26);<br />
M5.Display.println("Scanning Wi-Fi...");</p>
<p dir="auto">drawProgressBar(0);<br />
int numNetworks = WiFi.scanNetworks();<br />
if (numNetworks &lt;= 0) {<br />
ClearCorps();<br />
M5.Display.setTextColor(YELLOW, BLACK);<br />
M5.Display.setCursor(10, 40);<br />
M5.Display.println("No Wi-Fi found");<br />
return;<br />
}</p>
<p dir="auto">for (int i = 0; i &lt;= 100; i += 5) {<br />
drawProgressBar(i);<br />
delay(28);<br />
}</p>
<p dir="auto">struct Network {<br />
String ssid;<br />
int rssi;<br />
};</p>
<p dir="auto">Network networks[numNetworks];<br />
for (int i = 0; i &lt; numNetworks; i++) {<br />
networks[i].ssid = WiFi.SSID(i);<br />
networks[i].rssi = WiFi.RSSI(i);<br />
}</p>
<p dir="auto">for (int i = 0; i &lt; numNetworks - 1; i++) {<br />
for (int j = i + 1; j &lt; numNetworks; j++) {<br />
if (networks[i].rssi &lt; networks[j].rssi) {<br />
Network temp = networks[i];<br />
networks[i] = networks[j];<br />
networks[j] = temp;<br />
}<br />
}<br />
}</p>
<p dir="auto">TxtCorps = "SSID found (" + String(numNetworks) + "):\n";<br />
for (int i = 0; i &lt; min(4, numNetworks); i++) {<br />
TxtCorps += String(i + 1) + ". " + networks[i].ssid + "\n";<br />
}<br />
delay(9);<br />
Corps();</p>
<p dir="auto">for (int n = 0; n &lt; numNetworks; n++) {<br />
String candidateSSID = networks[n].ssid;</p>
<pre><code>int foundIndex = -1;
for (int i = 0; i &lt; 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 &lt; 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();
}
</code></pre>
<p dir="auto">}</p>
<p dir="auto">// Aucun réseau connu trouvé ou aucune connexion réussie<br />
M5.Display.setCursor(10, 26);<br />
M5.Display.setTextColor(YELLOW, BLACK);<br />
M5.Display.println("No SSID found");<br />
}</p>
<p dir="auto">// *********************************************<br />
void audioTask(void* param) {<br />
(void)param;<br />
while (true) {<br />
if (!audioRequested) {<br />
vTaskDelay(pdMS_TO_TICKS(200));<br />
continue;<br />
}</p>
<pre><code>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);
</code></pre>
<p dir="auto">}<br />
}</p>
<p dir="auto">// *********************************************<br />
String Clavier(String TxtClavier) {<br />
M5Cardputer.update();<br />
M5.Display.fillRect(0, 120, 240, 14, BLACK);<br />
data = TxtClavier + "&gt; ";<br />
M5.Display.drawString(data, 3, 120);</p>
<p dir="auto">unsigned long startTime = millis();<br />
const unsigned long TIMEOUT = 30000;</p>
<p dir="auto">while (millis() - startTime &lt; TIMEOUT) {<br />
M5Cardputer.update();<br />
if (M5Cardputer.Keyboard.isChange() &amp;&amp; M5Cardputer.Keyboard.isPressed()) {<br />
Keyboard_Class::KeysState status = M5Cardputer.Keyboard.keysState();</p>
<pre><code>  for (auto i : status.word) {
    data += i;
  }

  if (status.del &amp;&amp; data.length() &gt; (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);
</code></pre>
<p dir="auto">}<br />
return "";<br />
}</p>
<p dir="auto">// *********************************************<br />
void saveSettingsToLittleFS() {<br />
DynamicJsonDocument doc(256);<br />
doc["contrast"] = contrast;<br />
doc["volume"] = volume;<br />
doc["minutes"] = TRIGGER_MINUTE;</p>
<p dir="auto">File file = LittleFS.open("/settings.json", "w");<br />
if (!file) {<br />
Serial.println("Échec sauvegarde sur LittleFS.");<br />
return;<br />
}</p>
<p dir="auto">serializeJson(doc, file);<br />
file.close();<br />
Serial.println("[LittleFS] Paramètres sauvegardés :");<br />
Serial.print("  - Contraste: ");<br />
Serial.println(contrast);<br />
Serial.print("  - Volume: ");<br />
Serial.println(volume);<br />
Serial.print("  - Minutes de réveil: ");<br />
Serial.println(TRIGGER_MINUTE);<br />
}</p>
<p dir="auto">// *********************************************<br />
void handleButtonA() {<br />
if (!isWaitingForInput &amp;&amp; M5Cardputer.BtnA.wasPressed()) {<br />
isWaitingForInput = true;<br />
ClearCorps();<br />
M5.Display.setTextColor(WHITE, BLACK);</p>
<pre><code>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 -&gt; 255");
  M5.Display.setCursor(10, 90);
  M5.Display.println("CR =&gt; Cancel");

  String contrastStr = Clavier("Contrast: ");
  if (contrastStr.length() &gt; 0) {
    bool isNumeric = true;
    for (int i = 0; i &lt; contrastStr.length(); i++) {
      if (!isdigit(contrastStr.charAt(i))) {
        isNumeric = false;
        break;
      }
    }
    if (isNumeric) {
      int newContrast = contrastStr.toInt();
      if (newContrast &gt;= 0 &amp;&amp; newContrast &lt;= 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 -&gt; 255");
  M5.Display.setCursor(10, 90);
  M5.Display.println("CR =&gt; Cancel");

  String volumeStr = Clavier("Volume: ");
  if (volumeStr.length() &gt; 0) {
    bool isNumeric = true;
    for (int i = 0; i &lt; volumeStr.length(); i++) {
      if (!isdigit(volumeStr.charAt(i))) {
        isNumeric = false;
        break;
      }
    }
    if (isNumeric) {
      int inputVolume = volumeStr.toInt();
      if (inputVolume &gt;= 0 &amp;&amp; inputVolume &lt;= 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 &gt;= 0 &amp;&amp; newMinutes &lt;= 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' -&gt; 59'");
  M5.Display.setCursor(10, 90);
  M5.Display.println("CR =&gt; Cancel");

  String minutesStr = Clavier("Minutes: ");
  if (minutesStr.length() &gt; 0) {
    bool isNumeric = true;
    for (int i = 0; i &lt; minutesStr.length(); i++) {
      if (!isdigit(minutesStr.charAt(i))) {
        isNumeric = false;
        break;
      }
    }
    if (isNumeric) {
      int newMinutes = minutesStr.toInt();
      if (newMinutes &gt;= 0 &amp;&amp; newMinutes &lt;= 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;
</code></pre>
<p dir="auto">}<br />
}</p>
<p dir="auto">// ==============================================<br />
void turnScreenOn() {<br />
if (lowPowerMode || !screenOn) {<br />
M5.Display.setBrightness(contrast);<br />
M5.Display.clear();<br />
EnTete();<br />
Corps();<br />
Pied();<br />
PiedII();<br />
lowPowerMode = false;<br />
screenOn = true;<br />
}<br />
lastActivityTime = millis();<br />
}</p>
<p dir="auto">// ==============================================<br />
void turnScreenToLowPower() {<br />
if (!lowPowerMode) {<br />
M5.Display.setBrightness(5);<br />
lowPowerMode = true;<br />
}<br />
}</p>
<p dir="auto">// ==============================================<br />
void checkScreenTimeout() {<br />
if (millis() - lastActivityTime &gt; SCREEN_TIMEOUT) {<br />
turnScreenToLowPower();<br />
}<br />
}</p>
<p dir="auto">// ==============================================<br />
String getTimeUntilAlarm() {<br />
timeClient.update();</p>
<p dir="auto">time_t utc = timeClient.getEpochTime();<br />
time_t local = CE.toLocal(utc, &amp;tcr);</p>
<p dir="auto">int currentHour = hour(local);<br />
int currentMinute = minute(local);<br />
int currentSecond = second(local);</p>
<p dir="auto">int alarmHour = TRIGGER_HOUR;<br />
int alarmMinute = TRIGGER_MINUTE;</p>
<p dir="auto">int hoursUntilAlarm = alarmHour - currentHour;<br />
int minutesUntilAlarm = alarmMinute - currentMinute;<br />
int secondsUntilAlarm = 0 - currentSecond;</p>
<p dir="auto">if (hoursUntilAlarm &lt; 0) {<br />
hoursUntilAlarm += 24;<br />
}</p>
<p dir="auto">if (minutesUntilAlarm &lt; 0) {<br />
minutesUntilAlarm += 60;<br />
hoursUntilAlarm--;<br />
}</p>
<p dir="auto">if (secondsUntilAlarm &lt; 0) {<br />
secondsUntilAlarm += 60;<br />
minutesUntilAlarm--;<br />
}</p>
<p dir="auto">if (minutesUntilAlarm &lt; 0) {<br />
minutesUntilAlarm = 0;<br />
}</p>
<p dir="auto">char timeStr[20];<br />
snprintf(timeStr, sizeof(timeStr), "%02dh%02dm", hoursUntilAlarm, minutesUntilAlarm);<br />
return String(timeStr);</p>
<p dir="auto">TxtPiedII = "  Alarm in: " + getTimeUntilAlarm();<br />
PiedII();<br />
}</p>
<p dir="auto">// ==============================================<br />
void updateDisplay() {<br />
if (!screenOn) return;</p>
<p dir="auto">timeClient.update();<br />
time_t utc = timeClient.getEpochTime();<br />
time_t local = CE.toLocal(utc, &amp;tcr);</p>
<p dir="auto">int hh = hour(local);<br />
int mm = minute(local);<br />
int ss = second(local);<br />
int currentDay = day(local);<br />
int currentMonth = month(local);<br />
int currentYear = year(local);</p>
<p dir="auto">M5.Display.fillRect(0, 22, 240, 54, BLACK);<br />
char timeBuf[16];<br />
snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d:%02d", hh, mm, ss);<br />
M5.Display.setFont(&amp;fonts::FreeSansBold18pt7b);<br />
M5.Display.setTextSize(1);<br />
M5.Display.setTextColor(YELLOW, BLACK);<br />
int tw = M5.Display.textWidth(timeBuf);<br />
int tx = (M5.Display.width() - tw) / 2;<br />
M5.Display.setCursor(tx, 25);<br />
M5.Display.print(timeBuf);</p>
<p dir="auto">char dateBuf[24];<br />
snprintf(dateBuf, sizeof(dateBuf), "%02d/%02d/%04d", currentDay, currentMonth, currentYear);<br />
M5.Display.setFont(&amp;fonts::FreeMonoOblique9pt7b);<br />
M5.Display.setTextSize(1);<br />
M5.Display.setTextColor(LIGHTGREY, BLACK);<br />
int dw = M5.Display.textWidth(dateBuf);<br />
int dx = (M5.Display.width() - dw) / 2;<br />
M5.Display.setCursor(dx, 58);<br />
M5.Display.print(dateBuf);</p>
<p dir="auto">M5.Display.setTextColor(LIGHTGREY, BLACK);<br />
M5.Display.setCursor(2, 76);<br />
M5.Display.print("Wi-Fi                   ");<br />
if (WiFi.status() == WL_CONNECTED) {<br />
M5.Display.setTextColor(SKYBLUE, BLACK);<br />
M5.Display.setCursor(69, 76);<br />
M5.Display.println(WiFi.SSID());<br />
M5.Display.setTextColor(LIGHTGREY, BLACK);<br />
M5.Display.setCursor(2, 96);<br />
M5.Display.print("IP       ");<br />
M5.Display.setTextColor(SKYBLUE, BLACK);<br />
M5.Display.setCursor(69, 96);<br />
M5.Display.println(WiFi.localIP());<br />
} else {<br />
M5.Display.setTextColor(YELLOW, BLACK);<br />
M5.Display.println("No Wifi");<br />
}<br />
}</p>
<p dir="auto">// ==============================================<br />
void loadSettingsFromLittleFS() {<br />
if (!LittleFS.exists("/settings.json")) {<br />
Serial.println("Fichier settings.json introuvable.");<br />
return;<br />
}<br />
File file = LittleFS.open("/settings.json", "r");<br />
if (!file) {<br />
Serial.println("Échec ouverture fichier settings.json.");<br />
return;<br />
}<br />
DynamicJsonDocument doc(256);<br />
DeserializationError err = deserializeJson(doc, file);<br />
file.close();<br />
if (err) {<br />
Serial.println("Erreur parsing JSON (settings).");<br />
return;<br />
}<br />
if (doc.containsKey("contrast")) {<br />
contrast = doc["contrast"];<br />
Serial.print("[LittleFS] Contraste chargé: ");<br />
Serial.println(contrast);<br />
}<br />
if (doc.containsKey("volume")) {<br />
volume = doc["volume"];<br />
Serial.print("[LittleFS] Volume chargé: ");<br />
Serial.println(volume);<br />
}<br />
if (doc.containsKey("minutes")) {<br />
TRIGGER_MINUTE = doc["minutes"];<br />
Serial.print("[LittleFS] Minutes de réveil chargées: ");<br />
Serial.println(TRIGGER_MINUTE);<br />
}</p>
<p dir="auto">M5.Display.setBrightness(contrast);<br />
M5Cardputer.Speaker.setVolume(volume);<br />
}</p>
<p dir="auto">// ==============================================<br />
void loadDefaultRouters() {<br />
routerCount = 5;<br />
routers[0] = { "SSID1", "Pass1" };<br />
routers[1] = { "SSID2", "Pass2" };<br />
routers[2] = { "SSID3", "Pass3" };<br />
routers[3] = { "SSID4", "Pass4" };<br />
routers[4] = { "SSID5", "Pass5" };<br />
Serial.println("[LittleFS] Liste de routers par défaut chargée (5 entrées).");<br />
}</p>
<p dir="auto">// ==============================================<br />
bool loadRoutersFromLittleFS() {<br />
if (!LittleFS.exists("/routers.json")) {<br />
Serial.println("Fichier routers.json introuvable.");<br />
return false;<br />
}</p>
<p dir="auto">File file = LittleFS.open("/routers.json", "r");<br />
if (!file) {<br />
Serial.println("Échec ouverture fichier routers.json.");<br />
return false;<br />
}<br />
DynamicJsonDocument doc(1024);<br />
DeserializationError err = deserializeJson(doc, file);<br />
file.close();</p>
<p dir="auto">if (err) {<br />
Serial.println("Erreur parsing JSON (routers).");<br />
return false;<br />
}</p>
<p dir="auto">JsonArray arr = doc["routers"];<br />
routerCount = 0;<br />
Serial.println("[LittleFS] Liste des routers chargés :");<br />
for (JsonObject o : arr) {<br />
if (routerCount &gt;= MAX_ROUTERS) break;<br />
strncpy(routers[routerCount].ssid, o["ssid"], sizeof(routers[routerCount].ssid) - 1);<br />
strncpy(routers[routerCount].password, o["pwd"], sizeof(routers[routerCount].password) - 1);<br />
Serial.print("  - SSID: ");<br />
Serial.print(routers[routerCount].ssid);<br />
Serial.print(", Password: ");<br />
Serial.println(routers[routerCount].password);<br />
routerCount++;<br />
}<br />
return true;<br />
}</p>
<p dir="auto">// ==============================================<br />
void setup() {<br />
Serial.begin(115200);<br />
Serial.println("Démarrage du M5Cardputer...");</p>
<p dir="auto">auto cfg = M5.config();<br />
M5Cardputer.begin(cfg, true);</p>
<p dir="auto">M5.Display.setRotation(1);<br />
M5Cardputer.Display.setTextSize(1);<br />
M5.Display.setFont(&amp;fonts::FreeMonoOblique9pt7b);<br />
M5.Display.setBrightness(contrast);<br />
M5.Display.clear();</p>
<p dir="auto">canvas.setTextFont(&amp;fonts::FreeMonoOblique9pt7b);<br />
canvas.setTextSize(1);<br />
canvas.pushSprite(3, 120);</p>
<p dir="auto">if (!LittleFS.begin(true)) {<br />
Serial.println("Erreur LittleFS");<br />
} else {<br />
Serial.println("LittleFS initialisé.");<br />
delay(999);<br />
loadSettingsFromLittleFS();<br />
Serial.println("Chargement des paramètres depuis LittleFS terminé.");<br />
if (!loadRoutersFromLittleFS()) {<br />
loadDefaultRouters();<br />
}<br />
Serial.println("Chargement des routers terminé.");<br />
}</p>
<p dir="auto">EnTete();<br />
Serial.println("OLED initialisé.");</p>
<p dir="auto">M5Cardputer.Speaker.begin();<br />
Serial.println("Speaker initialisé.");</p>
<p dir="auto">if (!SD.begin()) {<br />
Serial.println("SD init failed");<br />
} else {<br />
Serial.println("SD ready");<br />
}</p>
<p dir="auto">WiFi.mode(WIFI_STA);<br />
connectToStrongestWiFi();</p>
<p dir="auto">timeClient.begin();<br />
timeClient.update();<br />
setTime(timeClient.getEpochTime());</p>
<p dir="auto">EnTete();<br />
Corps();<br />
Pied();<br />
PiedII();</p>
<p dir="auto">resourceMutex = xSemaphoreCreateMutex();<br />
xTaskCreatePinnedToCore(audioTask, "audioTask", 4096, NULL, 2, &amp;audioTaskHandle, 1);</p>
<p dir="auto">lastActivityTime = millis();<br />
}</p>
<p dir="auto">// ==============================================<br />
void loop() {<br />
checkScreenTimeout();<br />
handleButtonA();</p>
<p dir="auto">timeClient.update();<br />
time_t utc = timeClient.getEpochTime();<br />
time_t local = CE.toLocal(utc, &amp;tcr);<br />
int hh = hour(local);<br />
int mm = minute(local);</p>
<p dir="auto">bool shouldSync = (mm == 0) &amp;&amp; (hh == 9 || hh == 12 || hh == 15 || hh == 18);<br />
if (shouldSync &amp;&amp; lastSyncMinute != (hh * 60 + mm)) {<br />
lastSyncMinute = hh * 60 + mm;<br />
if (WiFi.status() == WL_CONNECTED) {<br />
if (timeClient.forceUpdate()) {<br />
unsigned long epoch = timeClient.getEpochTime();<br />
time_t t = (time_t)epoch;<br />
struct tm tm;<br />
gmtime_r(&amp;t, &amp;tm);<br />
M5.Rtc.setDateTime(&amp;tm);<br />
}<br />
}<br />
}<br />
if (!triggeredToday &amp;&amp; hh == TRIGGER_HOUR &amp;&amp; mm == TRIGGER_MINUTE) {<br />
if (!audioPlaying &amp;&amp; SD.exists(WAV_FILENAME)) {<br />
audioRequested = true;<br />
triggeredToday = true;<br />
}<br />
}</p>
<p dir="auto">if (hh == 0 &amp;&amp; mm == 0) {<br />
triggeredToday = false;<br />
}</p>
<p dir="auto">if (screenOn) {<br />
updateDisplay();<br />
}</p>
<p dir="auto">M5Cardputer.update();<br />
if (M5Cardputer.Keyboard.isChange() &amp;&amp; M5Cardputer.Keyboard.isPressed()) {<br />
lastActivityTime = millis();<br />
turnScreenOn();<br />
}<br />
PiedII();<br />
delay(940);<br />
}</p>
]]></description><link>https://community.m5stack.com/topic/8247/cardputer-wake-up-clock-with-wav-sound-from-sd</link><generator>RSS for Node</generator><lastBuildDate>Sun, 07 Jun 2026 07:32:31 GMT</lastBuildDate><atom:link href="https://community.m5stack.com/topic/8247.rss" rel="self" type="application/rss+xml"/><pubDate>Sat, 06 Jun 2026 15:01:11 GMT</pubDate><ttl>60</ttl></channel></rss>