Two operable buttons, a deep sleep mode, switchable time zones, and even custom fonts and a weather station example.
You can acquire the Lilygo T-Display S3 with a matching black or gray plastic case. There is a 1.9-inch color LCD screen (320×170px) on board. The module has 2 buttons at the front (GPIO0 and GPIO14), and there is also a reset button on the side. For a stable WiFi connection, you must use WiFi.setSleep(false); before the connection is made. See sketch below.
Since we have two buttons — apart from the reset button — we can use both of them in this code. The top button was used for deep sleep mode, and the bottom button to select different time zones. We store the set time zone in flash memory, so that it is retained when we restart. We use the library preferences.h.
We also added some toys for a nicer presentation. A small animation at startup, when going into deep sleep mode, and when changing time zone.
In the TFT_eSPI library used, a file is provided in the User_Setups folder for this board: Setup206_LilyGo_T_Display_S3.h.
The sketch connects to WiFi (or starts a setup portal if no network credentials are stored yet), shows the time with a startup animation, and saves both the WiFi credentials and the last-selected time zone to flash memory.
#include <WiFi.h> // Arduino IDE - board: Lilygo T_Display-S3
#include <WebServer.h>
#include <TFT_eSPI.h> // https://github.com/Bodmer/TFT_eSPI
#include <Preferences.h> // to save the selected time zone
WebServer server(80);
Preferences flash;
TFT_eSPI tft = TFT_eSPI();
TFT_eSprite sprite = TFT_eSprite(&tft);
TFT_eSprite leftSp = TFT_eSprite(&tft);
TFT_eSprite rightS = TFT_eSprite(&tft);
uint8_t SCREEN_ORIENTATION = 3; // USB connection: 3 = left side - 1 = right side.
const char* location[] = { "Brussels", "Shanghai", "Auckland", "New York" };
const char* timeZone[] = { // avoid memory leaks by giving all elements in the array the same length (trailing spaces)
"CET-1CEST,M3.5.0,M10.5.0/3 ",
"CST-8 ",
"NZST-12NZDT,M9.5.0,M4.1.0/3",
"EST5EDT,M3.2.0,M11.1.0 "
};
const char* weekDays[] = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };
const char* startTxt[] = { "Connecting to WiFi", "WiFi OK: Time sync" };
const char* noConnec[] = { " WiFi: no connection.", " Connect to hotspot 'TD-S3'", " and open a browser",
" at adres 192.168.4.1", " to enter network name", " and password." };
bool noCon = false, freshStart = true;
String webText, buttons, ssid, pasw;
uint8_t count;
void setup() {
pinMode(15, OUTPUT); // PWD
digitalWrite(15, HIGH);
pinMode(14, INPUT_PULLUP); // button for changing time zone
pinMode(0, INPUT_PULLUP); // flash button
initDisplay();
showStartUpLogo();
connect_to_WiFi(); // Will start web server (for setup WiFi data) if connection fails
showConnected();
setTimeZoneFromFlash();
}
void loop() {
display_Time();
if (!digitalRead(14)) switchTimeZone();
if (!digitalRead(0)) displayOnOff(); // prevent burn-in
}
void initDisplay() {
tft.init(), tft.setRotation(SCREEN_ORIENTATION);
sprite.createSprite(320, 170); // sprite for faster rendering
}
void showStartUpLogo() { // use functions of library to draw = smaller than a graphic file
tft.fillScreen(TFT_BLACK);
sprite.fillSprite(sprite.color565(100, 100, 100));
for (uint16_t i = 0; i < 12000; i++) { // create surface with fine texture
uint8_t j = random(100) + 50;
sprite.drawPixel(random(320), random(170), sprite.color565(j, j, j));
}
sprite.setTextColor(tft.color565(48, 48, 48));
for (uint8_t i = 0; i < 8; i++) { // draw IC legs
sprite.drawRect(131 + (i * 10), 10, 2, 115, sprite.color565(240, 240, 240));
sprite.fillRoundRect(129 + (i * 10), 18, 6, 99, 2, sprite.color565(240, 240, 240));
sprite.drawRect(110, 32 + (i * 10), 115, 2, sprite.color565(240, 240, 240));
sprite.fillRoundRect(118, 30 + (i * 10), 99, 6, 2, sprite.color565(240, 240, 240));
}
sprite.fillRoundRect(122, 22, 91, 91, 3, TFT_BLACK); // draw IC
sprite.drawRoundRect(122, 22, 91, 91, 3, TFT_DARKGREY);
sprite.setTextFont(1);
sprite.drawCentreString("ESP32", 157, 74, 1);
sprite.drawCentreString("1732S019", 164, 86, 1);
sprite.fillCircle(200, 34, 3, sprite.color565(16, 16, 16));
sprite.setFreeFont(&FreeSans18pt7b);
sprite.setTextColor(TFT_BLACK); // embossed text below IC
sprite.drawCentreString(startTxt[0], 161, 132, 1);
sprite.setTextColor(TFT_WHITE);
sprite.drawCentreString(startTxt[0], 159, 130, 1);
sprite.setTextColor(sprite.color565(100, 100, 100));
sprite.drawCentreString(startTxt[0], 160, 131, 1);
sprite.pushSprite(0, 0);
}
void show_Message_No_Connection() {
tft.fillScreen(TFT_NAVY);
tft.setTextColor(TFT_YELLOW), tft.setTextFont(4), tft.setCursor(0, 0, 4);
for (uint8_t count = 0; count < 6; count++) tft.println(noConnec[count]);
noCon = true;
}
void showConnected() {
leftSp.createSprite(320, 58);
leftSp.fillSprite(sprite.color565(100, 100, 100));
leftSp.setFreeFont(&FreeSans18pt7b);
for (uint16_t i = 0; i < 4000; i++) {
uint8_t j = random(100) + 50;
leftSp.drawPixel(random(320), random(58), sprite.color565(j, j, j)); // random greyscale
}
leftSp.setTextColor(TFT_BLACK); // embossed text "connected"
leftSp.drawCentreString(startTxt[1], 161, 6, 1);
leftSp.setTextColor(TFT_WHITE);
leftSp.drawCentreString(startTxt[1], 159, 4, 1);
leftSp.setTextColor(sprite.color565(100, 100, 100));
leftSp.drawCentreString(startTxt[1], 160, 5, 1);
for (uint16_t i = 850; i > 650; i--) leftSp.pushSprite(0, i / 5); // slide text upwards
leftSp.pushToSprite(&sprite, 0, 131); // add new text to existing sprite
leftSp.deleteSprite();
}
void setTimeZoneFromFlash() {
flash.begin("my-clock", true); // read from flash (true = read only)
count = flash.getInt("counter", 0); // retrieve the last set time zone - default to first in the array [0]
flash.end();
count = count % (sizeof(timeZone) / sizeof(timeZone[0])); // modulo (# of elements in array) = prevent errors
configTzTime(timeZone[count], "pool.ntp.org"); // clock will automatically adjust to daylight saving time
}
void display_Time() {
struct tm tInfo; // https://cplusplus.com/reference/ctime/tm/
getLocalTime(&tInfo);
if (freshStart) splitScreen(true);
sprite.fillSprite(TFT_BLACK);
uint16_t myColor = (WiFi.status() == WL_CONNECTED) ? TFT_GREEN : TFT_RED;
sprite.fillCircle(294, 30, 6, myColor); // logo WiFi
for (uint8_t tel = 0; tel < 3; tel++) {
sprite.drawSmoothArc(294, 32, 30 - tel * 7, 28 - tel * 7, 135, 225, myColor, myColor, true);
}
sprite.setTextFont(7), sprite.setTextColor(TFT_CYAN, TFT_BLACK), sprite.setTextSize(2);
sprite.setCursor(-2, 0), sprite.printf("%02d", tInfo.tm_hour);
sprite.fillRect(126, 28, 5, 10, TFT_CYAN), sprite.fillRect(126, 56, 5, 10, TFT_CYAN); // colon between hour & minutes
sprite.setCursor(131, 0), sprite.printf("%02d", tInfo.tm_min);
sprite.setCursor(256, 45), sprite.setTextSize(1), sprite.printf("%02d", tInfo.tm_sec);
sprite.setTextColor(TFT_YELLOW), sprite.setFreeFont(&FreeSans12pt7b);
sprite.setCursor(9, 157), sprite.print(weekDays[tInfo.tm_wday]);
sprite.drawRoundRect(0, 134, 320, 34, 5, TFT_YELLOW);
sprite.setCursor(188, 159), sprite.setTextColor(TFT_CYAN);
sprite.printf("%02d-%02d-%04d", tInfo.tm_mday, 1 + tInfo.tm_mon, 1900 + tInfo.tm_year);
sprite.setTextColor(TFT_RED), sprite.drawCentreString(location[count], 160, 108, 1);
if (freshStart) splitScreen(false), freshStart = false; // before "pushSprite" because the background must be black
sprite.pushSprite(0, 0);
}
void splitScreen(bool split) { // split (true) or merge (false) sprite horizontally
leftSp.createSprite(160, 170), rightS.createSprite(160, 170);
for (uint8_t ver = 0; ver < 170; ver++) { // divide the sprite into 2 pieces & write data to two smaller sprites
for (uint16_t hor = 0; hor < 160; hor++) leftSp.drawPixel(hor, ver, sprite.readPixel(hor, ver));
for (uint16_t hor = 160; hor < 320; hor++) rightS.drawPixel(hor - 160, ver, sprite.readPixel(hor, ver));
}
if (split) { // avoid sloppy lines: make sure the remaining part of the screen is black
leftSp.drawFastVLine(159, 0, 170, TFT_BLACK), leftSp.drawFastVLine(158, 0, 170, TFT_BLACK);
rightS.drawFastVLine(0, 0, 170, TFT_BLACK), rightS.drawFastVLine(1, 0, 170, TFT_BLACK);
}
for (uint16_t hor = 0; hor < 160; hor += 2) {
if (split) leftSp.pushSprite(0 - hor, 0), rightS.pushSprite(hor + 160, 0); // move both sprites to the outer edge
else leftSp.pushSprite(hor - 160, 0), rightS.pushSprite(320 - hor, 0); // merge both sprites
}
leftSp.deleteSprite(), rightS.deleteSprite();
}
void switchTimeZone() {
leftSp.createSprite(320, 28), leftSp.setFreeFont(&FreeSans12pt7b);
leftSp.fillSprite(TFT_BLACK);
leftSp.setTextColor(TFT_RED), leftSp.drawCentreString(location[count], 160, 3, 1);
for (uint16_t tel = 0; tel < 320; tel++) leftSp.pushSprite(tel, 105); // UI debouncing
count = (count + 1) % (sizeof(timeZone) / sizeof(timeZone[0])); // increase modulo (number of elements in char array)
configTzTime(timeZone[count], "pool.ntp.org");
leftSp.fillSprite(TFT_BLACK);
leftSp.drawCentreString(location[count], 160, 3, 1);
for (int16_t tel = -320; tel < 1; tel++) leftSp.pushSprite(tel, 105);
leftSp.deleteSprite();
}
void displayOnOff() { // flash button pressed: display on - off
uint8_t savedZone;
flash.begin("my-clock"); // flash memory (also write since 2nd param = not set)
savedZone = flash.getInt("counter", 0); // retrieve the last set time zone - default to first in the array [0]
if (savedZone != count) flash.putInt("counter", count); // only write the time zone to flash memory when it was changed
flash.end(); // to prevent chip wear from excessive writing
digitalWrite(TFT_BL, !digitalRead(TFT_BL)), delay(300);
}
void connect_to_WiFi() { // connect to WiFi, if not successful: start web server
WiFi.mode(WIFI_MODE_STA);
flash.begin("login_data", true); // true = read only
ssid = flash.getString("ssid", "");
pasw = flash.getString("pasw", "");
flash.end();
WiFi.setSleep(false);
#if CONFIG_IDF_TARGET_ESP32S3
WiFi.setSleep(false); // Ensure stable WiFi on ESP32-S3
#endif
WiFi.begin(ssid.c_str(), pasw.c_str());
for (uint8_t i = 0; i < 50; ++i) { // we try for about 8 seconds to connect
if (WiFi.isConnected()) {
WiFi.setAutoReconnect(true);
WiFi.persistent(true);
return; // jumps out of this function when WiFi connection succeeds
}
delay(160);
}
show_Message_No_Connection(); // script lands on this line only when the connection fails
int n = WiFi.scanNetworks();
for (int i = 0; i < n; ++i) { // html to put found networks on buttons on web page
buttons += "\n<button onclick='scrollNaar(this.id)' id='" + WiFi.SSID(i) + "'>" + WiFi.SSID(i) + "</button><br>";
}
WiFi.mode(WIFI_MODE_AP);
delay(500);
WiFi.softAP("TD-S3", "", 1);
server.on("/", server_Root);
server.on("/setting", server_Setting);
server.begin();
for (;;) server.handleClient(); // infinite loop until the WiFi credentials are inserted
}
void server_Root() {
webText = "<!DOCTYPE HTML>\n<html lang='en'>\n<head><title>Setup</title>\n<meta name='viewport' ";
webText += "content='width=device-width, initial-scale=1.0'>";
webText += "\n<style>\np {\n font-family: Tahoma, Verdana, Arial, Helvetica, sans-serif;\n font-size: 18px;\n margin: 0;\n text-align: ";
webText += "center;\n}\n\nbutton, input[type=submit] {\n width: 250px;\n border-radius: 5px;\n color: White;\n padding:";
webText += " 4px 4px;\n margin-top: 16px;\n margin: 0 auto;\n display:block;\n font-size: 18px;\n font-weight: 600;";
webText += "\n background: DodgerBlue;\n}\n\ninput {\n width: 250px;\n font-size: 18px;\n font-weight: 600;\n}";
webText += "\n</style>\n</head>\n<body><p style='font-family:arial; ";
webText += "font-size:240%;'>WiFi setup\n</p><p style='font-family:arial; font-size:160%;'>\n<br>";
webText += "Networks found:<br> Click on item to select or<br>Enter your network data<br> in the boxes below:</p><br>";
webText += buttons;
webText += "\n<form method='get' action='setting'>\n<p><b>\nSSID: <br>\n<input id='ssid' name='ssid'>";
webText += "<br>PASW: </b><br>\n<input type='password' name='pass'><br><br>\n<input type='submit' value='Save'>";
webText += "\n</p>\n</form>\n<script>\nfunction scrollNaar(tekst) {\n document.getElementById('ssid')";
webText += ".value = tekst;\n window.scrollTo(0, document.body.scrollHeight);\n}\n</script>\n</body>\n</html>";
server.send(200, "\ntext/html", webText);
}
void server_Setting() {
webText = "<!DOCTYPE HTML>\n<html lang='en'>\n<head><title>Setup</title>\n<meta name='viewport' ";
webText += "content='width=device-width, initial-scale=1.0'>\n<style>\n* {\n font-family: Arial, Helvetica";
webText += ", sans-serif;\n font-size: 45px;\n font-weight: 600;\n margin: 0;\n text-align: center;\n}";
webText += "\n\n@keyframes op_en_neer {\n 0% {height: 0px;}\n 50% {height: 40px;}\n 100% {height: 0px;}\n}";
webText += "\n\n.opneer {\n margin: auto;\n text-align: center;\n animation: op_en_neer 2s infinite;\n}";
webText += "\n</style>\n</head>\n<body>\n<div class=\"opneer\"></div>\nESP will reboot<br>Close this window";
webText += "\n</body>\n</html>";
String myssid = server.arg("ssid"); // we want to store this in flash memory
String mypasw = server.arg("pass");
server.send(200, "\ntext/html", webText);
delay(500);
if (myssid.length() > 0 && mypasw.length() > 0) {
flash.begin("login_data", false);
flash.putString("ssid", myssid);
flash.putString("pasw", mypasw);
flash.end();
ESP.restart();
}
}
Curious about other displays? Check out the full overview, including TM1637, SSD1306, SH1106, ST7735, ST7789, e-paper, the Lilygo TTGO T-Display and the Waveshare ESP32-S3.