The ESP8266 and the ESP32 are microcontrollers with WiFi on board. They can be programmed via a USB cable, connected to a PC. Cheap and easy.
After uploading the code, the microcontroller will work on its own, provided we supply power via battery or adaptor with a USB power cable. These microcontrollers are so cheap and versatile that they have largely succeeded their predecessor - the Arduino. Meanwhile, they are commonplace among thousands of hobbyists worldwide.
Thanks to the many connection possibilities (WiFi, bluetooth, IR remote control, ...) and the possible connections (relay, LED, display, sensor for temperature, etc.), it can be used to create numerous projects to control remotely (drones, thermostat, control of a lock or lamp or motor), or projects that can send alerts to a cell phone. Control via an app on smartphone (even from hundreds of kilometers away) is also easy.
The examples here are clocks with different displays. Why clocks? They are relatively simple, both in terms of hardware and programming. And they are excellent examples if you want to work with displays. An NTP clock (which synchronizes time over the Internet) must take into account DST or winter time, and must switch automatically. Otherwise, the code is incomplete. Displaying the hour as "14:7" is sloppy. There are numerous methods and functions for using a leading "0".
On this page, the code was kept as compact as possible.
Download the Arduino IDE (free)
This is the compiler, which you use to write sketches and upload the code to the microcontroller.
How to configure the Arduino IDE for the ESP32
How to configure the Arduino IDE for the ESP8266
How to install libraries in the Arduino IDE
Starter package hardware:
An inexpensive starter pack for the ESP8266
A more expensive (more complete) starter pack for the ESP8266
Solderless connecting wires
An ESP32 microcontroller
Amazon: TM1637
Connections:
ESP8266 -> TM1637
=================
GND --------> GND
3V3 --------> VCC
D2 --------> DIO
D1 --------> CLK
Sketch for a simple clock:
#include <WiFiManager.h> // https://github.com/tzapu/WiFiManager/
#include <TM1637Display.h> // https://github.com/avishorp/TM1637/
struct tm tInfo; // https://cplusplus.com/reference/ctime/tm/
WiFiManager myWiFi;
TM1637Display ledSeg(5, 4); // scl (clk) -> GPIO5 (D1) / sda (dio) -> GPIO4 (D2).
void setup() {
Serial.begin(115200); // watch serial monitor in case of new WiFi-connection
myWiFi.autoConnect("TM1637_ESP"); // connect ESP to new wifi via GSM or PC in that case
ledSeg.setBrightness(2); // 0 .. 7
configTzTime("CET-1CEST,M3.5.0,M10.5.0/3", "be.pool.ntp.org"); // https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
}
void loop() {
getLocalTime(&tInfo); // sntp sync at startup & every hour from then on
ledSeg.showNumberDecEx(tInfo.tm_hour * 100 + tInfo.tm_min, 0x40 * (millis() % 1000 < 500), 1); // 0x40 = ":", 1 = leading zeros
}
How does it work?
Where are the libraries?
In the many examples of clocks you find on the Internet, you see "NTPClient.h", "Time.h", "TimeLib.h" and "WiFiUDP.h". Where are those in this sketch?
Short answer: you don't need those. Time.h is built into the ESP core. So we should not add these libraries to the list of the Arduino IDE, and we should not declare them in these sketches.
The following code for our "minimum clock" is adapted for both the ESP8266 and the ESP32. The default time synchronization interval for the ESP32 is 3 hours, for the ESP8266 it is 1 hour. The only modifications are the display GPIO connections:
Connections:
ESP32 ---> TM1637
=================
GND --------> GND
3V3 --------> VCC
G21 --------> DIO
G22 --------> CLK
Sketch:
#include <WiFiManager.h> // https://github.com/tzapu/WiFiManager/
#include <TM1637Display.h> // https://github.com/avishorp/TM1637/
struct tm tInfo; // https://cplusplus.com/reference/ctime/tm/
WiFiManager myWiFi;
#if defined(ESP32)
TM1637Display ledSeg(22, 21); // scl (clk) -> G22 / sda (dio) -> G21.
#elif defined(ESP8266)
TM1637Display ledSeg(5, 4); // scl (clk) -> GPIO5 (D1) / sda (dio) -> GPIO4 (D2).
#endif
void setup() {
Serial.begin(115200); // watch serial monitor in case of new WiFi-connection
myWiFi.autoConnect("TM1637_ESP"); // connect ESP to new wifi via GSM or PC in that case
ledSeg.setBrightness(2); // 0 .. 7
configTzTime("CET-1CEST,M3.5.0,M10.5.0/3", "be.pool.ntp.org"); // https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
}
void loop() {
getLocalTime(&tInfo); // sntp sync at startup & every hour from then on
ledSeg.showNumberDecEx(tInfo.tm_hour * 100 + tInfo.tm_min, 0x40 * (millis() % 1000 < 500), 1); // 0x40 = ":", 1 = leading zeros
}
We could extend the sketch so that when the clock establishes a WiFi connection during startup, it looks up its own location via an API. Then the time zone could be assigned automatically. Thus, you could create an NTP clock with the ESP that you could use anywhere in the world. After connecting to an available WiFi, it displays the correct local time.
This is a small and inexpensive display (0.96"), with easy connection via the same pins as I2C. This display can show much more information than an LED segment display. This example code can be put on both the ESP8266 and the ESP32 without changes. Of course, SCL and SDA must be connected to D1 (GPIO5) and D2 (GPIO4)[ESP8266] or to 22 and 21 [ESP32] respectively.
In the code below, we have set a 4-hour interval to synchronize with SNTP.
Amazon: SSD1306
Amazon: SSD1306 (blue text)
Connections:
ESP32 ---> SDD1306 / SH1106
===========================
GND --------> GND
3V3 --------> VCC
G21 --------> SDA
G22 --------> SCL
===========================
ESP8266 -> SDD1306 / SH1106
===========================
GND --------> GND
3V3 --------> VCC
D2 --------> SDA
D1 --------> SCL
Sketch:
#include <WiFiManager.h> // https://github.com/tzapu/WiFiManager/
#if defined(ESP32)
#include <esp_sntp.h> // ESP32 core
#endif
#include <Adafruit_SSD1306.h>
#include <Fonts/FreeSansBold12pt7b.h>
#include <Fonts/FreeSansBold18pt7b.h>
WiFiManager myWiFi;
Adafruit_SSD1306 display(128, 64, &Wire, -1); // GND -> GND, 3V3 -> VCC, esp32: G21 -> SDA, G22 -> SCL
struct tm tInfo; // https://cplusplus.com/reference/ctime/tm/ esp8266: D1 -> SCL, D2 -> SDA
void setup() {
Serial.begin(115200); // watch serial monitor in case of new WiFi-connection
display.begin(2, 0x3C);
display.setTextColor(1); // white = 1, black = 0
display.setFont(&FreeSansBold12pt7b);
myWiFi.autoConnect("SSD1306_ESP"); // connect ESP to new wifi via GSM or PC if new WiFi-connection
#if defined(ESP32)
sntp_set_sync_interval(4 * 60 * 60 * 1000UL);
#endif
configTzTime("CET-1CEST,M3.5.0,M10.5.0/3", "be.pool.ntp.org");
}
void loop() {
getLocalTime(&tInfo); // sntp sync at startup & every 4 hours from then on
display.clearDisplay();
display.drawRect(0, 0, 128, 40, 1);
display.setFont(&FreeSansBold18pt7b);
display.setCursor(4, 31);
display.printf("%02d:%02d", tInfo.tm_hour, tInfo.tm_min);
display.setFont(&FreeSansBold12pt7b);
display.printf(":%02d", tInfo.tm_sec);
display.setCursor(4, 63);
display.printf("%02d-%02d-%04d", tInfo.tm_mday, 1 + tInfo.tm_mon, 1900 + tInfo.tm_year);
display.display();
}
#if defined(ESP8266)
uint32_t sntp_update_delay_MS_rfc_not_less_than_15000() { // declare only, unnecessary to call this function.
return 4 * 60 * 60 * 1000UL; // NTP sync every 4 hr
}
#endif
This is a slightly larger display (1.3 inches), also easy to connect via the same pins as I2C. Compared to the previous code, he library was modified and the time synchronization interval omitted. This code can also be used on both the ESP8266 and ESP32 without changes. Of course, SCL and SDA must go to D1 (GPIO5) and D2 (GPIO4)[ESP8266] or to 22 and 21 [ESP32] respectively.
Aliexpress: SH1106 (blue text)
Connections:
ESP32 ---> SDD1306 / SH1106
===========================
GND --------> GND
3V3 --------> VCC
G21 --------> SDA
G22 --------> SCL
===========================
ESP8266 -> SDD1306 / SH1106
===========================
GND --------> GND
3V3 --------> VCC
D2 --------> SDA
D1 --------> SCL
Sketch:
#include <WiFiManager.h> // https://github.com/tzapu/WiFiManager/
#include <Adafruit_SH110X.h> // https://github.com/adafruit/Adafruit_SH110X
#include <Fonts/FreeSansBold12pt7b.h>
#include <Fonts/FreeSansBold18pt7b.h>
WiFiManager myWiFi;
Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire, -1); // GND -> GND, 3V3 -> VCC, esp32: G21 -> SDA, G22 -> SCL
struct tm tInfo; // https://cplusplus.com/reference/ctime/tm/ esp8266: D1 -> SCL, D2 -> SDA
void setup() {
Serial.begin(115200); // watch serial monitor in case of new WiFi-connection
display.begin(0x3C, true); // Address 0x3C default
display.setTextColor(1); // white = 1, black = 0
display.setFont(&FreeSansBold12pt7b);
myWiFi.autoConnect("SSD1306_ESP"); // connect ESP to new wifi via GSM or PC if new WiFi-connection
configTzTime("CET-1CEST,M3.5.0,M10.5.0/3", "be.pool.ntp.org");
}
void loop() {
getLocalTime(&tInfo); // sntp sync at startup & every hour from then on (esp8266) - every 3 hours (esp32)
display.clearDisplay();
display.drawRect(0, 0, 128, 40, 1);
display.setFont(&FreeSansBold18pt7b);
display.setCursor(4, 31);
display.printf("%02d:%02d", tInfo.tm_hour, tInfo.tm_min);
display.setFont(&FreeSansBold12pt7b);
display.printf(":%02d", tInfo.tm_sec);
display.setCursor(4, 63);
display.printf("%02d-%02d-%04d", tInfo.tm_mday, 1 + tInfo.tm_mon, 1900 + tInfo.tm_year);
display.display();
}
AliExpress: ESP8266 V3 (Blue/yellow - connected: SDA->D6, SCL->D5)
AliExpress: ESP8266 V2 - without pins (White - connected: SDA->D1, SCL->D2)
This ESP8266 is very convenient. You don't need a breadboard or wires to connect the display, the display is "on board". Just plug it into your PC's USB and you can program a WiFi network scanner or a clock.
The SCL (serial clock) of the display is already connected to D5 (GPIO14) of the ESP8266, and the SDA (serial data) to D6 (GPIO12).
Here a different library is used for the display, "SSD1306.h". In the sketch shown, this library is implicitly started by the second line #include <SSD1306Wire.h>.
This library has some useful features, such as automatic centering. There are 3 fonts integrated into the library "SSD1306.h".
Sketch:
#include <WiFiManager.h> // https://github.com/tzapu/WiFiManager/
#include <SSD1306Wire.h> // https://github.com/ThingPulse/esp8266-oled-ssd1306
WiFiManager myWiFi;
SSD1306Wire display(0x3c, D6, D5); // ADDRESS, SDA, SCL
struct tm tInfo; // https://cplusplus.com/reference/ctime/tm/
char days[7][10] = { "zondag", "maandag", "dinsdag", "woensdag", "donderdag", "vrijdag", "zaterdag" };
void setup() {
Serial.begin(115200); // watch serial monitor in case of new WiFi-connection
myWiFi.autoConnect("SSD1306_ESP"); // connect ESP to new wifi via GSM or PC if new WiFi-connection
configTzTime("CET-1CEST,M3.5.0,M10.5.0/3", "pool.ntp.org");
display.init();
display.setContrast(255);
display.setTextAlignment(TEXT_ALIGN_CENTER);
}
void loop() {
char hourMin[6], second[3], theDate[11];
getLocalTime(&tInfo); // sntp sync at startup & every hour from then on
strftime(hourMin, sizeof(hourMin), "%R", &tInfo); // https://cplusplus.com/reference/ctime/strftime
strftime(second, sizeof(second), "%S", &tInfo);
strftime(theDate, sizeof(theDate), "%d-%m-%G", &tInfo);
display.clear();
display.setFont(ArialMT_Plain_24); // display hour:minute in a larger font
display.drawString(54, 0, hourMin);
display.setFont(ArialMT_Plain_16);
display.drawString(95, 7, second);
display.drawString(64, 26, theDate);
display.drawString(64, 45, days[tInfo.tm_wday]);
display.display();
}
A second version, this time for the black and white version from AliExpress.
We added an extra option to the code: if the "flash" button (next to the USB connector) is pressed for three seconds, the WiFi data should be erased. Of course, we do not use "delay()". The serial monitor was omitted, and a function was added to show the user if no WiFi connection is set up.
Sketch:
#include <WiFiManager.h> // https://github.com/tzapu/WiFiManager/
#include <SSD1306Wire.h> // https://github.com/ThingPulse/esp8266-oled-ssd1306
WiFiManager myWiFi;
SSD1306Wire display(0x3c, D1, D2); // ADDRESS, SDA, SCL (D1, D2)
struct tm tInfo; // https://cplusplus.com/reference/ctime/tm/
char days[7][10] = { "zondag", "maandag", "dinsdag", "woensdag", "donderdag", "vrijdag", "zaterdag" };
unsigned long startC;
bool pressed = false;
void setup() {
display.init();
display.setContrast(255);
display.setTextAlignment(TEXT_ALIGN_CENTER);
myWiFi.setAPCallback(messageNoConnection); // is called when no wifi was set up yet
myWiFi.autoConnect("DUINO"); // connect ESP to new wifi via GSM or PC if new WiFi-connection
configTzTime("CET-1CEST,M3.5.0,M10.5.0/3", "pool.ntp.org");
}
void loop() {
char hourMin[6], second[3], theDate[11];
getLocalTime(&tInfo); // sntp sync at startup & every hour from then on
strftime(hourMin, sizeof(hourMin), "%R", &tInfo); // https://cplusplus.com/reference/ctime/strftime
strftime(second, sizeof(second), "%S", &tInfo);
strftime(theDate, sizeof(theDate), "%d-%m-%G", &tInfo);
display.clear();
display.setFont(ArialMT_Plain_24); // display hour:minute in a larger font
display.drawString(54, 0, hourMin);
display.setFont(ArialMT_Plain_16);
display.drawString(95, 7, second);
display.drawString(64, 26, theDate);
display.drawString(64, 45, days[tInfo.tm_wday]);
display.display();
buttonFlashPressed_3_Sec();
}
void buttonFlashPressed_3_Sec() { // flash button pressed during 3 seconds: erase WiFi credentials
if (!digitalRead(0) && (pressed) && (millis() - startC > 3000)) myWiFi.resetSettings(), ESP.restart();
if (!digitalRead(0)) {
if (!pressed) startC = millis(); // first time the loop encounters the pressed button: start counter
pressed = true; // Indicate that the button was previously pressed
} else pressed = false; // button released before the 3 seconds expired.
}
void messageNoConnection(WiFiManager* myWiFi) {
display.setFont(ArialMT_Plain_10);
display.clear();
display.drawString(64, 0, "WiFi: no connection.");
display.drawString(64, 10, "Connect to hotspot DUINO");
display.drawString(64, 20, "and open a browser at");
display.drawString(64, 30, "address 192.168.4.1");
display.drawString(64, 40, "to enter network name");
display.drawString(64, 50, "and password");
display.display();
}
With Adafruit's library, this code causes flickering on the screen. Therefore, the (much faster) library TFT_eSPI was chosen here. You have to put a file "User_Setup.h".in the folder of the library "TFT_eSPI" (see tip in red text below). You can read the circuit schematics between the display and the microcontroller from this file "User_Setup.h".
Connections:
ESP8266 ====== 128*160 display
==================================
3V3 --------------- VCC
GND --------------- GND
D8 --------------- CS
D4 --------------- RESET
D3 --------------- A0
D7 --------------- SDA
D5 --------------- SCK
3V3 --------------- LED
Sketch:
#include <WiFiManager.h> // https://github.com/tzapu/WiFiManager/
#include <TFT_eSPI.h> // https://github.com/Bodmer/TFT_eSPI
WiFiManager myWiFi;
TFT_eSPI tft = TFT_eSPI();
TFT_eSprite sprite = TFT_eSprite(&tft);
struct tm tInfo; // https://cplusplus.com/reference/ctime/tm/
char weekDay[7][10] = { "zondag", "maandag", "dinsdag", "woensdag", "donderdag", "vrijdag", "zaterdag" };
void setup() {
tft.init(), tft.setRotation(1);
myWiFi.setAPCallback(showMessageNoConnection); // is called when no wifi was found
myWiFi.autoConnect("ST7735_ESP");
sprite.createSprite(160, 128); // sprite for faster rendering
configTzTime(PSTR("CET-1CEST,M3.5.0,M10.5.0/3"), "be.pool.ntp.org");
sprite.setTextSize(1); // time zones: https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
}
void loop() {
getLocalTime(&tInfo); // sntp sync at startup & every hour from then on (esp8266)
sprite.fillScreen(TFT_BLACK);
sprite.setTextColor(TFT_CYAN, TFT_BLACK);
sprite.setTextFont(7), sprite.setCursor(0, 0, 7);
sprite.printf("%02d:%02d", tInfo.tm_hour, tInfo.tm_min);
sprite.setCursor(142, 34, 2);
sprite.printf("%02d", tInfo.tm_sec);
sprite.setTextColor(TFT_YELLOW, TFT_BLACK), sprite.setFreeFont(&FreeSansBold12pt7b); // custom font
sprite.drawCentreString(weekDay[tInfo.tm_wday], 80, 66, 1);
sprite.setTextColor(TFT_GREEN, TFT_BLACK), sprite.setCursor(20, 120, 4);
sprite.printf("%02d-%02d-%04d", tInfo.tm_mday, 1 + tInfo.tm_mon, 1900 + tInfo.tm_year); // dd-mm-yyyy
sprite.drawRect(-1, 56, 162, 40, TFT_WHITE);
sprite.pushSprite(0, 0);
}
void showMessageNoConnection(WiFiManager* myWiFi) {
tft.fillScreen(TFT_NAVY);
tft.setTextColor(TFT_YELLOW), tft.setCursor(0, 0, 2);
tft.print(F("WiFi: no connection\nConnect to hotspot\nST7735_ESP and open\n"));
tft.print(F("a browser at address\n192.168.4.1\nto enter network name\nand password"));
}
Contents of the "User_Setup.h" file in the case of an ST7735 display:
https://github.com/Bodmer/TFT_eSPI/blob/master/User_Setups/Setup2_ST7735.h
TFT_eSPI - Attention:
For easy overview, we modify the content of the file "User_Setup.h". This is a quick and dirty solution if you want to get 1 display working and see quick results.
If you use multiple displays and/or don't want to lose data when installing a new copy of this library, you'd better follow the tip from the designer of the library:
https://github.com/Bodmer/TFT_eSPI/#tips
You can comment out the entire contents of "User_Setup.h" if you work this way.
Amazon: 2.13inch e-paper
You can use the code from the sketch below unchanged for the LILYGO TTGO T5: AliExpress
The advantage of an e-paper display is that no power is needed to display information on the screen. Only a little bit to change the data on the screen. What you put on it stays there forever, even without power. The disadvantage is a slower update than the tft-spi displays (which is why the seconds are omitted), and you have to pay attention to how you "refresh" the screen so as not to get a violent "flicker". The library used does not have a function "drawCentreString" we have to create our own function for a string we want to display centered.
Connections:
ESP32 ---> 2.13 inch e-paper
============================
G4 --------> BUSY
G16 --------> RST
G17 --------> DC
G5 --------> CS
G18 --------> CLK
G23 --------> DIN
GND --------> GND
3V3 --------> VCC
Sketch:
#include <GxEPD.h>
#include <GxDEPG0213BN/GxDEPG0213BN.h> // 2.13" black/white 128x250 px
#include <WiFiManager.h> // https://github.com/tzapu/WiFiManager/
#include <esp_sntp.h> // ESP32 core
#include <Fonts/FreeSansBold24pt7b.h>
#include <Fonts/FreeSansBold18pt7b.h>
#include <Fonts/FreeSansBold9pt7b.h>
#include <GxIO/GxIO_SPI/GxIO_SPI.h>
#include <GxIO/GxIO.h>
char days[7][10] = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };
int previousMinute;
GxIO_Class io(SPI, /*CS=5*/ SS, /*DC=*/17, /*RST=*/16); // 17 & 16 = arbitrary choice
GxEPD_Class display(io, /*RST=*/16, /*BUSY=*/4); // 16 & 4 = arbitrary choice
struct tm tInfo; // https://cplusplus.com/reference/ctime/tm/
WiFiManager myWiFi;
void setup() {
display.init();
display.setRotation(1); // landscape
display.setTextColor(GxEPD_BLACK, GxEPD_WHITE); // GxEPD_BLACK = 0x0000, GxEPD_WHITE = 0xFFFF
myWiFi.setAPCallback(noConnection); // on screen instructions for wifi connection
myWiFi.autoConnect("E-PAPER-ESP32"); // will also reconnect after connection was lost
sntp_set_sync_interval(4 * 60 * 60 * 1000UL); // sntp sync every 4 hr
configTzTime("CET-1CEST,M3.5.0,M10.5.0/3", "be.pool.ntp.org");
display.fillScreen(GxEPD_WHITE);
display.update(); // fully updating the display causes flicker for 2 seconds
}
void loop() {
char hourMin[6], theDate[11];
getLocalTime(&tInfo);
if (previousMinute != tInfo.tm_min) { // update display only when minute changes
strftime(hourMin, sizeof(hourMin), "%R", &tInfo); // https://cplusplus.com/reference/ctime/strftime
strftime(theDate, sizeof(theDate), "%d-%m-%G", &tInfo); // this display is too slow to show the seconds
display.fillScreen(GxEPD_WHITE);
showCentered(39, hourMin);
showCentered(84, days[tInfo.tm_wday]);
showCentered(126, theDate);
previousMinute = tInfo.tm_min;
display.updateWindow(66, 0, 116, 41, true); // partial update time (= no flicker)
display.updateWindow(0, 50, 250, 78, true); // partial update date
display.powerDown();
}
}
void showCentered(byte myHeight, const char* myText) {
display.setTextColor(GxEPD_WHITE);
display.setFont(strlen(myText) > 9 ? &FreeSansBold18pt7b : &FreeSansBold24pt7b); // date = smaller font
display.setCursor(0, myHeight);
display.print(myText);
display.setTextColor(GxEPD_BLACK);
display.setCursor((display.width() - display.getCursorX()) / 2, myHeight);
display.print(myText);
}
void noConnection(WiFiManager* myWiFi) {
display.fillScreen(GxEPD_WHITE);
display.setFont(&FreeSansBold9pt7b);
display.setTextSize(1);
showNoConnection(18, F("WiFi: no connection."));
showNoConnection(35, F("Connect to hotspot:"));
showNoConnection(52, F("E-PAPER-ESP32 and open"));
showNoConnection(69, F("a browser at address"));
showNoConnection(86, F("192.168.4.1"));
showNoConnection(103, F("to enter network name"));
showNoConnection(120, F("and password"));
display.update();
}
void showNoConnection(byte top, const __FlashStringHelper* tekst) {
display.setCursor(0, top); // allows for 7 lines on a 128 px height display
display.print(tekst); // println without setCursor = interline of text is too large
}
Amazon: 2 inch display
This display has a resolution of 320 * 240 pixels, providing a very sharp image.
The display has a high refresh rate, you could display - if desired - hundredths of a second. The fastest and most versatile library for such displays is TFT-eSPI (Bodmer). It includes numerous examples, analog clocks, fonts, sprites, various gauges, the game "pong", graphs and fractals.
The code contains about 10 lines more than that of the e-paper above, because it provides functions for the synchronization interval and the callback of the synchronization.
Because the TFT_eSPI library supports multiple displays, you need to set up a file "User_Setup.h" (or choose a predefined setup from the "User_Setups" folder of the TFT_eSPI library). Below the code I'll go into more detail on that. You will also find the schematics (ESP8266 - display) below.
Connections:
ESP8266 | 2 inch Waveshare (ST7739V driver) | 2.4 inch Waveshare (ILI9341 driver)
===============================
GND ------- GND
3V3 ------- VCC
D3 ------- DC
RST ------- RST
D5 ------- CLK
D7 ------- DIN
D8 ------- CS
------- BL (not connected - niet verbonden)
Sketch:
#include <WiFiManager.h> // https://github.com/tzapu/WiFiManager/
#include <TFT_eSPI.h> // https://github.com/Bodmer/TFT_eSPI
#include <coredecls.h> // ESP8266 core
struct tm tInfo; // https://cplusplus.com/reference/ctime/tm/
char days[7][10] = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };
byte previousHour = 24, previousMinute = 61, previousSecond;
String previousWeekDay;
TFT_eSPI tft = TFT_eSPI();
WiFiManager myWiFi;
void setup() {
Serial.begin(115200);
tft.init();
tft.setRotation(1);
tft.fillScreen(TFT_BLACK); // clear screen from any previous session
myWiFi.setAPCallback(showMessageNoConnection);
myWiFi.autoConnect("ST7789_ESP");
settimeofday_cb(esp8266NtpCallback); // we need "#include <coredecls.h>" for this
tft.fillScreen(TFT_BLACK); // clear screen from "showMessageNoConnection" messages
configTzTime(PSTR("CET-1CEST,M3.5.0,M10.5.0/3"), "be.pool.ntp.org");
tft.setTextColor(TFT_CYAN);
tft.setTextSize(2);
tft.setCursor(117, 14, 7);
tft.print(":");
}
void loop() {
showChangesOnDisplay();
WiFi.isConnected() ? showLogoWiFi(TFT_GREEN) : showLogoWiFi(TFT_RED);
}
void showTime(char timeChar[2], int leftS, int top, bool large) {
tft.setCursor(leftS, top, 7); // 7 is a built in font
tft.setTextColor(TFT_CYAN, TFT_BLACK);
tft.setTextSize(large + 1);
tft.print(timeChar);
tft.setTextSize(1);
}
void showChangesOnDisplay() { // this function divides the screen changes
char hourChar[3], minChar[3], secChar[3], theDate[11]; // into chunks to prevent flicker
getLocalTime(&tInfo);
if (tInfo.tm_sec != previousSecond) {
sprintf(secChar, "%02d", tInfo.tm_sec); // format 2 digits decimal
showTime(secChar, 256, 61, false);
previousSecond = tInfo.tm_sec;
}
if (tInfo.tm_min != previousMinute) {
sprintf(minChar, "%02d", tInfo.tm_min);
showTime(minChar, 131, 14, true);
previousMinute = tInfo.tm_min;
}
if (previousHour != tInfo.tm_hour) {
sprintf(hourChar, "%02d", tInfo.tm_hour);
showTime(hourChar, -2, 14, true);
previousHour = tInfo.tm_hour;
if (previousWeekDay != days[tInfo.tm_wday]) {
tft.fillRect(0, 134, 320, 106, TFT_BLACK);
tft.setTextColor(TFT_YELLOW);
tft.setFreeFont(&FreeSansBold24pt7b); // custom font. &FreeMonoBold24pt7b is possible too
tft.drawCentreString(days[tInfo.tm_wday], 160, 136, 1);
tft.drawRoundRect(0, 124, 320, 70, 3, TFT_WHITE);
tft.setTextColor(TFT_GREEN);
strftime(theDate, sizeof(theDate), "%d-%m-%G", &tInfo);
tft.drawCentreString(theDate, 160, 202, 1);
tft.setTextFont(7);
previousWeekDay = days[tInfo.tm_wday];
}
}
}
void showLogoWiFi(int myColor) {
tft.fillCircle(294, 30, 6, myColor);
for (byte tel = 0; tel < 3; tel++) {
tft.drawSmoothArc(294, 32, 30 - tel * 7, 28 - tel * 7, 135, 225, myColor, myColor, true);
}
}
void showMessageNoConnection(WiFiManager* myWiFi) {
tft.setCursor(0, 0, 4);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.print(F("\nWiFi: no connection\nConnect to hotspot:\n"));
tft.print(F("ST7789_ESP and open\na browser at address \n192.168.4.1\n"));
tft.print(F("to enter network name\nand password"));
}
void esp8266NtpCallback(bool from_sntp) { // in setup: "settimeofday_cb(esp8266NtpCallback)";
if (from_sntp) Serial.printf("\n*** NTP Sync ***\n%s \n", getenv("TZ"));
}
uint32_t sntp_update_delay_MS_rfc_not_less_than_15000() { // declare only, unnecessary to call this function
return 4 * 60 * 60 * 1000UL; // SNTP sync every 4 hr
}
You'll find among the predefined "User_Setups" the most common configurations. For example the "Setup18_ST7789.h" in this folder can be used for this project. Should the displayed colors not be correct (red and green swapped): you can set the 3rd line active and uncomment the 4th line. See tip in red text below. Contents of the User_Setup.h file (for this project/display; in the TFT_eSPI folder - libraries):
// ST7789
#define USER_SETUP_INFO "User_Setup"
#define ST7789_DRIVER
// #define TFT_RGB_ORDER TFT_RGB // Colour order Red-Green-Blue
#define TFT_RGB_ORDER TFT_BGR // Colour order Blue-Green-Red
#define TFT_HEIGHT 320 // ST7789 240 x 320
#define TFT_CS PIN_D8 // Chip select control pin D8
#define TFT_DC PIN_D3 // Data Command control pin
#define TFT_RST -1 // Set TFT_RST to -1 if the display RESET is connected to NodeMCU RST or 3.3V
// fonts die je niet gebruikt best uitcommenten om FLASH te besparen
#define LOAD_GLCD // Font 1. Original Adafruit 8 pixel font needs ~1820 bytes in FLASH
#define LOAD_FONT2 // Font 2. Small 16 pixel high font, needs ~3534 bytes in FLASH, 96 characters
#define LOAD_FONT4 // Font 4. Medium 26 pixel high font, needs ~5848 bytes in FLASH, 96 characters
#define LOAD_FONT6 // Font 6. Large 48 pixel font, needs ~2666 bytes in FLASH, only characters 1234567890:-.apm
#define LOAD_FONT7 // Font 7. 7 segment 48 pixel font, needs ~2438 bytes in FLASH, only characters 1234567890:-.
#define LOAD_FONT8 // Font 8. Large 75 pixel font needs ~3256 bytes in FLASH, only characters 1234567890:-.
#define LOAD_FONT8N // Font 8. Alternative to Font 8 above, slightly narrower, so 3 digits fit a 160 pixel TFT
#define LOAD_GFXFF // FreeFonts. Include access to the 48 Adafruit_GFX free fonts FF1 to FF48 and custom fonts
#define SPI_FREQUENCY 27000000
#define SPI_READ_FREQUENCY 20000000
#define SPI_TOUCH_FREQUENCY 2500000
TFT_eSPI - Attention:
For easy overview, we modify the content of the file "User_Setup.h". This is a quick and dirty solution if you want to get 1 display working and see quick results.
If you use multiple displays and/or don't want to lose data when installing a new copy of this library, you'd better follow the tip from the designer of the library:
https://github.com/Bodmer/TFT_eSPI/#tips
You can comment out the entire contents of "User_Setup.h" if you work this way.
This is a convenient combination of an ESP32 and a relatively large touch screen.
You won't find many examples of this combination for the Arduino IDE.
We do not use the touch part in this example (but it works perfectly), you can just load (and test) that from the examples of the library "TFT_eSPI".
In this sketch, we retrieve dynamic electricity prices (Europe) via an API to show them on the display. We can change the view to numeric data or graph (with the flash button).
Obviously there is no circuit schematics to display, but it is quite a search if you want to configure this system for the Arduino IDE. We will again use the library TFT-eSPI (Bodmer). You need to place a file "User_Setup.h" in the folder of the library "TFT_eSPI".
Contents of the file "User_Setup.h" for the ESP32-3248S035:
#define ST7796_DRIVER
#define TFT_WIDTH 320
#define TFT_HEIGHT 480 //
#define TFT_BL 27 // LED back-light control pin
#define TFT_BACKLIGHT_ON HIGH // Level to turn ON back-light (HIGH or LOW)
#define TFT_MISO 12
#define TFT_MOSI 13 // In some display driver board, it might be written as "SDA" and so on.
#define TFT_SCLK 14
#define TFT_CS 15 // Chip select control pin
#define TFT_DC 2 // Data Command control pin
#define TFT_RST -1 // Reset pin (could connect to Arduino RESET pin)
#define TOUCH_CS 33 // Chip select pin (T_CS) of touch screen
#define LOAD_GLCD // Font 1. Original Adafruit 8 pixel font needs ~1820 bytes in FLASH
#define LOAD_FONT2 // Font 2. Small 16 pixel high font, needs ~3534 bytes in FLASH, 96 characters
#define LOAD_FONT4 // Font 4. Medium 26 pixel high font, needs ~5848 bytes in FLASH, 96 characters
#define LOAD_FONT6 // Font 6. Large 48 pixel font, needs ~2666 bytes in FLASH, only characters 1234567890:-.apm
#define LOAD_FONT7 // Font 7. 7 segment 48 pixel font, needs ~2438 bytes in FLASH, only characters 1234567890:-.
#define LOAD_FONT8 // Font 8. Large 75 pixel font needs ~3256 bytes in FLASH, only characters 1234567890:-.
// #define LOAD_FONT8N // Font 8. Alternative to Font 8 above, slightly narrower, so 3 digits fit a 160 pixel TFT
#define LOAD_GFXFF // FreeFonts. Include access to the 48 Adafruit_GFX free fonts FF1 to FF48 and custom fonts
// Comment out the #define below to stop the SPIFFS filing system and smooth font code being loaded
// this will save ~20kbytes of FLASH
#define SMOOTH_FONT
#define SPI_FREQUENCY 65000000
#define SPI_READ_FREQUENCY 20000000
#define SPI_TOUCH_FREQUENCY 2500000 //2500000
TFT_eSPI - Attention:
For easy overview, we modify the content of the file "User_Setup.h". This is a quick and dirty solution if you want to get 1 display working and see quick results.
If you use multiple displays and/or don't want to lose data when installing a new copy of this library, you'd better follow the tip from the designer of the library:
https://github.com/Bodmer/TFT_eSPI/#tips
You can comment out the entire contents of "User_Setup.h" if you work this way.
Sketch:
/*==========================================================================================
This sketch was written for the ESP32-3248S035 with integrated display (480 * 320 pixels).
If a WiFi connection cannot be made, a web server is started to store the WiFi credentials.
Dynamic electricity prices (Europe) are retrieved via an API and displayed on the screen.
The flash button switches the display between figures in two columns or graph.
Prices for the next day are available between 1200 hours and 2100 hours (UTC).
The script will check (starting at 1200 UTC) every hour if new API data is available.
The script keeps repeating this every hour until it succeeds. If the first bidding zone
returns no results any more, the script tries the second zone. If only the second bidding
zone produces results, every hour an attempt is made to obtain the data for the first zone.
=============================================================================================
Arduino IDE - board: ESP32 Dev Module or DOIT ESP32 DEVKIT V1
https://nl.aliexpress.com/item/1005006398547061.html
https://nl.aliexpress.com/item/1005005900820162.html
==========================================================================================*/
#include <WiFiManager.h> // https://github.com/tzapu/WiFiManager/
#include <TFT_eSPI.h> // https://github.com/Bodmer/TFT_eSPI
#include <ArduinoJson.h> // https://github.com/bblanchon/ArduinoJson
#include <HTTPClient.h> // for API connection
#include <Preferences.h> // no need to install this library
TFT_eSPI tft = TFT_eSPI(); // User_Setup.h settings available on: https://espgo.be/index-en.html#2035
WiFiManager myWiFi;
Preferences flash;
#define SHOW_ITEMS 34 // max number of records you want to process
#define this_Ssid "S035_ESP"
struct tm tInfo; // https://cplusplus.com/reference/ctime/tm/
bool chart = true, bidLU = false, freshStart = true, cursorUpdated = false;
unsigned long lastAction;
constexpr uint8_t VALUE_RED = 100, FLASH_BUTTON = 0; // electricity price in red color if higher than VALUE_RED
const char* biddingZone[] = { "BE", "DE-LU" }; // default & backup bidding zone
double prijzen[SHOW_ITEMS]; // most recent prices
double prices[SHOW_ITEMS * 2]; // all prices returned by the API
double priceNow; // price of current hour
time_t epo[SHOW_ITEMS]; // most recent epoch times
time_t epoc[SHOW_ITEMS * 2]; // all epoch times returned by the API
void setup() {
const char* time_Zone = "CET-1CEST,M3.5.0,M10.5.0/3"; // https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
pinMode(FLASH_BUTTON, INPUT_PULLUP);
for (const uint8_t &pin : {4, 16, 17}) pinMode(pin, OUTPUT), digitalWrite(pin, HIGH); // red, green & blue LEDs off
tft_Setup();
getBidZoneFromFlash();
myWiFi.setAPCallback(messageNoConnection);
myWiFi.autoConnect(this_Ssid);
configTzTime(time_Zone, "be.pool.ntp.org");
showStartupData();
}
void loop() {
display_Time();
if (!digitalRead(FLASH_BUTTON)) changeView(); // flash button changes between chart or list of figures
// if (priceNow < 0) start_Energy-Guzzling_device(); // priceNow = current electricity price
// else stop_Energy-Guzzling_device();
}
void tft_Setup() { // no sprites because the data on the screen is rather static
tft.init();
tft.setRotation(3);
tft.setTextWrap(false);
tft.fillScreen(TFT_BLACK); // delete output from previous sessions
tft.drawCentreString("Connecting to WIFi", 240, 160, 4);
}
void getBidZoneFromFlash() {
flash.begin("bid_zone", true); // read from flash, true = read only
bidLU = flash.getBool("bid_zone", 0); // retrieve the last set bidding zone - default to first in the array [0]
flash.end();
}
bool timeElapsed(unsigned long myMillis) { // avoid blocking task "delay(millisec)"
return millis() - lastAction >= myMillis;
}
void showStartupData() {
tft.fillScreen(TFT_BLACK);
tft.drawCentreString("Retrieving SNTP time", 240, 160, 4);
getLocalTime(&tInfo);
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_ORANGE);
tft.drawCentreString("Retrieving Json Data", 240, 160, 4);
}
void changeView() {
if (timeElapsed(800)) { // UI debouncing (flash button)
chart = !chart;
chart ? show_Chart() : show_Data();
showCurrentPrice();
lastAction = millis();
}
}
void display_Time() {
char theDate[11], theTime[9];
getLocalTime(&tInfo); // SNTP sync at startup and every 3 hours thereafter (ESP32)
time_t now;
time(&now); // assign current epoch time (UTC) to "now"
tft.drawRect(-1, 26, 482, 2, WiFi.isConnected() ? TFT_GREEN : TFT_RED);
strftime(theDate, sizeof(theDate), "%d-%m-%G", &tInfo); // https://cplusplus.com/reference/ctime/strftime/
strftime(theTime, sizeof(theTime), "%T", &tInfo);
tft.setTextColor(TFT_GREEN, TFT_BLACK), tft.drawString(theDate, 2, 0, 4);
tft.setTextColor(TFT_WHITE, TFT_BLACK), tft.drawString(theTime, 139, 0, 4);
tft.drawRightString(bidLU ? "Lux" : "Bel", 480, 8, 2);
if ((freshStart) || ((((epo[SHOW_ITEMS - 1] - now) / 3600) < 9) && (tInfo.tm_min == 2) && (tInfo.tm_sec == 1))) {
if (WiFi.isConnected()) get_JsonData(); // ^^from 1200 hrs UTC: try to retrieve new data every hour until it succeeds
chart ? show_Chart() : show_Data(), showCurrentPrice();
freshStart = false;
}
if (tInfo.tm_min == 0) {
if (tInfo.tm_sec == 0) tryDefaultZone(); // if not default bidding zone, try default zone every new hour
if (tInfo.tm_sec == 1) cursorUpdated = false; // reset update flag
}
}
void tryDefaultZone() { // if current bidding zone isn't the default bidding zone, try default zone
if (!cursorUpdated) { // prevent updating several times per cycle
cursorUpdated = true;
if (bidLU) { // if the current bidding zone isn't the default zone
flash.begin("bid_zone", false); // flash memory (false = also write)
flash.putBool("bid_zone", false); // store default bidding zone to flash memory
flash.end();
ESP.restart(); // restart because want to retrieve the data of the default bidding zone.
}
chart ? show_Chart() : show_Data(); // refresh local data every hour
showCurrentPrice();
}
}
void show_Chart() {
tft.fillRect(0, 28, 480, 292, TFT_BLACK); // erase any previous graphic data
tft.setTextColor(tft.color565(160, 160, 160)); // set color once and adjust only when needed
for (uint8_t i = 0; i < 6; i++) { // draw horizontal grid lines and value labels
tft.drawFastHLine(0, 40 + i * 50, 480, tft.color565(48, 48, 48));
for (uint8_t j = 0; j < 4; j++) {
tft.drawRightString(String(i * 100 - 100), j * 150 + 23, 286 - i * 50, 1);
}
}
for (uint8_t i = 0; i < SHOW_ITEMS; i++) {
if (epo[i]) { // only process if there is valid data
struct tm* cor = localtime(&epo[i]); // convert epoch to local time
bool isCurrentHour = (cor->tm_hour == tInfo.tm_hour) && (cor->tm_mday == tInfo.tm_mday);
if (isCurrentHour) {
priceNow = prijzen[i];
tft.drawFastVLine(6 + i * 14, 26, 292, TFT_MAGENTA);
tft.fillTriangle((i * 14) - 4, 300, (i * 14) - 4, 310, 6 + i * 14, 305, TFT_MAGENTA);
tft.fillTriangle((i * 14) - 4, 55, (i * 14) - 4, 65, 6 + i * 14, 60, TFT_MAGENTA);
}
uint16_t myColor; // determine the color based on the value of prijzen[i]
if (prijzen[i] < 0) myColor = TFT_YELLOW;
else if (prijzen[i] < 50) myColor = TFT_GREEN;
else if (prijzen[i] < VALUE_RED) myColor = TFT_CYAN;
else myColor = TFT_RED;
int price_scaled = prijzen[i] / 2; // scale prices for display
int height_offset = (480 - prijzen[i]) / 2; // calculate offset for positive values
if (prijzen[i] < 0) { // draw the price bar
tft.fillRect(i * 14, 240, 13, 0 - price_scaled, myColor);
tft.drawFastVLine(i * 14 + 13, 240, (0 - prijzen[i]) / 2, TFT_BLACK);
} else {
tft.fillRect(i * 14, height_offset, 13, price_scaled, myColor);
tft.drawFastVLine(i * 14 + 13, height_offset, price_scaled, TFT_BLACK);
}
tft.setCursor(1 + i * 14, 230, 1); // draw the hour label at the bottom of the chart
tft.printf("%02d", cor->tm_hour);
tft.setTextColor(TFT_RED); // draw the hour label again (red)
tft.setCursor(1 + i * 14, 243, 1);
tft.printf("%02d", cor->tm_hour);
tft.setTextColor(prijzen[i] > 0 ? TFT_BLACK : TFT_YELLOW); // Reset text color to default
}
}
}
void show_Data() { // display Json data in 2 columns (time / price)
tft.fillRect(0, 28, 480, 292, TFT_BLACK);
tft.drawRect(240, 26, 2, 292, WiFi.isConnected() ? TFT_GREEN : TFT_RED);
struct tm* cor;
uint16_t xPos, yPos;
for (uint8_t i = 0; i < SHOW_ITEMS; i++) {
if (epo[i]) { // check for valid data
cor = localtime(&epo[i]); // convert epoch to local time
xPos = (i < 18) ? 0 : 260; // calculate position for cursor
yPos = 33 + (i % 18) * 16;
tft.setTextColor(TFT_LIGHTGREY, TFT_BLACK); // display index number
tft.setCursor(xPos, yPos, 2);
tft.printf(" %02d. ", i + 1);
if ((cor->tm_hour == tInfo.tm_hour) && (cor->tm_mday == tInfo.tm_mday)) { // check if current hour/day matches
priceNow = prijzen[i];
tft.setTextColor(TFT_MAGENTA, TFT_BLACK);
} else tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.printf("%02d-%02d-%02d %02d:%02d", cor->tm_mday, cor->tm_mon + 1, cor->tm_year + 1900, cor->tm_hour, cor->tm_min);
if (prijzen[i] < 0) tft.setTextColor(TFT_YELLOW, TFT_BLACK); // set color based on price value
else if (prijzen[i] < 50) tft.setTextColor(TFT_GREEN, TFT_BLACK);
else if (prijzen[i] < VALUE_RED) tft.setTextColor(TFT_CYAN, TFT_BLACK);
else tft.setTextColor(TFT_RED, TFT_BLACK);
tft.drawRightString(String(prijzen[i]), (i < 18 ? 220 : 480), yPos, 2); // display the price on the right
}
}
}
void showCurrentPrice() { // price (upper right corner of display)
tft.setCursor(264, 0, 4);
tft.setTextColor(TFT_SILVER, TFT_BLACK);
tft.print("/MWh:");
tft.fillCircle(255, 10, 9, TFT_SILVER); // euro symbol
tft.fillCircle(255, 10, 7, TFT_BLACK), tft.fillRect(258, 5, 8, 12, TFT_BLACK);
tft.drawRect(247, 8, 9, 2, TFT_SILVER), tft.drawRect(249, 11, 6, 2, TFT_SILVER);
tft.setTextColor(priceNow < VALUE_RED ? TFT_CYAN : TFT_RED);
if (priceNow < 50) tft.setTextColor(TFT_GREEN);
if (priceNow < 0) tft.setTextColor(TFT_YELLOW);
tft.fillRect(344, 0, 100, 22, TFT_BLACK);
tft.setCursor(344, 0, 4);
tft.printf("%3.2F", priceNow);
}
void get_JsonData() {
String my_link;
uint8_t API_ITEMS;
memset(prijzen, 0, sizeof(prijzen));
memset(prices, 0, sizeof(prices));
memset(epo, 0, sizeof(epo));
memset(epoc, 0, sizeof(epoc));
time_t now = mktime(&tInfo) - (mktime(&tInfo) % 3600); // convert "struct tm" to time_t and round down to the whole hour
my_link = String("https://api.energy-charts.info/price?bzn=") + biddingZone[bidLU] + "&start="
+ String(now - (24 * 3600)) + "&end="
+ String(now + (SHOW_ITEMS * 3600));
HTTPClient http;
http.begin(my_link);
if (timeElapsed(3000)) { // allows the http connection to be complete
lastAction = millis();
if (http.GET() >= 200 && http.GET() < 300) {
JsonDocument doc;
String json = http.getString();
auto error = deserializeJson(doc, json.c_str());
if (error) {
tft.drawCentreString("deserializeJson() failed", 240, 200, 4);
tft.drawCentreString(error.c_str(), 240, 250, 4);
}
for (uint8_t i = 0; i < SHOW_ITEMS * 2; i++) { // actually more data than we can show on display
prices[i] = doc["price"][i];
epoc[i] = doc["unix_seconds"][i];
API_ITEMS = i; // count number of returned items
}
} else { // http.get has failed
tft.fillScreen(TFT_BLACK);
tft.drawCentreString("Error in API response", 240, 160, 4);
tft.drawCentreString("Restarting device", 240, 190, 4);
flash.begin("bid_zone", false); // true = read only
flash.putBool("bid_zone", !bidLU); // try other bidding zone
flash.end();
delay(1000);
ESP.restart();
}
}
http.end();
uint8_t j = SHOW_ITEMS + 1;
for (uint8_t i = API_ITEMS; i >= 0; i--) { // select the most recent Json data
if (epoc[i] != 0) { // ignore empty data
j--; // counter for results
prijzen[j - 1] = prices[i]; // transferring valid prices from API results
epo[j - 1] = epoc[i]; // also hours to "local" array
if (j == 0) break; // stop when we have the [SHOW_ITEMS] most recent items
}
}
}
void messageNoConnection(WiFiManager* myWiFi) {
const char* connect[] = { "WiFi: no connection", "Connect to hotspot", this_Ssid, "Start a browser - address",
"192.168.4.1", "to save network", "and password" };
tft.fillScreen(TFT_NAVY), tft.setTextColor(TFT_YELLOW);
for (uint8_t i = 0; i < 7; i++) tft.drawCentreString(connect[i], 240, 40 + i * 30, 4);
}
LILYGO TTGO T-Display (Aliexpress)
LILYGO TTGO T-Display (Lilygo)
This is an ESP32 with an onboard SPI display (240*135px). On the front, there are two buttons that we can operate while the software is running, and a reset button on the side. Because the display is relatively small, it works quickly. You can notice that in e.g. the sprites. We can order this combination with a matching plastic box. It is a pity that there is no magnet in the back of the box (in the two following combinations there is). When using the plastic case, you have to push the buttons fairly hard. For operating a game, it is not really suitable. That's much better designed in the next Lilygo product we review here.
For clock operation (deep sleep, change time zones) the buttons do not hamper. As you notice on the video below, we save the selected time zone, so that the set time zone is retained on restart or after deep sleep mode.
We also used our own fonts and multiple animations on this development board. Click here for the sample sketch of the image below.
In the TFT_eSPI library used, a file is provided in the User_Setups folder for this board: Setup25_TTGO_T_Display.h
Sketch:
#include <WiFi.h> // Arduino IDE - board: ESP32 DEV Module
#include <WebServer.h>
#include <TFT_eSPI.h> // https://github.com/Bodmer/TFT_eSPI
#include <Preferences.h> // no need to install this library
WebServer server(80);
Preferences flash;
TFT_eSPI tft = TFT_eSPI(); // User_Setup: Setup25_TTGO_T_Display.h
TFT_eSprite sprite = TFT_eSprite(&tft);
TFT_eSprite leftSp = TFT_eSprite(&tft);
TFT_eSprite rightS = TFT_eSprite(&tft);
const char* weekdays[] = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };
const char* location[] = { "Brussels", "Shanghai", "Auckland", "New York" }; // # of items must match # of time zones (line below)
const char* timeZone[] = { "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* startTxt[] = { "connecting", "to WiFi", "WiFi OK", "Time sync" };
const char* noConnec[] = { "WiFi: no connection.", "Connect to hotspot", "'Lily', open browser", "address 192.168.4.1",
"for login + password." };
bool freshStart = true;
String webText, buttons, ssid, pasw;
uint8_t count;
void setup() {
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
pinMode(35, INPUT_PULLUP); // button "switch time zones"
pinMode(0, INPUT_PULLUP);
tft.init(), tft.setRotation(3);
tft.fillScreen(TFT_BLACK);
sprite.createSprite(240, 135); // sprite for faster rendering
show_Logo_WiFi(); // this is not really necessary but it's nicer.
connect_to_WiFi();
showConnected();
configTzTime(timeZone[count], "pool.ntp.org"); // set the time zone
}
void loop() {
displayTime();
if (!digitalRead(0)) displayOnOff(); // flash button
if (!digitalRead(35)) switchTimeZone(); // button opposite flash button
}
void show_Logo_WiFi() {
sprite.fillSprite(sprite.color565(100, 100, 100));
for (int i = 0; i < 8192; i++) { // create surface with fine texture
byte j = random(100) + 50;
sprite.drawPixel(random(240), random(135), sprite.color565(j, j, j)); // random grayscale
}
sprite.setTextColor(TFT_YELLOW);
sprite.drawCentreString(startTxt[0], 120, 76, 4), sprite.drawCentreString(startTxt[1], 120, 100, 4);
sprite.fillCircle(120, 56, 6, TFT_BLUE);
for (byte i = 0; i < 3; i++) sprite.drawSmoothArc(120, 54, 40 - i * 11, 35 - i * 11, 128, 232, TFT_BLUE, TFT_BLUE, 1);
sprite.pushSprite(0, 0);
}
void showConnected() {
for (byte i = 0; i < 3; i++) sprite.drawSmoothArc(120, 54, 40 - i * 11, 35 - i * 11, 128, 232, TFT_GREEN, TFT_GREEN, 1);
sprite.fillCircle(120, 56, 6, TFT_GREEN);
sprite.pushSprite(0, 0);
leftSp.createSprite(128, 66); // sprite for new text to be scrolled up
leftSp.fillSprite(sprite.color565(100, 100, 100));
for (int i = 0; i < 3072; i++) {
byte j = random(100) + 50;
leftSp.drawPixel(random(128), random(66), sprite.color565(j, j, j)); // random grayscale
}
leftSp.setTextColor(TFT_GREEN);
leftSp.drawCentreString(startTxt[2], 64, 5, 4);
leftSp.setTextColor(TFT_YELLOW);
leftSp.drawCentreString(startTxt[3], 64, 30, 4);
for (int i = 1024; i > 560; i--) leftSp.pushSprite(56, i / 8); // high values to slow down the animation
leftSp.pushToSprite(&sprite, 56, 70);
leftSp.deleteSprite();
}
void show_Message_No_Connection() { // message on display when there is no WiFi connection
tft.fillScreen(TFT_NAVY); // also blue color for the leftmost 9 pixels - sprite.pushSprite(9, 0) below
tft.setTextColor(TFT_YELLOW);
for (uint8_t i = 0; i < 5; i++) tft.drawCentreString(noConnec[i], 120, i * 27, 4);
}
void displayTime() {
struct tm tInfo; // https://cplusplus.com/reference/ctime/tm/
getLocalTime(&tInfo); // SNTP update every 3 hours (default ESP32) since we did not set an interval
if (freshStart) splitScreen(true);
sprite.setTextColor(TFT_CYAN, TFT_BLACK);
sprite.fillSprite(TFT_BLACK);
sprite.setFreeFont(&FreeSansBold18pt7b);
sprite.setTextSize(2);
sprite.setCursor(13, 56);
sprite.printf("%02d:%02d", tInfo.tm_hour, tInfo.tm_min);
sprite.setTextSize(1);
sprite.drawFastHLine(0, 72, 240, WiFi.isConnected() ? TFT_GREEN : TFT_RED);
sprite.setCursor(187, 58);
sprite.setFreeFont(&FreeSansBold18pt7b);
sprite.printf("%02d", tInfo.tm_sec);
sprite.setTextColor(TFT_YELLOW);
sprite.drawCentreString(weekdays[tInfo.tm_wday], 120, 80, 1); // weekday
sprite.setTextColor(TFT_RED);
sprite.setFreeFont(&FreeSans12pt7b);
sprite.setCursor(62, 134);
sprite.printf("%02d-%02d-%04d", tInfo.tm_mday, 1 + tInfo.tm_mon, 1900 + tInfo.tm_year); // date
if (freshStart) splitScreen(false);
sprite.pushSprite(0, 0);
}
void splitScreen(bool split) { // split (true) or merge (false) sprite horizontally
leftSp.createSprite(120, 135), rightS.createSprite(120, 135);
if (!split) freshStart = false;
for (byte ver = 0; ver < 135; ver++) { // divide the sprite into 2 pieces & write data to two smaller sprites
for (byte hor = 0; hor < 120; hor++) leftSp.drawPixel(hor, ver, sprite.readPixel(hor, ver));
for (byte hor = 120; hor < 240; hor++) rightS.drawPixel(hor - 120, ver, sprite.readPixel(hor, ver));
}
if (split) leftSp.drawFastVLine(119, 0, 135, TFT_BLACK), rightS.drawFastVLine(0, 0, 135, TFT_BLACK);
for (byte hor = 0; hor < 120; hor++) { // move both sprites to the left & right outer edges
if (split) leftSp.pushSprite(0 - hor, 0), rightS.pushSprite(hor + 120, 0);
else leftSp.pushSprite(hor - 120, 0), rightS.pushSprite(240 - hor, 0);
}
leftSp.deleteSprite(), rightS.deleteSprite();
}
void switchTimeZone() {
leftSp.createSprite(240, 28), leftSp.setFreeFont(&FreeSans12pt7b);
leftSp.fillSprite(TFT_BLACK);
leftSp.setTextColor(TFT_RED), leftSp.drawCentreString(location[count], 120, 0, 1);
for (int tel = 0; tel < 240; tel++) leftSp.pushSprite(tel, 110); // we need an animation that uses up time = UI debouncing
count = (count + 1) % (sizeof(timeZone) / sizeof(timeZone[0])); // increase counter modulo (number of elements in string array)
configTzTime(timeZone[count], "pool.ntp.org");
leftSp.fillSprite(TFT_BLACK);
leftSp.drawCentreString(location[count], 120, 0, 1);
for (int tel = -960; tel < 1; tel++) leftSp.pushSprite(tel / 4, 114);
leftSp.deleteSprite();
}
void displayOnOff() {
byte savedZone;
flash.begin("my-clock"); // read from flash memory
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.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);
WiFi.softAP("Lily", "", 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();
}
}
LILYGO T-Display S3
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.
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
You can also use your own fonts on a ESP. Click here for the sample sketch of the image below.
Of course, a simple weather station is also possible. Click here for the sample sketch of the image below.
Sketch:
#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(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.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);
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();
}
}
The ESP8266 / ESP32 is basically a programmable switch of GPIO pins, with WiFi and Bluetooth / BLE. WiFi is almost always used with the ESP: most programmable boards without WiFi are much more energy efficient than the ESP. So we can say that we always use the ESP for WiFi.
So how do we best program the connection?
Hard-coding the login information in the sketch? That's a bad idea. Add a separate file “credentials.h” with SSID and password to the sketch? Neither can developing a professional application that way. A customer will not recompile a sketch, he wants his IOT gadget to just work after setup.
On startup, the ESP should try to connect to WiFi, using the login credentials from the EEPROM. If that fails, launch an access point and a web server to store the login credentials, then reboot the ESP.
That is exactly what the library WiFiManager.h does, with only 2 lines in your sketch. You'll find plenty of sketches on this page to see how that works. However, since the release of core 3.0.x (Arduino IDE, boards manager), WiFiManager.h causes errors with some boards.
If you want to avoid these errors, and don't like to depend on a library, you can manually add the code in your sketch. Here is a simple example (ESP32) connecting to WiFi, and if it fails, starting a WiFi access point and web server to store the data.
#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h> // no need to install or download this library
#include <vector> // C++ Standard Library: dynamic array container
WebServer server(80);
Preferences flash;
#define this_Ssid "ESP32-Conn"
String ssid, pasw;
std::vector<String> availableSSIDs;
void setup() {
Serial.begin(115200);
connect_to_WiFi();
Serial.println(" Connected to " + WiFi.SSID()); // script lands on this line only if the connection succeeded
}
void loop() {} // no command lines in this design
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();
Serial.print("Connecting");
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);
Serial.print(".");
}
Serial.print("\nConnection failed. Connect to hotspot\n"); // script lands on this line only when the connection fails
Serial.print(this_Ssid);
Serial.print("\nand open a browser @ 192.168.4.1 to save the WiFi credentials");
int numNetworks = WiFi.scanNetworks();
availableSSIDs.clear();
for (int i = 0; i < numNetworks; i++) availableSSIDs.push_back(WiFi.SSID(i)); // https://favtutor.com/blogs/push-back-vector-cpp
WiFi.mode(WIFI_MODE_AP);
WiFi.softAP(this_Ssid, "", 1);
server.on("/", serveWiFiSetupPage);
server.on("/setting", processWiFiSettingsSubmission);
server.begin();
for (;;) server.handleClient(); // infinite loop until the WiFi credentials are inserted
}
void serveWiFiSetupPage() { // html to insert WiFi credentials and P1 meter data
String webText = F("<!DOCTYPE HTML><html lang='en'><head><title>WiFi Setup</title>");
webText += F("<meta name='viewport' content='width=device-width, initial-scale=1.0'><style>");
webText += F("body { font-family: Arial, sans-serif; background: #f4f4f4; margin: 0; padding: 0; text-align: center; }");
webText += F(".container { max-width: 440px; background: white; padding: 20px; margin: 20px auto; border-radius: 10px;");
webText += F("box-shadow: 0 0 10px rgba(0,0,0,0.1); } h1 { font-size: 24px; color: #333; margin-bottom: 10px; }");
webText += F("p { font-size: 16px; color: #555; } label { display: block; text-align: left; margin-top: 12px; color: #333; }");
webText += F("input, select, button { width: 100%; padding: 10px; margin-top: 4px; border-radius: 5px;");
webText += F("border: 1px solid #ccc; font-size: 16px; box-sizing: border-box; }");
webText += F("input[type=submit], button { background: #007BFF; color: white; border: none; cursor: pointer; }");
webText += F("input[type=submit]:hover, button:hover { background: #0056b3; }");
webText += F("fieldset { border: 2px solid #007BFF; border-radius: 10px; padding: 16px; margin-top: 20px; background: #f0f8ff;");
webText += F("box-shadow: inset 0 0 5px rgba(0,123,255,0.1); }legend { font-weight: bold; color: white; background: ");
webText += F("#006400; padding: 4px 10px; border-radius: 5px; font-size: 16px; }");
webText += F("#ssid-buttons { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-top: 16px; }");
webText += F("#ssid-buttons button { width: auto; padding: 8px 14px; font-size: 14px; }</style></head><body>");
webText += F("<div class='container'><h1>WiFi Setup</h1><p>Select a network or enter details below.</p>");
webText += F("<div id='ssid-buttons'>");
for (const String& ssid : availableSSIDs) {
webText += "<button onclick=\"document.getElementsByName('ssid')[0].value='" + ssid + "'\">" + ssid + "</button>";
}
webText += F("</div><form method='get' action='setting'><fieldset><legend>WiFi Settings</legend>");
webText += F("<label><b>SSID:</b></label><input name='ssid' required><label><b>Password:</b></label><input type='password'");
webText += F(" id='pass' name='pass' required></fieldset>");
webText += F("<br><input type='submit' value='Save'></form></div></body></html>"); // Submit
server.send(200, "text/html", webText);
}
void processWiFiSettingsSubmission() {
String newSsid = server.arg("ssid"); // collect data from web page
String newPassword = server.arg("pass");
uint16_t newP1Interval = server.arg("p1Upd").toInt();
if (newSsid.length() > 0) { // save data to FLASH memory
flash.begin("login_data", false);
flash.putString("ssid", newSsid);
flash.putString("pasw", newPassword);
flash.end();
}
String webText = F("<!DOCTYPE HTML>\n<html lang='en'>\n<head><title>Setup</title>\n<meta name='viewport' "); // user feedback
webText += F("content='width=device-width, initial-scale=1.0'>\n<style>\n* {\n font-family: Arial, Helvetica");
webText += F(", sans-serif;\n font-size: 45px;\n font-weight: 600;\n margin: 0;\n text-align: center;\n}");
webText += F("\n\n@keyframes op_en_neer {\n 0% {height: 0px;}\n 50% {height: 40px;}\n 100% {height: 0px;}\n}");
webText += F("\n\n.opneer {\n margin: auto;\n text-align: center;\n animation: op_en_neer 2s infinite;\n}");
webText += F("\n</style>\n</head>\n<body>\n<div class=\"opneer\"></div>\nESP will reboot<br>Close this window");
webText += F("\n</body>\n</html>");
server.send(200, "text/html", webText);
delay(500);
ESP.restart();
}