In this project we will be creating a wireless screen that will be capable of receiving messages from bluetooth or WiFi depending on your needs. I will be keeping the tutorial simple however I will also give links to the sources that I used which can give a more in depth explanation to each step. At the bottom I will leave the full code to each part.
Project Materials:
For Arduino:
For ESP-32:
Arduino:
The Arduino portion of this project is relatively simple as the tft screen was made for it. For the bluetooth module I will be using the HC-05 bluetooth controller. It is simple and easy to use. For controlling the output I recommend using the terminal connection from this app. I found it only works on android however the Arduino IDE can also be used.
The 2.8” tft screen is built specifically for “plug and play” on arduino. This means it can easily be fitted to an arduino without the use of jumper wires and there are easy to use libraries to run it. Elegoo provides a downloadable user manual with all the libraries and instructions for how to install them. ELEGOO 2.8 Inch Touch Screen Manual
Once you have installed all the necessary libraries and made sure it works you can move onto step 2-connecting the HC-05 BT to the Arduino.
The Arduino system uses serial communication as a way to communicate with outside devices, for example sensors. So, if we pretend that the HC-05 is a sensor we can receive input data from it the same way. On the arduino there are two special pins knows as RX and TX. These are the pins responsible for serial communication RX(receiving) and TX(transmitting).
We have to use the Arduino Mega board because the TFT screen blocks the RX/TX pins as well as the 3.3V output we need for the BT. In this tutorial I will be using RX1 and TX1 on the Arduino. Make sure when connecting the HC-05 to the Mega that you connect the RX1>>TX and the TX1>>RX.
Code
The code for the project is very simple and for an input there are two options a. Using the arduino ide with the BT serial port on your computer or b. Using an app (insert name) with a terminal connection.
Step one of the code is to insert the necessary building blocks for connecting to the LCD. This can be done by copy and pasting code from the given libraries. This will only include header code and the void setup code NOT the void loop.
This is the amount that I used:
#include <Elegoo_GFX.h> // Core graphics library
#include <Elegoo_TFTLCD.h> // Hardware-specific library
#define LCD_CS A3 // Chip Select goes to Analog 3
#define LCD_CD A2 // Command/Data goes to Analog 2
#define LCD_WR A1 // LCD Write goes to Analog 1
#define LCD_RD A0 // LCD Read goes to Analog 0
#define LCD_RESET A4 // Can alternately just connect to Arduino's reset pin
#define BLACK 0x0000
#define BLUE 0x001F
#define RED 0xF800
#define GREEN 0x07E0
#define CYAN 0x07FF
#define MAGENTA 0xF81F
#define YELLOW 0xFFE0
#define WHITE 0xFFFF
Elegoo_TFTLCD tft(LCD_CS, LCD_CD, LCD_WR, LCD_RD, LCD_RESET);
bool reading_string = false;
String txt_to_screen = ""; //initialize a string to hold my message
void setup(){
Serial.begin(9600);
Serial1.begin(9600);
// ready to start reading from term
reading_string = false;
#ifdef USE_Elegoo_SHIELD_PINOUT
Serial.println(F("Using Elegoo 2.4\" TFT Arduino Shield Pinout"));
#else
Serial.println(F("Using Elegoo 2.4\" TFT Breakout Board Pinout"));
#endif
Serial.print("TFT size is "); Serial.print(tft.width()); Serial.print("x"); Serial.println(tft.height());
tft.reset();
uint16_t identifier = tft.readID();
if(identifier == 0x9325) {
Serial.println(F("Found ILI9325 LCD driver"));
} else if(identifier == 0x9328) {
Serial.println(F("Found ILI9328 LCD driver"));
} else if(identifier == 0x4535) {
Serial.println(F("Found LGDP4535 LCD driver"));
}else if(identifier == 0x7575) {
Serial.println(F("Found HX8347G LCD driver"));
} else if(identifier == 0x9341) {
Serial.println(F("Found ILI9341 LCD driver"));
} else if(identifier == 0x8357) {
Serial.println(F("Found HX8357D LCD driver"));
} else if(identifier==0x0101)
{
identifier=0x9341;
Serial.println(F("Found 0x9341 LCD driver"));
}
else if(identifier==0x1111)
{
identifier=0x9328;
Serial.println(F("Found 0x9328 LCD driver"));
}
else {
Serial.print(F("Unknown LCD driver chip: "));
Serial.println(identifier, HEX);
Serial.println(F("If using the Elegoo 2.8\" TFT Arduino shield, the line:"));
Serial.println(F(" #define USE_Elegoo_SHIELD_PINOUT"));
Serial.println(F("should appear in the library header (Elegoo_TFT.h)."));
Serial.println(F("If using the breakout board, it should NOT be #defined!"));
Serial.println(F("Also if using the breakout, double-check that all wiring"));
Serial.println(F("matches the tutorial."));
identifier=0x9328;
}
tft.begin(identifier);
}
Step 2 is to add the global variables needed and set them. In this case there will be “reading_string” which will be a boolean initially set to false. In the void setup again setup reading_string to false for consistency. The next will be a string “txt_to_screen” which will begin empty.
bool reading_string = false;
String txt_to_screen = ""; //initialize a string to hold my message
Step 3 is to add the void loop.
There are many ways to do this and the way I used made it so that no initialization character is needed. If you are using this for a sensor I recommend using this to avoid overlapping. The other flaw in this code is that it can only output 61 characters onto the string rather than the 154 that the screen can hold.
The RX1 and TX1 pins are the Serial1 communication and signal when data is being inputted. So we can use an if statement to check whether they are receiving the data (Serial1.available). The serial port is eyther 0 for off or 1 for on so if Serial.available is greater than 0 it is active.
if(Serial1.available() > 0){
}
If Serial1 is >0 we move onto the embedded if statement that uses reading_string to clear the previous message to prevent overlap. If it is false it will clear the txt_to_screen and set reading_string to true so that way in the next iteration it will skip that step and begin to display the message.
if(Serial1.available() > 0){
//Check to if start reading string
if(!reading_string){
// if char is ready to be read and the first character in string
txt_to_screen = "";
//started reading a string
reading_string = true;
}
char char_from_tablet = Serial1.read();
txt_to_screen.concat(char_from_tablet);
delay(100);
To read the message we will use the Serial1.read command which will read the incoming character and append/concatenate the character onto the txt_to_screen string. This will NOT display the message and will only build the string.
Once the message is over Serial1.available will be 0 allowing the code to move onto the else statement (display portion). Once again the code will check reading_screen. If reading_string is true (which we set it to in the previous statement) it will move onto displaying the message. This part is customisable where you can display the string wherever you want, how big, and what color. Here are the basic settings I used. The print command is simple. It calls the tft library’s print command using tft.println(). After this we set reading_string to false to allow the code to reset and prepare for the next message.
}else{
if(reading_string){
//no more characters to be read
//erase screen and add new text
tft.fillScreen(WHITE);
unsigned long start = micros();
tft.setCursor(5, 0);
tft.setTextColor(BLACK);
tft.setTextSize(3);
Serial.print(txt_to_screen);
tft.println(txt_to_screen);
//ready to start reading the nxt string
reading_string = false;
}
}
ESP-32s:
Overview:
The reason to use an ESP-32s is because of its built-in WiFi chip. Being able to communicate over WiFi allows us to control the output of the screen from anywhere connected to the internet. In order to connect to the ESP-32, however, we must use a server as a “middleman.” Unlike the ESP-32 the server is accessible from everywhere. The ESP-32 will use HTTP get requests to ping the server and obtain the data.
Step 1, Wiring
To use the LCD screen with the ESP-32 is a significantly different process than with the Arduino. The ESP-32 is a completely different chip with a completely different pinout. The ESP-32 uses General Purpose Input Output(GPIO) pins and figuring out which ones work with the screen is the challenge. Thankfully someone online has already figured out how to do it and created libraries to run it. They used ESP-32Wroom and I am using the NodeMCU ESP-32s. Here are the pinouts that work for the 32s.
Here is the pinout that is used to connect the ESP-32s to the LCD screen and the diagrams above to help:
ESP-32 Diagram | ESP-32s(Board labeling) | LCD |
5V0 | 5V | 5V |
GND | GND | GND |
GPIO13 | P13 | LCD_RD |
GPIO12 | P12 | LCD_WR |
GPIO14 | P14 | LCD_RS |
GPIO27 | P27 | LCD_CS |
GPIO26 | P26 | |
OTHER | SIDE OF | MODULE |
GPIO4 | P4 | LCD_D1 |
GPIO16 | P16 | LCD_D0 |
GPIO17 | P17 | LCD_D7 |
GPIO18 | P18 | LCD_D6 |
GPIO19 | P19 | LCD_D5 |
GPIO21 | P21 | LCD_D4 |
GPIO22 | P22 | LCD_D3 |
GPIO23 | P23 | LCD_D2 |
The link for the library tutorial and the ESP-32-WROOM-32 wiring tutorial are the same. Scroll down to just past figure 7 for the library tutorial. The Arduino IDE has the library and all you need to do is download it. It is called “TFT_eSPI” by Bodmer. This library is VERY important, without it nothing will display. I recommend duplicating the User_setup file then changing it and replacing the old one. I had some issues with it not actually changing when I would use the other code.
Step 2, Programming
To understand how this works better, building the server is the best way to start. There are many different server building platforms like DigitalOcean which allow you to create any server you want. The only drawback is that they can be pricey and complex, requiring coding html and PHP scripts. If you would like to do that there is a great website which outlines how to do that with the ESP-32. The server I will be using will be Anvil.works. This is an amazing free platform that gives all the tools necessary and the code is in python.
Before continuing on this tutorial you need to create an account and start your first server. I urge you to follow their opening tutorial on how Anvil works. Here in the link to sign up and to take their tutorial: https://anvil.works/learn/tutorials/feedback-form
REMEMBER TO CHANGE THE NAME OF YOUR WEBSITE WHEN YOU PUBLISH IT.
Server Step 1:
I recommend starting with the user interface first. Here is what mine looks like. I just have a simple text box and a button to submit the text. If you follow their tutorial this will be an easy task.
Server Step 2:
Add the function send_button_click to form 1 veins steps outlined in the tutorial. The send button will save the text that was in the text box to a variable. Because the length of the message can be no more than 154 characters long I put an if statement making sure that the message can’t be more than 150 characters long. I also added a clear output function to clear the text. Here is the code for the button:
def send_button_click(self, **event_args):
text_to_screen = self.input_box.text
if len(text_to_screen)>150:
Notification("Too Long Must Be Under 155 characters").show()
else:
anvil.server.call('input_to_table', text_to_screen)
Notification("Sent!").show()
# Call your 'clear_inputs' method to clear the boxes
self.clear_inputs()
def clear_inputs(self):
# Clear our three text boxes
self.input_box.text = ""
Server Step 3:
For the third step I created the database which will store the message to be sent out. I called this Input_Table and I added to column: text_to_screen and created. text _to_screen stores the message and uses the builtin date and time function to record when the message was made.
Server Step 4:
The last step for the server is to make the code that will send the message that is in the table to the ESP-32. This requires two functions that will be called. First is input_to_tabel and the other is input_to_screen.
input_to_table:
This function takes the text_to_screen variable made by clicking the button and using it as an argument for the function. This function does two simple tasks. First it sets the tables in the Database to their assigned variables. Second it sends an email confirming that the button was pressed and what the message is. I recommend setting this up to help keep track of the
Message.
@anvil.server.callable
def input_to_table(text_to_screen):
app_tables.input_table.add_row(
text_to_screen = text_to_screen,
created=datetime.now()
)
# Send yourself an email each time input is submitted
anvil.email.send(to="thomas@ferrell.com", # Change this to your email address!
subject=f"New Input Received",
text=f"""The input to the screen was: {text_to_screen} """)
Input_to_screen:
This function is the one that sends the message to the ESP-32 when it receive the get request. To initiate the send you have to put @anvil.server.http_endpoint("/users") above the function to let Anvil know that whatever that function returns is the output. This will also give you the link that is used for the ESP-32 side of the get request. To learn more about it Anvil has an awesome in depth tutorial on how to really utilize this feature. The rest of the code is very simple. I used a for loop to get the message from the database and return that text. I also delete the row because if I didn’t it would get confused and crash.
@anvil.server.http_endpoint("/users")
def input_to_screen():
# get row from table
for row in app_tables.input_table.search():
text_to_send = row['text_to_screen']
row.delete()
return(text_to_send)
ESP-32s Code
Programming the ESP-32 is relatively simple. We have to first connect the ESP-32 to the local WiFi network. Then we use a http get request to obtain the string. Once that is finished we can use the libraries mentioned earlier to display the message to the screen.
Connecting the ESP-32 to WiFi
There are many more in depth ways to do this depending on what you want. For what we want. Using WiFi can be very complex and explaining how it works would take a while. Here is a link that describes it well: https://www.upesy.com/blogs/tutorials/how-to-connect-wifi-acces-point-with-esp32
I used code modified from the website linked above. This code uses the network name and password to connect to WiFi. If your network does not have a password use the word null instead of a password. If your WiFi password is null, change it.
First import these libraries into your code:
#include <TFT_eSPI.h> // Graphics and font library for ILI9341 driver chip
#include <SPI.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <Arduino_JSON.h>
The ARDUINO_JSON.h library also must be installed thankfully is already in the Arduino program. It is called Arduino_Json and you will recognise it because it says “beta.”
Set up the WiFi name and password with these lines:
const char* ssid = "";
const char* password = "";
Add these variables to be used later in the code:
unsigned long last_time = 0;
unsigned long timer_delay = 10000;
String json_array;
String display_string;
String payload;
bool string_updated = true;
TFT_eSPI tft = TFT_eSPI(); // Invoke library
Here is the void setup for connecting to the WiFi. in the void setup I also initialized the tft library and set the screen rotation to horizontal.
void setup(void) {
Serial.begin(115200);
WiFi.begin(ssid, password);
Serial.println("Connecting to WIFI...");
while(WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.print("IP Address: ");
Serial.println(WiFi.localIP());
tft.init();
tft.setRotation(1);
}
Once the WiFi is set up we can move onto the code to receive and print the string. We will first create a function that will be run in the void loop to help with organization.
Get_new_string function:
At the top of the code we run the HTTPClient to tell the code that the ESP-32 will running as a client to receive information.
HTTPClient http;
Then we begin the http request by connecting to the server we previously made. And then make the get request for the string.
http.begin("Your Website Name"); //Specify the URL
int httpCode = http.GET();
Then we move onto an if statement that will help us know whether we are getting anything. Similar to how with the Bluetooth we asked if the port was open we asked if we were still connected to the server. The httpCode always returns as 200 when the GET request is active. On top of that if the database is empty the get request will return a “null.” This comes up later.
To get the string from the get request we set the variable payload to the string. If there is no new message, the payload will be set to “null” as that is what was received.
if (httpCode > 0) { //Check for the returning code
payload = http.getString();
Serial.println(httpCode);
Serial.println(payload);
}
else {
Serial.println("Error on HTTP request");
}
Next we move on to set the variables that will be used to display the message. Another if statement is needed here. Because even if the GET request is “null” that still becomes the string that the deceive will display and will replace the current message with “null.” to avoid this we check to see if the message is “null” and if it is we print “No incoming Message.” If the message is not null we can move on to display the real message.
First we set display_string to pauload and then set string_updated to true. This will come up later.
if (payload == "null"){
Serial.println("No incoming message");
}else{
display_string = payload;
string_updated = true;
delay(1000);
}
delay(5000);
}
Void Loop:
The final stage is the void loop where the message that we received from the get command will be displayed on the screen. To prevent flickering we use an if statement to verify that the string received is a new string. The next part is almost identical to the bluetooth display and customisable to your needs. Here are the settings I used: don't forget that to control the tft screen you have to initialize the library by starting the line with tft. At the end we set string_updated to false to prevent the flicker.
// put your main code here, to run repeatedly:
get_new_string();
if (string_updated){
tft.fillScreen(TFT_WHITE);
tft.setCursor(2, 0, 2);
tft.setTextColor(TFT_BLACK); tft.setTextSize(2);
tft.println(display_string);
string_updated = false;
}
}
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Here is the full code I used:
FULL ARDUINO CODE:
#include <Elegoo_GFX.h> // Core graphics library
#include <Elegoo_TFTLCD.h> // Hardware-specific library
#define LCD_CS A3 // Chip Select goes to Analog 3
#define LCD_CD A2 // Command/Data goes to Analog 2
#define LCD_WR A1 // LCD Write goes to Analog 1
#define LCD_RD A0 // LCD Read goes to Analog 0
#define LCD_RESET A4 // Can alternately just connect to Arduino's reset pin
#define BLACK 0x0000
#define BLUE 0x001F
#define RED 0xF800
#define GREEN 0x07E0
#define CYAN 0x07FF
#define MAGENTA 0xF81F
#define YELLOW 0xFFE0
#define WHITE 0xFFFF
Elegoo_TFTLCD tft(LCD_CS, LCD_CD, LCD_WR, LCD_RD, LCD_RESET);
bool reading_string = false;
String txt_to_screen = ""; //initialize a string to hold my message
void setup(){
Serial.begin(9600);
Serial1.begin(9600);
// ready to start reading from term
reading_string = false;
#ifdef USE_Elegoo_SHIELD_PINOUT
Serial.println(F("Using Elegoo 2.4\" TFT Arduino Shield Pinout"));
#else
Serial.println(F("Using Elegoo 2.4\" TFT Breakout Board Pinout"));
#endif
Serial.print("TFT size is "); Serial.print(tft.width()); Serial.print("x"); Serial.println(tft.height());
tft.reset();
uint16_t identifier = tft.readID();
if(identifier == 0x9325) {
Serial.println(F("Found ILI9325 LCD driver"));
} else if(identifier == 0x9328) {
Serial.println(F("Found ILI9328 LCD driver"));
} else if(identifier == 0x4535) {
Serial.println(F("Found LGDP4535 LCD driver"));
}else if(identifier == 0x7575) {
Serial.println(F("Found HX8347G LCD driver"));
} else if(identifier == 0x9341) {
Serial.println(F("Found ILI9341 LCD driver"));
} else if(identifier == 0x8357) {
Serial.println(F("Found HX8357D LCD driver"));
} else if(identifier==0x0101)
{
identifier=0x9341;
Serial.println(F("Found 0x9341 LCD driver"));
}
else if(identifier==0x1111)
{
identifier=0x9328;
Serial.println(F("Found 0x9328 LCD driver"));
}
else {
Serial.print(F("Unknown LCD driver chip: "));
Serial.println(identifier, HEX);
Serial.println(F("If using the Elegoo 2.8\" TFT Arduino shield, the line:"));
Serial.println(F(" #define USE_Elegoo_SHIELD_PINOUT"));
Serial.println(F("should appear in the library header (Elegoo_TFT.h)."));
Serial.println(F("If using the breakout board, it should NOT be #defined!"));
Serial.println(F("Also if using the breakout, double-check that all wiring"));
Serial.println(F("matches the tutorial."));
identifier=0x9328;
}
tft.begin(identifier);
}
void loop(){
if(Serial1.available() > 0){
//Check to if start reading string
if(!reading_string){
// if char is ready to be read and the first character in string
txt_to_screen = "";
//started reading a string
reading_string = true;
}
char char_from_tablet = Serial1.read();
txt_to_screen.concat(char_from_tablet);
delay(100);
}else{
if(reading_string){
//no more characters to be read
//erase screen and add new text
tft.fillScreen(WHITE);
unsigned long start = micros();
tft.setCursor(5, 0);
tft.setTextColor(BLACK);
tft.setTextSize(3);
Serial.print(txt_to_screen);
tft.println(txt_to_screen);
//ready to start reading the nxt string
reading_string = false;
}
}
}
FULL SERVER CODE:
FORM1:
from ._anvil_designer import Form1Template
from anvil import *
import anvil.server
import anvil.tables as tables
import anvil.tables.query as q
from anvil.tables import app_tables
class Form1(Form1Template):
def __init__(self, **properties):
# Set Form properties and Data Bindings.
self.init_components(**properties)
# Any code you write here will run before the form opens.
def send_button_click(self, **event_args):
text_to_screen = self.input_box.text
if len(text_to_screen)>150:
Notification("Too Long Must Be Under 155 characters").show()
else:
anvil.server.call('input_to_table', text_to_screen)
Notification("Sent!").show()
# Call your 'clear_inputs' method to clear the boxes
self.clear_inputs()
def clear_inputs(self):
# Clear our three text boxes
self.input_box.text = ""
SERVERMODULE1:
import anvil.email
import anvil.tables as tables
import anvil.tables.query as q
from anvil.tables import app_tables
import anvil.server
from datetime import datetime
# This is a server module. It runs on the Anvil server,
# rather than in the user's browser.
#
# To allow anvil.server.call() to call functions here, we mark
# them with @anvil.server.callable.
# Here is an example - you can replace it with your own:
#
# @anvil.server.callable
# def say_hello(name):
# print("Hello, " + name + "!")
# return 42
#
@anvil.server.callable
def input_to_table(text_to_screen):
app_tables.input_table.add_row(
text_to_screen = text_to_screen,
created=datetime.now()
)
# Send yourself an email each time input is submitted
anvil.email.send(to="thomas@ferrell.com", # Change this to your email address!
subject=f"New Input Recieved",
text=f"""The input to the screen was: {text_to_screen} """)
@anvil.server.http_endpoint("/users")
def input_to_screen():
# get row from table
for row in app_tables.input_table.search():
text_to_send = row['text_to_screen']
row.delete()
return(text_to_send)
ESP-32 FULL CODE
#include <TFT_eSPI.h> // Graphics and font library for ILI9341 driver chip
#include <SPI.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <Arduino_JSON.h>
const char* ssid = "Guest";
const char* password = "null";
unsigned long last_time = 0;
unsigned long timer_delay = 10000;
String json_array;
String display_string;
String payload;
bool string_updated = true;
TFT_eSPI tft = TFT_eSPI(); // Invoke library
void setup(void) {
Serial.begin(115200);
WiFi.begin(ssid, null);
Serial.println("Connecting to WIFI...");
while(WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.print("IP Address: ");
Serial.println(WiFi.localIP());
tft.init();
tft.setRotation(1);
}
void get_new_string(){
HTTPClient http;
http.begin("https://flcesp-32screen.anvil.app/_/api/users"); //Specify the URL
int httpCode = http.GET();
if (httpCode > 0) { //Check for the returning code
payload = http.getString();
Serial.println(httpCode);
Serial.println(payload);
}
else {
Serial.println("Error on HTTP request");
}
if (payload == "null"){
Serial.println("No incoming message");
}else{
display_string = payload;
string_updated = true;
delay(1000);
}
delay(5000);
}
void loop() {
// put your main code here, to run repeatedly:
get_new_string();
if (string_updated){
tft.fillScreen(TFT_WHITE);
tft.setCursor(2, 0, 2);
tft.setTextColor(TFT_BLACK); tft.setTextSize(2);
tft.println(display_string);
string_updated = false;
}
}