
Code Arduino de ragnaclock
Remplace les deux XXXXXXXXXXXXXXXXXX par ton SSID Wi-Fi et ton mot de passe avant le téléversement.
Ce code est entièrement gratuit : tu peux l’utiliser, le copier, le modifier, l’intégrer à un autre projet et le partager où tu veux, sans demander d’autorisation.
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <Preferences.h>
#include <time.h>
#include <sys/time.h>
#include <esp_sntp.h>
#include <vector>
#include <Adafruit_GFX.h>
#include <ESP32-HUB75-MatrixPanel-I2S-DMA.h>
#include <ESP32-VirtualMatrixPanel-I2S-DMA.h>
// =====================================================
// WIFI
// =====================================================
const char* WIFI_SSID = "XXXXXXXXXXXXXXXXXX";
const char* WIFI_PASSWORD = "XXXXXXXXXXXXXXXXXX";
// Synchronisation de l'heure et reconnexion automatique
const unsigned long NTP_SYNC_INTERVAL_MS = 5UL * 60UL * 1000UL;
const unsigned long WIFI_RETRY_INTERVAL_MS = 10UL * 1000UL;
const unsigned long AP_START_DELAY_MS = 15UL * 1000UL;
bool rescueAccessPointActive = false;
bool wifiWasConnected = false;
unsigned long wifiDisconnectedSince = 0;
unsigned long lastWifiRetry = 0;
// =====================================================
// PANNEAUX - CONFIGURATION VALIDEE, NE PLUS TOUCHER
// =====================================================
#define PANEL_RES_X 32
#define PANEL_RES_Y 32
#define NUM_ROWS 1
#define NUM_COLS 3
#define PANEL_CHAIN 3
#define DEFAULT_BRIGHTNESS 2
#define VIRTUAL_MATRIX_CHAIN_TYPE CHAIN_TOP_LEFT_DOWN
// SEENGREAT RGB Matrix Adapter Board (E) Rev 2.2
#define R1_PIN 18
#define G1_PIN 8
#define B1_PIN 17
#define R2_PIN 16
#define G2_PIN 1
#define B2_PIN 15
#define A_PIN 7
#define B_PIN 48
#define C_PIN 6
#define D_PIN 47
#define E_PIN 2
#define LAT_PIN 21
#define OE_PIN 4
#define CLK_PIN 5
MatrixPanel_I2S_DMA *dma_display = nullptr;
VirtualMatrixPanel *display = nullptr;
WebServer server(80);
Preferences preferences;
// =====================================================
// REGLAGES PERSISTANTS
// =====================================================
uint8_t displayBrightness = DEFAULT_BRIGHTNESS;
uint8_t digitSize = 100;
uint8_t digitThickness = 3;
uint16_t scrollDelayMs = 45;
String digitFontStyle = "segments";
bool autoFontRotation = false;
const char* FONT_STYLE_LIST[8] = {
"segments", "segments_thin", "segments_thick", "lcd_retro",
"block", "block_round", "spaced", "compact"
};
String effectiveFontStyle = "segments";
String lastEffectiveFontStyle = "";
String hourColorHex = "#00ff96";
String minuteColorHex = "#00ff96";
String secondColorHex = "#00ff96";
String messageColorMode = "single";
String messageColorHex = "#00ff96";
String messageP1Hex = "#00ff96";
String messageP2Hex = "#a050ff";
String messageP3Hex = "#ff1493";
uint16_t hourColor;
uint16_t minuteColor;
uint16_t secondColor;
uint16_t messageColor;
uint16_t messagePanel1Color;
uint16_t messagePanel2Color;
uint16_t messagePanel3Color;
uint16_t blackColor;
void resetClockCache();
// =====================================================
// WIFI ET HEURE
// =====================================================
void setTemporaryMidnight() {
// Heure provisoire : 1er janvier 2024 à 00:00:00 en France (heure d'hiver).
// Elle avance normalement jusqu'à la première synchronisation Internet.
struct timeval tv;
tv.tv_sec = 1704063600;
tv.tv_usec = 0;
settimeofday(&tv, nullptr);
}
void configureNtp() {
configTzTime(
"CET-1CEST,M3.5.0/2,M10.5.0/3",
"pool.ntp.org",
"time.google.com"
);
// Le service NTP redemande l'heure toutes les 5 minutes.
sntp_set_sync_interval(NTP_SYNC_INTERVAL_MS);
}
void startRescueAccessPoint() {
if (rescueAccessPointActive) return;
WiFi.mode(WIFI_AP_STA);
WiFi.softAP("Barbapocalypse", "metroid8");
rescueAccessPointActive = true;
Serial.println("WiFi de secours : Barbapocalypse");
Serial.println("Adresse : http://192.168.4.1");
}
void stopRescueAccessPoint() {
if (!rescueAccessPointActive) return;
WiFi.softAPdisconnect(true);
WiFi.mode(WIFI_STA);
rescueAccessPointActive = false;
}
void maintainWifiAndTime() {
const bool connected = WiFi.status() == WL_CONNECTED;
if (connected) {
if (!wifiWasConnected) {
Serial.println("WiFi connecte ou retabli");
Serial.print("Adresse : http://");
Serial.println(WiFi.localIP());
// Force une synchronisation immédiate après chaque retour du WiFi.
configureNtp();
stopRescueAccessPoint();
resetClockCache();
}
wifiWasConnected = true;
wifiDisconnectedSince = 0;
return;
}
if (wifiWasConnected || wifiDisconnectedSince == 0) {
wifiDisconnectedSince = millis();
Serial.println("WiFi indisponible, tentative de reconnexion...");
}
wifiWasConnected = false;
if (millis() - lastWifiRetry >= WIFI_RETRY_INTERVAL_MS) {
lastWifiRetry = millis();
WiFi.reconnect();
}
if (!rescueAccessPointActive &&
millis() - wifiDisconnectedSince >= AP_START_DELAY_MS) {
startRescueAccessPoint();
}
}
// =====================================================
// MESSAGES
// =====================================================
String manualMessage = "";
bool manualMode = false;
#define MAX_SCHEDULES 10
String schedDate[MAX_SCHEDULES];
String schedMsg[MAX_SCHEDULES];
String currentPlaybackMessage = "";
String currentPlaybackSource = "";
int passCount = 0;
bool clockPause = false;
unsigned long clockPauseStart = 0;
int scrollX = 96;
unsigned long lastScrollRefresh = 0;
// =====================================================
// CACHE HORLOGE
// =====================================================
int oldH1 = -1;
int oldH2 = -1;
int oldM1 = -1;
int oldM2 = -1;
int oldS1 = -1;
int oldS2 = -1;
int oldSecond = -1;
int oldColonVisible = -1;
// =====================================================
// POLICE : 7 SEGMENTS
// =====================================================
const uint8_t digitSegments[10] = {
0x3F, 0x06, 0x5B, 0x4F, 0x66,
0x6D, 0x7D, 0x07, 0x7F, 0x6F
};
// =====================================================
// POLICE : PIXEL BLOC 5x7
// =====================================================
const uint8_t blockDigits[10][7] = {
{0x0E,0x11,0x13,0x15,0x19,0x11,0x0E}, // 0
{0x04,0x0C,0x04,0x04,0x04,0x04,0x0E}, // 1
{0x0E,0x11,0x01,0x02,0x04,0x08,0x1F}, // 2
{0x1F,0x02,0x04,0x02,0x01,0x11,0x0E}, // 3
{0x02,0x06,0x0A,0x12,0x1F,0x02,0x02}, // 4
{0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E}, // 5
{0x06,0x08,0x10,0x1E,0x11,0x11,0x0E}, // 6
{0x1F,0x01,0x02,0x04,0x08,0x08,0x08}, // 7
{0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E}, // 8
{0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C} // 9
};
// =====================================================
// EMOJIS PIXEL 8x8 (20 disponibles, utilises via codes dans le texte)
// =====================================================
const uint8_t emojiHeart[8] = {0x66,0xFF,0xFF,0xFF,0x7E,0x3C,0x18,0x00};
const uint8_t emojiBrokenHeart[8] = {0x66,0xFF,0xDB,0xBD,0x66,0x3C,0x18,0x00};
const uint8_t emojiStar[8] = {0x18,0x18,0xFF,0x7E,0x3C,0x66,0xC3,0x81};
const uint8_t emojiBolt[8] = {0x18,0x30,0x60,0xFE,0x18,0x30,0x60,0x00};
const uint8_t emojiFire[8] = {0x18,0x18,0x3C,0x3C,0x7E,0x7E,0x3C,0x18};
const uint8_t emojiCoffee[8] = {0x14,0x28,0x00,0x7C,0x46,0x46,0x7C,0x38};
const uint8_t emojiSmile[8] = {0x3C,0x42,0xA5,0x81,0xA5,0x99,0x42,0x3C};
const uint8_t emojiWink[8] = {0x3C,0x42,0x81,0x85,0xA5,0x99,0x42,0x3C};
const uint8_t emojiSad[8] = {0x3C,0x42,0xA5,0x81,0x99,0xA5,0x42,0x3C};
const uint8_t emojiAngry[8] = {0x42,0x24,0xA5,0x81,0xBD,0xC3,0x42,0x3C};
const uint8_t emojiSun[8] = {0x24,0x18,0xA5,0x7E,0x7E,0xA5,0x18,0x24};
const uint8_t emojiMoon[8] = {0x1C,0x36,0x66,0x60,0x60,0x66,0x36,0x1C};
const uint8_t emojiRain[8] = {0x00,0x3C,0x7E,0xFF,0x00,0x24,0x24,0x00};
const uint8_t emojiSnow[8] = {0x18,0xDB,0x7E,0x24,0x24,0x7E,0xDB,0x18};
const uint8_t emojiSkull[8] = {0x3C,0x7E,0xDB,0xFF,0x7E,0x3C,0x24,0x24};
const uint8_t emojiGhost[8] = {0x3C,0x7E,0xFF,0xDB,0xFF,0xFF,0xFF,0xA5};
const uint8_t emojiMusic[8] = {0x06,0x0E,0x0A,0x0A,0x0A,0xEA,0xEE,0x6C};
const uint8_t emojiMountain[8] = {0x00,0x08,0x1C,0x36,0x63,0xC1,0xFF,0x00};
const uint8_t emojiRocket[8] = {0x18,0x3C,0x3C,0x7E,0xFF,0x5A,0x66,0x18};
const uint8_t emojiTrophy[8] = {0x3C,0x7E,0x7E,0x3C,0x18,0x18,0x3C,0x00};
// =====================================================
// COULEURS
// =====================================================
uint16_t hexTo565(String hex) {
if (hex.length() != 7 || hex.charAt(0) != '#') {
return dma_display->color565(0, 255, 150);
}
long rgb = strtol(hex.substring(1).c_str(), nullptr, 16);
uint8_t r = (rgb >> 16) & 0xFF;
uint8_t g = (rgb >> 8) & 0xFF;
uint8_t b = rgb & 0xFF;
return dma_display->color565(r, g, b);
}
void refreshDisplayColors() {
hourColor = hexTo565(hourColorHex);
minuteColor = hexTo565(minuteColorHex);
secondColor = hexTo565(secondColorHex);
messageColor = hexTo565(messageColorHex);
messagePanel1Color = hexTo565(messageP1Hex);
messagePanel2Color = hexTo565(messageP2Hex);
messagePanel3Color = hexTo565(messageP3Hex);
blackColor = dma_display->color565(0, 0, 0);
}
uint16_t rainbowColor(uint8_t pos) {
pos = 255 - pos;
if (pos < 85) {
return dma_display->color565(255 - pos * 3, 0, pos * 3);
}
if (pos < 170) {
pos -= 85;
return dma_display->color565(0, pos * 3, 255 - pos * 3);
}
pos -= 170;
return dma_display->color565(pos * 3, 255 - pos * 3, 0);
}
uint16_t scrollingTextColor(int x) {
if (messageColorMode == "rainbow") {
return rainbowColor((uint8_t)(x * 4 + millis() / 12));
}
if (messageColorMode == "panels") {
if (x < 32) return messagePanel1Color;
if (x < 64) return messagePanel2Color;
return messagePanel3Color;
}
return messageColor;
}
// =====================================================
// CHIFFRES - RENDU "7 SEGMENTS" (et ses variantes)
// =====================================================
void drawDigitSegments(int cellX, int number, uint16_t color,
int thickOverride, bool gap, bool forceMax = false) {
if (number < 0 || number > 9) return;
int digitW = forceMax ? 12 : map(digitSize, 50, 100, 7, 12);
int digitH = forceMax ? 25 : map(digitSize, 50, 100, 14, 25);
int thick = (thickOverride == -1)
? constrain((int)digitThickness, 1, 3)
: constrain(thickOverride, 1, 3);
int x = cellX + (16 - digitW) / 2;
int y = (32 - digitH) / 2;
int horizontalW = digitW - (2 * thick);
int halfH = (digitH - (3 * thick)) / 2;
if (gap) {
horizontalW -= 1;
halfH -= 1;
}
if (horizontalW < 1) horizontalW = 1;
if (halfH < 1) halfH = 1;
uint8_t s = digitSegments[number];
display->fillRect(x + thick, y, horizontalW, thick, (s & 0x01) ? color : blackColor);
display->fillRect(x + digitW - thick, y + thick, thick, halfH, (s & 0x02) ? color : blackColor);
display->fillRect(x + digitW - thick, y + (2 * thick) + halfH, thick, halfH, (s & 0x04) ? color : blackColor);
display->fillRect(x + thick, y + (2 * halfH) + (2 * thick), horizontalW, thick, (s & 0x08) ? color : blackColor);
display->fillRect(x, y + (2 * thick) + halfH, thick, halfH, (s & 0x10) ? color : blackColor);
display->fillRect(x, y + thick, thick, halfH, (s & 0x20) ? color : blackColor);
display->fillRect(x + thick, y + halfH + thick, horizontalW, thick, (s & 0x40) ? color : blackColor);
}
// =====================================================
// CHIFFRES - RENDU "PIXEL BLOC" (et ses variantes)
// =====================================================
void drawDigitBlock(int cellX, int number, uint16_t color,
int scaleOverride, bool roundCorners) {
if (number < 0 || number > 9) return;
int scale = (scaleOverride == -1)
? ((digitSize > 75) ? 2 : 1)
: scaleOverride;
int w = 5 * scale;
int h = 7 * scale;
int x = cellX + (16 - w) / 2;
int y = (32 - h) / 2;
for (int row = 0; row < 7; row++) {
uint8_t bits = blockDigits[number][row];
for (int col = 0; col < 5; col++) {
bool on = bits & (1 << (4 - col));
bool isCorner = (row == 0 || row == 6) && (col == 0 || col == 4);
if (roundCorners && isCorner) on = false;
display->fillRect(x + col * scale, y + row * scale, scale, scale, on ? color : blackColor);
}
}
}
void drawDigit(int cellX, int number, uint16_t color) {
if (effectiveFontStyle == "segments_thin") {
drawDigitSegments(cellX, number, color, 1, false);
} else if (effectiveFontStyle == "segments_thick") {
drawDigitSegments(cellX, number, color, 3, false, true);
} else if (effectiveFontStyle == "lcd_retro") {
drawDigitSegments(cellX, number, color, -1, true);
} else if (effectiveFontStyle == "block") {
drawDigitBlock(cellX, number, color, -1, false);
} else if (effectiveFontStyle == "block_round") {
drawDigitBlock(cellX, number, color, -1, true);
} else if (effectiveFontStyle == "spaced") {
drawDigitBlock(cellX, number, color, 1, false);
} else if (effectiveFontStyle == "compact") {
drawDigitBlock(cellX, number, color, 2, false);
} else {
drawDigitSegments(cellX, number, color, -1, false);
}
}
void resetClockCache() {
oldH1 = oldH2 = oldM1 = oldM2 = oldS1 = oldS2 = -1;
oldSecond = -1;
oldColonVisible = -1;
}
void updateClock() {
struct tm timeinfo;
if (!getLocalTime(&timeinfo, 10)) return;
if (autoFontRotation) {
int idx = timeinfo.tm_yday % 8;
effectiveFontStyle = FONT_STYLE_LIST[idx];
} else {
effectiveFontStyle = digitFontStyle;
}
if (effectiveFontStyle != lastEffectiveFontStyle) {
lastEffectiveFontStyle = effectiveFontStyle;
oldH1 = oldH2 = oldM1 = oldM2 = oldS1 = oldS2 = -1;
}
if (timeinfo.tm_sec == oldSecond) return;
oldSecond = timeinfo.tm_sec;
int h1 = timeinfo.tm_hour / 10;
int h2 = timeinfo.tm_hour % 10;
int m1 = timeinfo.tm_min / 10;
int m2 = timeinfo.tm_min % 10;
int s1 = timeinfo.tm_sec / 10;
int s2 = timeinfo.tm_sec % 10;
if (h1 != oldH1) { drawDigit(0, h1, hourColor); oldH1 = h1; }
if (h2 != oldH2) { drawDigit(16, h2, hourColor); oldH2 = h2; }
if (m1 != oldM1) { drawDigit(32, m1, minuteColor); oldM1 = m1; }
if (m2 != oldM2) { drawDigit(48, m2, minuteColor); oldM2 = m2; }
if (s1 != oldS1) { drawDigit(64, s1, secondColor); oldS1 = s1; }
if (s2 != oldS2) { drawDigit(80, s2, secondColor); oldS2 = s2; }
int colonVisible = (timeinfo.tm_sec % 2 == 0) ? 1 : 0;
if (colonVisible != oldColonVisible) {
uint16_t c = colonVisible ? minuteColor : blackColor;
display->fillRect(31, 11, 2, 2, c);
display->fillRect(31, 19, 2, 2, c);
display->fillRect(63, 11, 2, 2, c);
display->fillRect(63, 19, 2, 2, c);
oldColonVisible = colonVisible;
}
}
void showClockNow() {
dma_display->clearScreen();
resetClockCache();
updateClock();
}
// =====================================================
// EMOJIS - RENDU BAS NIVEAU
// =====================================================
void drawPixelEmojiBitmap(const uint8_t* bitmap, uint16_t color, int x, int y) {
if (!bitmap) return;
for (int row = 0; row < 8; row++) {
for (int col = 0; col < 8; col++) {
if (bitmap[row] & (1 << (7 - col))) {
display->fillRect(x + col * 2, y + row * 2, 2, 2, color);
}
}
}
}
// =====================================================
// MESSAGE : DECOUPAGE EN JETONS (TEXTE + EMOJIS INTEGRES)
//
// Codes reconnus dans le texte, entre crochets :
// [<3] [</3] [star] [bolt] [fire] [coffee] [:)] [:D]
// [;)] [:(] [>:(] [sun] [moon] [rain] [snow]
// [skull] [ghost] [music] [mountain] [rocket] [trophy]
// =====================================================
struct MsgToken {
bool isEmoji;
char ch;
const uint8_t* bitmap;
uint16_t color;
};
std::vector<MsgToken> activeTokens;
int activeTextWidthPx = 0;
// La police classique d'Adafruit_GFX utilise les codes CP437 alors que
// les telephones Android envoient les messages en UTF-8. Cette conversion
// permet d'afficher correctement les accents francais sur les panneaux.
uint8_t unicodeToCp437(uint32_t codepoint) {
switch (codepoint) {
case 0x00C7: return 128; // Ç
case 0x00FC: return 129; // ü
case 0x00E9: return 130; // é
case 0x00E2: return 131; // â
case 0x00E4: return 132; // ä
case 0x00E0: return 133; // à
case 0x00E5: return 134; // å
case 0x00E7: return 135; // ç
case 0x00EA: return 136; // ê
case 0x00EB: return 137; // ë
case 0x00E8: return 138; // è
case 0x00EF: return 139; // ï
case 0x00EE: return 140; // î
case 0x00C4: return 142; // Ä
case 0x00C9: return 144; // É
case 0x00F4: return 147; // ô
case 0x00F6: return 148; // ö
case 0x00FB: return 150; // û
case 0x00F9: return 151; // ù
case 0x00FF: return 152; // ÿ
case 0x00D6: return 153; // Ö
case 0x00DC: return 154; // Ü
case 0x2018:
case 0x2019: return '\'';
case 0x201C:
case 0x201D: return '"';
default: return '?';
}
}
uint8_t readUtf8Character(const String &text, int &index) {
uint8_t first = (uint8_t)text.charAt(index++);
if (first < 0x80) return first;
uint32_t codepoint = 0;
int continuationCount = 0;
if ((first & 0xE0) == 0xC0) {
codepoint = first & 0x1F;
continuationCount = 1;
} else if ((first & 0xF0) == 0xE0) {
codepoint = first & 0x0F;
continuationCount = 2;
} else if ((first & 0xF8) == 0xF0) {
codepoint = first & 0x07;
continuationCount = 3;
} else {
return '?';
}
for (int n = 0; n < continuationCount; n++) {
if (index >= text.length()) return '?';
uint8_t next = (uint8_t)text.charAt(index);
if ((next & 0xC0) != 0x80) return '?';
index++;
codepoint = (codepoint << 6) | (next & 0x3F);
}
return unicodeToCp437(codepoint);
}
bool resolveEmojiCode(const String &code, const uint8_t* &bmp, uint16_t &col) {
bmp = nullptr;
if (code == "<3") { bmp = emojiHeart; col = dma_display->color565(255, 20, 80); }
else if (code == "</3") { bmp = emojiBrokenHeart; col = dma_display->color565(180, 20, 60); }
else if (code == "star") { bmp = emojiStar; col = dma_display->color565(255, 230, 0); }
else if (code == "bolt") { bmp = emojiBolt; col = dma_display->color565(255, 240, 60); }
else if (code == "fire") { bmp = emojiFire; col = dma_display->color565(255, 90, 0); }
else if (code == "coffee") { bmp = emojiCoffee; col = dma_display->color565(255, 170, 70); }
else if (code == ":)" || code == ":D") { bmp = emojiSmile; col = dma_display->color565(255, 220, 0); }
else if (code == ";)") { bmp = emojiWink; col = dma_display->color565(255, 220, 0); }
else if (code == ":(") { bmp = emojiSad; col = dma_display->color565(90, 170, 255); }
else if (code == ">:(") { bmp = emojiAngry; col = dma_display->color565(255, 60, 40); }
else if (code == "sun") { bmp = emojiSun; col = dma_display->color565(255, 200, 0); }
else if (code == "moon") { bmp = emojiMoon; col = dma_display->color565(220, 220, 255); }
else if (code == "rain") { bmp = emojiRain; col = dma_display->color565(90, 170, 255); }
else if (code == "snow") { bmp = emojiSnow; col = dma_display->color565(200, 230, 255); }
else if (code == "skull") { bmp = emojiSkull; col = dma_display->color565(220, 255, 240); }
else if (code == "ghost") { bmp = emojiGhost; col = dma_display->color565(220, 220, 255); }
else if (code == "music") { bmp = emojiMusic; col = dma_display->color565(180, 90, 255); }
else if (code == "mountain") { bmp = emojiMountain; col = dma_display->color565(140, 200, 160); }
else if (code == "rocket") { bmp = emojiRocket; col = dma_display->color565(255, 100, 60); }
else if (code == "trophy") { bmp = emojiTrophy; col = dma_display->color565(255, 210, 0); }
return bmp != nullptr;
}
std::vector<MsgToken> parseMessageTokens(const String &message) {
std::vector<MsgToken> tokens;
int i = 0;
int len = message.length();
while (i < len) {
if (message.charAt(i) == '[') {
int close = message.indexOf(']', i);
if (close != -1 && close - i <= 10) {
String code = message.substring(i + 1, close);
const uint8_t* bmp = nullptr;
uint16_t col = messageColor;
if (resolveEmojiCode(code, bmp, col)) {
MsgToken t;
t.isEmoji = true;
t.bitmap = bmp;
t.color = col;
t.ch = 0;
tokens.push_back(t);
i = close + 1;
continue;
}
}
}
MsgToken t;
t.isEmoji = false;
t.ch = (char)readUtf8Character(message, i);
t.bitmap = nullptr;
t.color = 0;
tokens.push_back(t);
}
return tokens;
}
void prepareMessage(const String &message) {
activeTokens = parseMessageTokens(message);
activeTextWidthPx = 0;
for (auto &t : activeTokens) {
activeTextWidthPx += t.isEmoji ? 18 : 12;
}
scrollX = 96;
}
// =====================================================
// MESSAGE DEFILANT
// =====================================================
bool drawScrollingMessage() {
if (activeTokens.empty()) return true;
if (millis() - lastScrollRefresh < scrollDelayMs) return false;
lastScrollRefresh = millis();
dma_display->clearScreen();
display->setTextWrap(false);
display->setTextSize(2);
display->cp437(true);
int cursorX = scrollX;
for (auto &t : activeTokens) {
if (t.isEmoji) {
drawPixelEmojiBitmap(t.bitmap, t.color, cursorX, 8);
cursorX += 18;
} else {
display->setTextColor(scrollingTextColor(cursorX + 6));
display->setCursor(cursorX, 9);
display->write((uint8_t)t.ch);
cursorX += 12;
}
}
scrollX--;
if (scrollX < -activeTextWidthPx) {
scrollX = 96;
return true;
}
return false;
}
// =====================================================
// DATE
// =====================================================
String getToday() {
struct tm timeinfo;
if (!getLocalTime(&timeinfo, 10)) return "";
char buffer[11];
strftime(buffer, sizeof(buffer), "%Y-%m-%d", &timeinfo);
return String(buffer);
}
// =====================================================
// PROGRAMMATION : SAUVEGARDE / CHARGEMENT
// =====================================================
void loadSchedules() {
for (int i = 0; i < MAX_SCHEDULES; i++) {
schedDate[i] = preferences.getString(("d" + String(i)).c_str(), "");
schedMsg[i] = preferences.getString(("m" + String(i)).c_str(), "");
}
}
// =====================================================
// PAGE WEB
// =====================================================
String buildScheduleListHtml() {
String html = "";
bool any = false;
for (int i = 0; i < MAX_SCHEDULES; i++) {
if (schedDate[i].length() && schedMsg[i].length()) {
any = true;
html += "<div class='schedRow'>";
html += "<span class='schedText'><b>" + schedDate[i] + "</b> - " + schedMsg[i] + "</span>";
html += "<form action='/deleteSchedule' method='POST'>";
html += "<input type='hidden' name='index' value='" + String(i) + "'>";
html += "<button class='danger' type='submit'>Suppr.</button>";
html += "</form>";
html += "</div>";
}
}
if (!any) {
html = "<p class='info'>Aucun message programme pour le moment.</p>";
}
return html;
}
String fontOptionsHtml() {
struct FO { const char* value; const char* label; };
FO options[8] = {
{"segments", "7 segments (classique)"},
{"segments_thin", "7 segments fin"},
{"segments_thick", "7 segments epais"},
{"lcd_retro", "LCD retro (avec espaces)"},
{"block", "Pixel bloc"},
{"block_round", "Pixel bloc arrondi"},
{"spaced", "Pixel espace"},
{"compact", "Pixel compact"}
};
String html = "";
for (int i = 0; i < 8; i++) {
html += "<option value='";
html += options[i].value;
html += "'";
if (digitFontStyle == options[i].value) html += " selected";
html += ">";
html += options[i].label;
html += "</option>";
}
return html;
}
// Rangee de boutons emoji cliquables, inseres au curseur dans le champ vise.
String emojiRowHtml(const String &targetInputId) {
struct EB { const char* glyph; const char* code; };
EB buttons[20] = {
{"\xE2\x9D\xA4\xEF\xB8\x8F", "<3"},
{"\xF0\x9F\x92\x94", "</3"},
{"\xE2\xAD\x90", "star"},
{"\xE2\x9A\xA1", "bolt"},
{"\xF0\x9F\x94\xA5", "fire"},
{"\xE2\x98\x95", "coffee"},
{"\xF0\x9F\x99\x82", ":)"},
{"\xF0\x9F\x98\x89", ";)"},
{"\xF0\x9F\x99\x81", ":("},
{"\xF0\x9F\x98\xA0", ">:("},
{"\xE2\x98\x80\xEF\xB8\x8F", "sun"},
{"\xF0\x9F\x8C\x99", "moon"},
{"\xF0\x9F\x8C\xA7\xEF\xB8\x8F", "rain"},
{"\xE2\x9D\x84\xEF\xB8\x8F", "snow"},
{"\xF0\x9F\x92\x80", "skull"},
{"\xF0\x9F\x91\xBB", "ghost"},
{"\xF0\x9F\x8E\xB5", "music"},
{"\xF0\x9F\x8F\x94\xEF\xB8\x8F", "mountain"},
{"\xF0\x9F\x9A\x80", "rocket"},
{"\xF0\x9F\x8F\x86", "trophy"}
};
String html = "<div class='emojiRow'>";
for (int i = 0; i < 20; i++) {
html += "<button type='button' class='emojiBtn' onclick=\"insertEmoji('";
html += targetInputId;
html += "','";
html += buttons[i].code;
html += "')\">";
html += buttons[i].glyph;
html += "</button>";
}
html += "</div>";
return html;
}
String webPage() {
String page = R"rawliteral(
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Barbapocalypse</title>
<style>
*{box-sizing:border-box}
body{margin:0;background:#07100d;color:#d8fff1;font-family:Arial,sans-serif;text-align:center;padding:18px}
h1{color:#00ff96;margin:10px 0 24px;text-shadow:0 0 14px #00ff9670}
h2{color:#00ff96;margin-top:4px}
.card{max-width:620px;margin:16px auto;background:#101a17;border:1px solid #1d3b32;padding:18px;border-radius:16px;box-shadow:0 5px 22px #0008}
.row{display:flex;gap:10px;justify-content:center;align-items:center;flex-wrap:wrap}
label{display:block;margin-top:12px;color:#a9d9c9}
input[type=text],input[type=date],select{width:92%;max-width:460px;padding:12px;margin:7px;border-radius:9px;border:1px solid #31594d;background:#08120f;color:white;font-size:16px}
input[type=color]{width:70px;height:42px;border:0;background:transparent}
input[type=range]{-webkit-appearance:none;appearance:none;width:60%;max-width:320px;height:10px;border-radius:5px;background:#1d3b32}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:30px;height:30px;border-radius:50%;background:#00cc80;border:3px solid #001b11;cursor:pointer}
input[type=range]::-moz-range-thumb{width:30px;height:30px;border-radius:50%;background:#00cc80;border:3px solid #001b11;cursor:pointer}
.stepBtn{width:46px;height:46px;font-size:24px;font-weight:900;border:0;border-radius:10px;background:#1d3b32;color:#00ff96;cursor:pointer;flex-shrink:0}
.stepBtn:hover{background:#254a3f}
button{padding:12px 18px;margin:9px;border:0;border-radius:10px;background:#00cc80;color:#001b11;font-weight:800;font-size:16px;cursor:pointer}
button:hover{background:#00ff96}
.danger{background:#db4c5c;color:white}
.info{color:#a9d9c9;font-size:14px}
.colors{display:flex;justify-content:center;gap:16px;flex-wrap:wrap}
.colorbox{min-width:120px}
.codes{color:#7fd9bd;font-size:13px;margin-top:4px}
.schedRow{display:flex;justify-content:space-between;align-items:center;gap:10px;background:#0c1613;border:1px solid #1d3b32;border-radius:10px;padding:8px 12px;margin:8px 0;text-align:left}
.schedText{font-size:14px;word-break:break-word}
.schedRow form{margin:0}
.checkRow{display:flex;align-items:center;justify-content:center;gap:10px;margin-top:14px}
.checkRow input{width:22px;height:22px}
.emojiRow{display:flex;flex-wrap:wrap;justify-content:center;gap:6px;margin:10px 0}
.emojiBtn{width:42px;height:42px;font-size:20px;border:0;border-radius:8px;background:#1d3b32;cursor:pointer;padding:0}
.emojiBtn:hover{background:#254a3f}
</style>
</head>
<body>
<h1>CLOCK DESTROY</h1>
<div class="card">
<h2>Horloge</h2>
<form action="/clock" method="POST">
<button type="submit">AFFICHER L'HORLOGE</button>
</form>
</div>
<div class="card">
<h2>Affichage</h2>
<form action="/displaySettings" method="POST">
<label>Luminosité : <span id="val_brightness">BRIGHTNESS_VALUE</span> / 255</label>
<div class="row">
<button type="button" class="stepBtn" onclick="step('brightness',-5)">−</button>
<input type="range" id="brightness" name="brightness" min="1" max="255" value="BRIGHTNESS_VALUE" oninput="syncVal('brightness')">
<button type="button" class="stepBtn" onclick="step('brightness',5)">+</button>
</div>
<label>Taille des chiffres : <span id="val_size">SIZE_VALUE</span> %</label>
<div class="row">
<button type="button" class="stepBtn" onclick="step('size',-5)">−</button>
<input type="range" id="size" name="size" min="50" max="100" value="SIZE_VALUE" oninput="syncVal('size')">
<button type="button" class="stepBtn" onclick="step('size',5)">+</button>
</div>
<label>Épaisseur (police 7 segments) : <span id="val_thickness">THICK_VALUE</span></label>
<div class="row">
<button type="button" class="stepBtn" onclick="step('thickness',-1)">−</button>
<input type="range" id="thickness" name="thickness" min="1" max="3" value="THICK_VALUE" oninput="syncVal('thickness')">
<button type="button" class="stepBtn" onclick="step('thickness',1)">+</button>
</div>
<label>Vitesse de défilement des messages : <span id="val_scrollSpeed">SCROLL_VALUE</span> ms</label>
<div class="row">
<span class="info">Rapide</span>
<button type="button" class="stepBtn" onclick="step('scrollSpeed',-5)">−</button>
<input type="range" id="scrollSpeed" name="scrollSpeed" min="15" max="150" value="SCROLL_VALUE" oninput="syncVal('scrollSpeed')">
<button type="button" class="stepBtn" onclick="step('scrollSpeed',5)">+</button>
<span class="info">Lent</span>
</div>
<label>Police d'affichage des chiffres</label>
<select name="fontStyle">
FONT_OPTIONS_HTML
</select>
<div class="checkRow">
<input type="checkbox" name="autoFont" id="autoFont" value="1" AUTO_FONT_CHECKED>
<label for="autoFont" style="margin:0">Rotation automatique de la police (change chaque jour, ignore le choix ci-dessus)</label>
</div>
<div class="colors">
<div class="colorbox"><label>Heures</label><input type="color" name="hourColor" value="HOUR_COLOR"></div>
<div class="colorbox"><label>Minutes</label><input type="color" name="minuteColor" value="MINUTE_COLOR"></div>
<div class="colorbox"><label>Secondes</label><input type="color" name="secondColor" value="SECOND_COLOR"></div>
</div>
<br><button type="submit">APPLIQUER</button>
</form>
</div>
<div class="card">
<h2>Message immédiat</h2>
<p class="info">2 passages du message, puis retour à l'horloge pendant 1 minute, en boucle.</p>
<form action="/message" method="POST">
<input type="text" id="msgText" name="message" maxlength="140" placeholder="Ton message..." required>
MSG_EMOJI_ROW
<p class="codes">Clique un emoji pour l'insérer où est ton curseur dans le texte.</p>
<label>Couleurs</label>
<select name="colorMode" id="colorMode" onchange="updateColorMode()">
<option value="single">Une seule couleur</option>
<option value="panels">Une couleur par panneau</option>
<option value="rainbow">Arc-en-ciel</option>
</select>
<div id="singleColor"><label>Couleur</label><input type="color" name="messageColor" value="MSG_COLOR"></div>
<div id="panelColors" style="display:none" class="colors">
<div class="colorbox"><label>Panneau 1</label><input type="color" name="messageP1" value="MSG_P1"></div>
<div class="colorbox"><label>Panneau 2</label><input type="color" name="messageP2" value="MSG_P2"></div>
<div class="colorbox"><label>Panneau 3</label><input type="color" name="messageP3" value="MSG_P3"></div>
</div>
<br><button type="submit">AFFICHER LE MESSAGE</button>
</form>
</div>
<div class="card">
<h2>Messages programmés (10 maximum)</h2>
<form action="/schedule" method="POST">
<input type="date" name="date" required>
<input type="text" id="schedText" name="message" maxlength="140" placeholder="Message pour cette journée" required>
SCHED_EMOJI_ROW
<br><button type="submit">AJOUTER</button>
</form>
SCHEDULE_LIST_HTML
</div>
<script>
function updateColorMode(){
const m=document.getElementById('colorMode').value;
document.getElementById('singleColor').style.display=(m==='single')?'block':'none';
document.getElementById('panelColors').style.display=(m==='panels')?'flex':'none';
}
function syncVal(id){
document.getElementById('val_'+id).innerText = document.getElementById(id).value;
}
function step(id, amount){
const el = document.getElementById(id);
let v = parseInt(el.value) + amount;
const min = parseInt(el.min);
const max = parseInt(el.max);
if (v < min) v = min;
if (v > max) v = max;
el.value = v;
syncVal(id);
}
function insertEmoji(inputId, code){
const el = document.getElementById(inputId);
const start = el.selectionStart != null ? el.selectionStart : el.value.length;
const end = el.selectionEnd != null ? el.selectionEnd : el.value.length;
const text = el.value;
const insert = '[' + code + ']';
el.value = text.substring(0, start) + insert + text.substring(end);
const newPos = start + insert.length;
el.focus();
el.setSelectionRange(newPos, newPos);
}
fetch('/settime?epoch='+Math.floor(Date.now()/1000));
</script>
</body>
</html>
)rawliteral";
page.replace("BRIGHTNESS_VALUE", String(displayBrightness));
page.replace("SIZE_VALUE", String(digitSize));
page.replace("THICK_VALUE", String(digitThickness));
page.replace("SCROLL_VALUE", String(scrollDelayMs));
page.replace("FONT_OPTIONS_HTML", fontOptionsHtml());
page.replace("AUTO_FONT_CHECKED", autoFontRotation ? "checked" : "");
page.replace("HOUR_COLOR", hourColorHex);
page.replace("MINUTE_COLOR", minuteColorHex);
page.replace("SECOND_COLOR", secondColorHex);
page.replace("MSG_COLOR", messageColorHex);
page.replace("MSG_P1", messageP1Hex);
page.replace("MSG_P2", messageP2Hex);
page.replace("MSG_P3", messageP3Hex);
page.replace("MSG_EMOJI_ROW", emojiRowHtml("msgText"));
page.replace("SCHED_EMOJI_ROW", emojiRowHtml("schedText"));
page.replace("SCHEDULE_LIST_HTML", buildScheduleListHtml());
return page;
}
// =====================================================
// SERVEUR WEB
// =====================================================
void setupWebServer() {
server.on("/", HTTP_GET, []() {
server.send(200, "text/html; charset=utf-8", webPage());
});
server.on("/clock", HTTP_POST, []() {
manualMode = false;
manualMessage = "";
passCount = 0;
clockPause = false;
currentPlaybackMessage = "";
showClockNow();
server.sendHeader("Location", "/");
server.send(303);
});
server.on("/displaySettings", HTTP_GET, []() {
String json = "{";
json += "\"brightness\":" + String(displayBrightness) + ",";
json += "\"size\":" + String(digitSize) + ",";
json += "\"thickness\":" + String(digitThickness) + ",";
json += "\"scrollSpeed\":" + String(scrollDelayMs) + ",";
json += "\"fontStyle\":\"" + digitFontStyle + "\",";
json += "\"autoFont\":" + String(autoFontRotation ? "true" : "false") + ",";
json += "\"hourColor\":\"" + hourColorHex + "\",";
json += "\"minuteColor\":\"" + minuteColorHex + "\",";
json += "\"secondColor\":\"" + secondColorHex + "\"";
json += "}";
server.send(200, "application/json; charset=utf-8", json);
});
server.on("/displaySettings", HTTP_POST, []() {
displayBrightness = constrain(server.arg("brightness").toInt(), 1, 255);
digitSize = constrain(server.arg("size").toInt(), 50, 100);
digitThickness = constrain(server.arg("thickness").toInt(), 1, 3);
scrollDelayMs = constrain(server.arg("scrollSpeed").toInt(), 15, 150);
String fs = server.arg("fontStyle");
bool validFont = false;
for (int i = 0; i < 8; i++) {
if (fs == FONT_STYLE_LIST[i]) { validFont = true; break; }
}
if (validFont) digitFontStyle = fs;
autoFontRotation = server.hasArg("autoFont");
if (server.arg("hourColor").length() == 7) hourColorHex = server.arg("hourColor");
if (server.arg("minuteColor").length() == 7) minuteColorHex = server.arg("minuteColor");
if (server.arg("secondColor").length() == 7) secondColorHex = server.arg("secondColor");
preferences.putUChar("bright", displayBrightness);
preferences.putUChar("size", digitSize);
preferences.putUChar("thick", digitThickness);
preferences.putUShort("scrollms", scrollDelayMs);
preferences.putString("font", digitFontStyle);
preferences.putBool("autofont", autoFontRotation);
preferences.putString("hourcol", hourColorHex);
preferences.putString("mincol", minuteColorHex);
preferences.putString("seccol", secondColorHex);
dma_display->setBrightness8(displayBrightness);
refreshDisplayColors();
manualMode = false;
manualMessage = "";
passCount = 0;
clockPause = false;
currentPlaybackMessage = "";
lastEffectiveFontStyle = "";
showClockNow();
server.sendHeader("Location", "/");
server.send(303);
});
server.on("/message", HTTP_POST, []() {
manualMessage = server.arg("message");
messageColorMode = server.arg("colorMode");
if (messageColorMode != "single" &&
messageColorMode != "panels" &&
messageColorMode != "rainbow") {
messageColorMode = "single";
}
if (server.arg("messageColor").length() == 7) messageColorHex = server.arg("messageColor");
if (server.arg("messageP1").length() == 7) messageP1Hex = server.arg("messageP1");
if (server.arg("messageP2").length() == 7) messageP2Hex = server.arg("messageP2");
if (server.arg("messageP3").length() == 7) messageP3Hex = server.arg("messageP3");
preferences.putString("msgmode", messageColorMode);
preferences.putString("msgcol", messageColorHex);
preferences.putString("msgp1", messageP1Hex);
preferences.putString("msgp2", messageP2Hex);
preferences.putString("msgp3", messageP3Hex);
refreshDisplayColors();
if (manualMessage.length()) {
manualMode = true;
}
server.sendHeader("Location", "/");
server.send(303);
});
server.on("/schedule", HTTP_POST, []() {
String date = server.arg("date");
String msg = server.arg("message");
if (date.length() && msg.length()) {
int slot = -1;
for (int i = 0; i < MAX_SCHEDULES; i++) {
if (schedDate[i].length() == 0) { slot = i; break; }
}
if (slot != -1) {
schedDate[slot] = date;
schedMsg[slot] = msg;
preferences.putString(("d" + String(slot)).c_str(), date);
preferences.putString(("m" + String(slot)).c_str(), msg);
}
}
server.sendHeader("Location", "/");
server.send(303);
});
server.on("/deleteSchedule", HTTP_POST, []() {
int idx = server.arg("index").toInt();
if (idx >= 0 && idx < MAX_SCHEDULES) {
schedDate[idx] = "";
schedMsg[idx] = "";
preferences.remove(("d" + String(idx)).c_str());
preferences.remove(("m" + String(idx)).c_str());
}
server.sendHeader("Location", "/");
server.send(303);
});
server.on("/schedules", HTTP_GET, []() {
String json = "[";
bool first = true;
for (int i = 0; i < MAX_SCHEDULES; i++) {
if (schedDate[i].length() && schedMsg[i].length()) {
if (!first) json += ",";
first = false;
String safeMsg = schedMsg[i];
safeMsg.replace("\\", "\\\\");
safeMsg.replace("\"", "\\\"");
json += "{\"index\":" + String(i) + ",\"date\":\"" + schedDate[i] + "\",\"message\":\"" + safeMsg + "\"}";
}
}
json += "]";
server.send(200, "application/json; charset=utf-8", json);
});
server.on("/status", HTTP_GET, []() {
String json = "{";
json += "\"mode\":\"" + String(currentPlaybackMessage.length() ? "message" : "clock") + "\",";
json += "\"phase\":\"" + String(currentPlaybackMessage.length() ? (clockPause ? "clockPause" : "scrolling") : "none") + "\",";
json += "\"source\":\"" + currentPlaybackSource + "\",";
String safeText = currentPlaybackMessage;
safeText.replace("\\", "\\\\");
safeText.replace("\"", "\\\"");
json += "\"text\":\"" + safeText + "\"";
json += "}";
server.send(200, "application/json; charset=utf-8", json);
});
server.on("/settime", HTTP_GET, []() {
time_t epoch = (time_t)server.arg("epoch").toInt();
if (epoch > 1700000000) {
struct timeval tv;
tv.tv_sec = epoch;
tv.tv_usec = 0;
settimeofday(&tv, nullptr);
resetClockCache();
}
server.send(200, "text/plain", "OK");
});
server.begin();
}
// =====================================================
// SETUP
// =====================================================
void setup() {
Serial.begin(115200);
delay(500);
preferences.begin("barbapocalypse", false);
displayBrightness = preferences.getUChar("bright", DEFAULT_BRIGHTNESS);
digitSize = preferences.getUChar("size", 100);
digitThickness = preferences.getUChar("thick", 3);
scrollDelayMs = preferences.getUShort("scrollms", 45);
digitFontStyle = preferences.getString("font", "segments");
autoFontRotation = preferences.getBool("autofont", false);
displayBrightness = constrain(displayBrightness, 1, 255);
digitSize = constrain(digitSize, 50, 100);
digitThickness = constrain(digitThickness, 1, 3);
scrollDelayMs = constrain(scrollDelayMs, 15, 150);
hourColorHex = preferences.getString("hourcol", "#00ff96");
minuteColorHex = preferences.getString("mincol", "#00ff96");
secondColorHex = preferences.getString("seccol", "#00ff96");
messageColorMode = preferences.getString("msgmode", "single");
messageColorHex = preferences.getString("msgcol", "#00ff96");
messageP1Hex = preferences.getString("msgp1", "#00ff96");
messageP2Hex = preferences.getString("msgp2", "#a050ff");
messageP3Hex = preferences.getString("msgp3", "#ff1493");
loadSchedules();
// ===================================================
// WIFI
// ===================================================
// Toujours définir le fuseau horaire, même si Internet est absent.
setenv("TZ", "CET-1CEST,M3.5.0/2,M10.5.0/3", 1);
tzset();
setTemporaryMidnight();
WiFi.mode(WIFI_STA);
WiFi.setSleep(false);
WiFi.setAutoReconnect(true);
WiFi.persistent(true);
WiFi.setHostname("Barbapocalypse");
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.println();
Serial.println("Connexion WiFi...");
unsigned long wifiStart = millis();
while (WiFi.status() != WL_CONNECTED && millis() - wifiStart < 15000) {
delay(250);
Serial.print(".");
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println();
Serial.println("WiFi connecte");
Serial.print("Adresse : http://");
Serial.println(WiFi.localIP());
wifiWasConnected = true;
configureNtp();
} else {
Serial.println();
Serial.println("WiFi indisponible : horloge provisoire a 00:00:00");
setTemporaryMidnight();
wifiDisconnectedSince = millis();
startRescueAccessPoint();
}
// ===================================================
// HUB75
// ===================================================
HUB75_I2S_CFG::i2s_pins pins = {
R1_PIN, G1_PIN, B1_PIN,
R2_PIN, G2_PIN, B2_PIN,
A_PIN, B_PIN, C_PIN, D_PIN, E_PIN,
LAT_PIN, OE_PIN, CLK_PIN
};
HUB75_I2S_CFG config(
PANEL_RES_X * 2,
PANEL_RES_Y / 2,
PANEL_CHAIN,
pins
);
config.clkphase = false;
dma_display = new MatrixPanel_I2S_DMA(config);
if (!dma_display->begin()) {
Serial.println("ERREUR PANNEAUX");
while (true) delay(1000);
}
dma_display->setBrightness8(displayBrightness);
dma_display->clearScreen();
display = new VirtualMatrixPanel(
*dma_display,
NUM_ROWS,
NUM_COLS,
PANEL_RES_X,
PANEL_RES_Y,
VIRTUAL_MATRIX_CHAIN_TYPE
);
display->setPhysicalPanelScanRate(FOUR_SCAN_32PX_HIGH);
display->setRotation(2); // Rotation 180 degres : affichage remis tete en haut
refreshDisplayColors();
setupWebServer();
if (MDNS.begin("horloge")) {
Serial.println("Adresse locale : http://horloge.local");
}
resetClockCache();
updateClock();
}
// =====================================================
// LOOP
// =====================================================
void loop() {
server.handleClient();
maintainWifiAndTime();
static unsigned long lastDateCheck = 0;
static int todaySchedIndex = -1;
if (millis() - lastDateCheck >= 1000) {
lastDateCheck = millis();
String today = getToday();
todaySchedIndex = -1;
for (int i = 0; i < MAX_SCHEDULES; i++) {
if (schedDate[i].length() && schedMsg[i].length() && schedDate[i] == today) {
todaySchedIndex = i;
break;
}
}
}
String desired = "";
String desiredSource = "";
if (todaySchedIndex != -1) {
desired = schedMsg[todaySchedIndex];
desiredSource = "schedule";
} else if (manualMode && manualMessage.length()) {
desired = manualMessage;
desiredSource = "manual";
}
if (desired != currentPlaybackMessage) {
currentPlaybackMessage = desired;
currentPlaybackSource = desiredSource; // <-- AJOUT
passCount = 0;
clockPause = false;
if (desired.length()) {
prepareMessage(desired);
dma_display->clearScreen();
} else {
showClockNow();
}
}
if (currentPlaybackMessage.length()) {
if (clockPause) {
updateClock();
if (millis() - clockPauseStart >= 60000UL) {
clockPause = false;
passCount = 0;
scrollX = 96;
dma_display->clearScreen();
}
delay(2);
return;
}
if (drawScrollingMessage()) {
passCount++;
if (passCount >= 2) {
clockPause = true;
clockPauseStart = millis();
showClockNow();
}
}
return;
}
updateClock();
delay(2);
}