#include #include #include #include #include #include #include #include #include #include #include #include // ===================================================== // 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] [:(] [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 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 == "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 parseMessageTokens(const String &message) { std::vector 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 += "
"; html += "" + schedDate[i] + " - " + schedMsg[i] + ""; html += "
"; html += ""; html += ""; html += "
"; html += "
"; } } if (!any) { html = "

Aucun message programme pour le moment.

"; } 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 += "