Task 2: Individual assignment
Let’s break down this week’s assignment:
- Write an application
- that interfaces a user with an input and/or output device(s)
- on a board that you made.
As my final project is using a 2.8” TFT Display Output - this week was very crucial for me.
Design
My go-to quick application for designing an application interface would be Figma. I covered briefly in Week 2 how to use Figma. Always create a mock-up first as your guide
This is how I envisioned the dashboard for my application to look like.
I set the canvas size to 240px x 320px, the display dimension for the 2.8” TFT ILI9341 Screen.
For programming purposes, an important thing to check out is the panel on the right hand side.
This panel will tell us the properties for each component - which will help us alot when programming our design.
Now that we cover the basics, let us translate this design to the preferred programming library. Since the TFT Displays utilises mainly Adafruit_GFX Library, I would be programming the design we made in Figma manually.
Programming - Adafruit GFX Library
Check here for the complete function of the Adafruit Gfx library.
Libraries, Constants, & Variables
#include <esp_now.h>
#include <WiFi.h>
#include "SPI.h"
#include "Adafruit_GFX.h"
#include "Adafruit_ILI9341.h"
// For the Adafruit shield, these are the default.
#define TFT_RST D4
#define TFT_DC D5
#define TFT_CS D3 // SS
#define TFT_MOSI D10 // MOSI
#define TFT_MISO D9
#define TFT_CLK D8 // SCK
Adafruit_ILI9341 tft(TFT_CS, TFT_DC, TFT_MOSI, TFT_CLK, TFT_RST, TFT_MISO);
#define Channel 1 // same channel as TX
struct SensorData {
float loadV;
float currA;
float pwrW;
};
SensorData newData;
- Firstly input the necessary libraries and pin-outs
- Since this is our Rx Board - remember that the Tx and Rx board has to be in the same
channel
- Define the data type. Our data from the Current Sensor INA219 will be float data as they represent single-precision floating-point numbers, which are numbers that can have decimal points. Group them into one structure/struct called
SensorData
- Declare the incoming value of
SensorData
as the variablenewData
Setup
void setup() {
//initialize serial monitor
Serial.begin(115200);
// initialize TFT display
tft.begin();
tft.setRotation(4); // Set display orientation if needed
tft.fillScreen(ILI9341_BLACK); // Fill screen with black color
UI ();
//initialize WiFi communication
WiFi.mode(WIFI_AP); // set wifi to AP mode
WiFi.softAP("RX_1", "RX_1_Password", Channel, 0); // provide SSID that TX can recognize
esp_now_init();
esp_now_register_recv_cb(OnDataRecv);
}
In the initialize TFT Display code.
- Always start with
tft.begin()
- To set orientation, (1) & (3) are landscape, meanwhile (2) & (4) are portrait
- Next we have to create our own
UI()
function
UI()
Here is a an example how to draw with Adafruit GFX and translate our design to the interface. Let us look at our reference again.
- Firstly our rectangle is rounded. I looked into the GFX library and found the function
tft.fillRoundRect(int16_t x0, int16_t y0, int16_t w, int16_t h, int16_t radius, uint16_t color);
- Follow our reference and input the numbers which in our case is
tft.fillRoundRect(7, 7, 225, 112, 7, ILI9341_WHITE);
- Before we put text, make sure that we set the text’s position first with
tft.setCursor(int16_t x0, int16_t y0);
- Input the text we want to display with
tft.println(" ");
- Follow the same principle for other components
void UI () {
//Rectangle 1 - Current Meter
tft.fillRoundRect(7, 7, 225, 112,
7, ILI9341_WHITE);
tft.setCursor(13, 12);
tft.setTextSize (2);
tft.setTextColor(ILI9341_BLACK);
tft.println("Current");
tft.setTextSize (1);
tft.setCursor(13, 55);
tft.println("Current: ");
// Draw graph area
tft.drawRect(97, 22, 135, 85, ILI9341_BLACK);
//Rectangle 2 - Voltage
tft.fillRoundRect(7,126, 110, 105,
7, ILI9341_WHITE);
tft.setCursor(13, 128);
tft.setTextSize (2);
tft.setTextColor(ILI9341_BLACK);
tft.println("Voltage");
// Draw Gauge
drawSemiCircleGauge(62, 160, 45, 0, 180);
// tft.setCursor(43, 190);
// tft.setTextSize (1);
// tft.println("COMING");
// tft.setCursor(48, 200);
// tft.println("SOON");
//Rectangle 3 - Power
tft.fillRoundRect(122,126, 110, 105,
7, ILI9341_WHITE);
//tft.fillCircle(175, 180, 40, ILI9341_YELLOW);
tft.setCursor(128, 128);
tft.setTextSize (2);
tft.setTextColor(ILI9341_BLACK);
tft.println("Power");
// tft.setCursor(128, 146);
//tft.println("Storage");
// tft.setCursor(157, 190);
// tft.setTextSize (1);
// tft.println("COMING");
// tft.setCursor(162, 200);
// tft.println("SOON");
// Line
tft.drawFastHLine(7, 238, 225, ILI9341_WHITE);
//Clock
tft.setCursor(9, 255);
tft.setTextSize (4);
tft.setTextColor(ILI9341_WHITE);
tft.println("14:14");
//Location
tft.setCursor(13, 300);
tft.setTextSize (1);
tft.setTextColor(ILI9341_WHITE);
tft.println("Serangan, Bali");
//Menu Button
tft.fillRoundRect(138,248, 90, 25,
25, ILI9341_WHITE);
tft.setCursor(158, 255);
tft.setTextSize (2);
tft.setTextColor(ILI9341_BLACK);
tft.println("Menu");
//Help Button
tft.fillRoundRect(138,280, 90, 25,
25, ILI9341_WHITE);
tft.setCursor(158, 289);
tft.setTextSize (2);
tft.setTextColor(ILI9341_BLACK);
tft.println("Help");
}
Loop
In the loop function - we want to map the electrical values we receive from the Tx board into a graphical representation that can be easily understood.
void loop() {
Serial.print("Load Voltage: ");
Serial.println(newData.loadV); // display load voltage
Serial.print("Current: ");
Serial.println(newData.currA); // display current
Serial.print("Power: ");
Serial.println(newData.pwrW); // display power
//CURRENT BAR GRAPH
// Scale the current value; With the LED Diode as a load, I would do a *-100 multiplier, with the fan *-1
int currValScaled = (newData.currA * -1);
// Map the Value of the current data into a targeted range to be displayed (min value reading, max value reading, min target range, max target range)
int currVal = map(currValScaled, 230, 330, 0, 100);
Serial.println(currVal);
//There are alot of fill rectangle function here to cover up TFT Display's refreshing limitation.
tft.fillRect(0,40,7,50,ILI9341_BLACK);
tft.fillRect(7,40,97,45,ILI9341_WHITE);
tft.fillRect(98,40,133,50,ILI9341_WHITE);
//I couldn't figure out how to draw a sinewave - so the representation becomes a Blue bar graph instead
tft.fillRect(97, 50, currVal, 35, ILI9341_BLUE);
//Fill rectangle function here to cover up TFT Display's refreshing limitation on the Text area
tft.fillRect(30,65,60,30,ILI9341_WHITE);
//Text - current value
tft.setTextSize(1);
tft.setTextColor(ILI9341_BLACK);
tft.setCursor(13, 55);
tft.println("Current: ");
tft.setCursor(30, 65);
tft.printf("%.2f mA\n", -newData.currA);
//POWER CIRCLE GRAPH
//Fill rectangle function here to cover up TFT Display's refreshing limitation on the Power area
tft.fillCircle(175,180, 45, ILI9341_WHITE);
// Yellow Circle Graph with values
tft.fillCircle(175, 180, newData.pwrW/10, ILI9341_YELLOW);
tft.setCursor(150, 200);
tft.setTextSize(1);
tft.setTextColor(ILI9341_BLACK);
tft.printf("%.2f mW\n", newData.pwrW);
tft.setCursor(128, 128);
tft.setTextSize (2);
tft.setTextColor(ILI9341_BLACK);
tft.println("Power");
// VOLTAGE GAUGE GRAPH
drawNeedleGauge();
// // tft.printf("Load Voltage: %.2f V\n", newData.loadV);
// // // specify data receipt interval
delay(1000); // every 3 seconds
}
To draw the Voltage Gauge Graph - I asked the help of ChatGPT
void drawSemiCircleGauge(int x, int y, int radius, int minAngle, int maxAngle) {
int center_x = x;
int center_y = y + radius; // Center of the semi-circle is shifted downwards
// Draw semi-circle
int startAngle = minAngle;
int endAngle = maxAngle;
int segments = 30; // Number of line segments to approximate the arc
float angleIncrement = (endAngle - startAngle) / (float)segments;
for (int i = 0; i <= segments; i++) {
float angle = startAngle + angleIncrement * i;
int x1 = center_x + cos(angle * PI / 180) * radius;
int y1 = center_y - sin(angle * PI / 180) * radius;
int x2 = center_x + cos(angle * PI / 180) * (radius - 5); // Reduce the inner radius for a clearer gauge
int y2 = center_y - sin(angle * PI / 180) * (radius - 5);
tft.drawLine(x1, y1, x2, y2, ILI9341_BLACK);
}
}
void drawNeedleGauge() {
static float previousValue = 0;
float currentValue = newData.loadV;
// Clear the previous needle
drawNeedle(62, 160 + 45, 45, previousValue, 0, 180, ILI9341_BLACK);
// Draw the new needle
drawNeedle(62, 160 + 45, 45, currentValue, 0, 180, ILI9341_RED);
// Display the voltage value
tft.setCursor(43, 210);
tft.setTextSize(1);
tft.setTextColor(ILI9341_BLACK);
tft.fillRect(43, 207, 70, 24, ILI9341_WHITE); // Clear the previous value area
tft.printf("%.2f V", currentValue);
// Save the current value as the previous value for the next update
previousValue = currentValue;
}
void drawNeedle(int x, int y, int radius, float value, int minAngle, int maxAngle, uint16_t color) {
// Map the voltage range to the angle range
float minValue = 1.0; // Minimum voltage
float maxValue = 5.0; // Maximum voltage
int angle = map(value, minValue, maxValue, maxAngle, minAngle); // Map the value within the desired range
int needleLength = radius * 0.8;
int x_end = x + cos(angle * PI / 180) * needleLength;
int y_end = y - sin(angle * PI / 180) * needleLength;
// Draw thicker needle by drawing multiple lines with a slight offset
for (int i = -2; i <= 2; i++) {
tft.drawLine(x + i, y, x_end + i, y_end, color);
}
}
Finalised Code
//Elaine FP
//Xiao ESP32C3 + TFT Display
//by Elaine Regina & Rico Kanthatham, Fablab Bali/Skylabworkshop, June 2024
//refactored from original code by...Ruis Santos,
//Receiver Code
//Receives data and displays it on a TFT screen
#include <esp_now.h>
#include <WiFi.h>
#include "SPI.h"
#include "Adafruit_GFX.h"
#include "Adafruit_ILI9341.h"
// For the Adafruit shield, these are the default.
#define TFT_RST D4
#define TFT_DC D5
#define TFT_CS D3 // SS
#define TFT_MOSI D10 // MOSI
#define TFT_MISO D9
#define TFT_CLK D8 // SCK
Adafruit_ILI9341 tft(TFT_CS, TFT_DC, TFT_MOSI, TFT_CLK, TFT_RST, TFT_MISO);
#define Channel 1 // same channel as TX
struct SensorData {
float loadV;
float currA;
float pwrW;
};
SensorData newData;
void setup() {
//initialize serial monitor
Serial.begin(115200);
// initialize TFT display
tft.begin();
tft.setRotation(4); // Set display orientation if needed
tft.fillScreen(ILI9341_BLACK); // Fill screen with black color
UI ();
//initialize WiFi communication
WiFi.mode(WIFI_AP); // set wifi to AP mode
WiFi.softAP("RX_1", "RX_1_Password", Channel, 0); // provide SSID that TX can recognize
esp_now_init();
esp_now_register_recv_cb(OnDataRecv);
}
void loop() {
Serial.print("Load Voltage: ");
Serial.println(newData.loadV); // display load voltage
Serial.print("Current: ");
Serial.println(newData.currA); // display current
Serial.print("Power: ");
Serial.println(newData.pwrW); // display power
//current bar graph
int currValScaled = (newData.currA * -1); // Scale the current value;
int currVal = map(currValScaled, 230, 330, 0, 100);
Serial.println(currVal);
tft.fillRect(0,40,7,50,ILI9341_BLACK);
tft.fillRect(7,40,97,45,ILI9341_WHITE);
tft.fillRect(98,40,133,50,ILI9341_WHITE);
tft.fillRect(97, 50, currVal, 35, ILI9341_BLUE);
tft.fillRect(30,65,60,30,ILI9341_WHITE);
tft.setTextSize(1);
tft.setTextColor(ILI9341_BLACK);
tft.setCursor(13, 55);
tft.println("Current: ");
tft.setCursor(30, 65);
tft.printf("%.2f mA\n", -newData.currA);
//Power circle graph
tft.fillCircle(175,180, 45, ILI9341_WHITE);
tft.fillCircle(175, 180, newData.pwrW/10, ILI9341_YELLOW);
tft.setCursor(150, 200);
tft.setTextSize(1);
tft.setTextColor(ILI9341_BLACK);
tft.printf("%.2f mW\n", newData.pwrW);
tft.setCursor(128, 128);
tft.setTextSize (2);
tft.setTextColor(ILI9341_BLACK);
tft.println("Power");
// VOLTAGE GAUGE GRAPH
drawNeedleGauge();
// // tft.printf("Load Voltage: %.2f V\n", newData.loadV);
// // // specify data receipt interval
delay(1000); // every 3 seconds
void OnDataRecv(const uint8_t *mac_addr, const uint8_t *data, int data_len){
//Serial.print("I just received >> ");
// Serial.println(*sensorData);
memcpy(&newData, data, sizeof(newData)); //copy data from membory (memory copy) for use in loop
}
}
void OnDataRecv(const uint8_t *mac_addr, const uint8_t *data, int data_len){
//Serial.print("I just received >> ");
// Serial.println(*sensorData);
memcpy(&newData, data, sizeof(newData)); //copy data from membory (memory copy) for use in loop
}
void UI () {
//Rectangle 1 - Current Meter
tft.fillRoundRect(7, 7, 225, 112,
7, ILI9341_WHITE);
tft.setCursor(13, 12);
tft.setTextSize (2);
tft.setTextColor(ILI9341_BLACK);
tft.println("Current");
tft.setTextSize (1);
tft.setCursor(13, 55);
tft.println("Current: ");
// Draw graph area
tft.drawRect(97, 22, 135, 85, ILI9341_BLACK);
//Rectangle 2 - Voltage
tft.fillRoundRect(7,126, 110, 105,
7, ILI9341_WHITE);
tft.setCursor(13, 128);
tft.setTextSize (2);
tft.setTextColor(ILI9341_BLACK);
tft.println("Voltage");
// Draw Gauge
drawSemiCircleGauge(62, 160, 45, 0, 180);
// tft.setCursor(43, 190);
// tft.setTextSize (1);
// tft.println("COMING");
// tft.setCursor(48, 200);
// tft.println("SOON");
//Rectangle 3 - Hydrogen Storage
tft.fillRoundRect(122,126, 110, 105,
7, ILI9341_WHITE);
//tft.fillCircle(175, 180, 40, ILI9341_YELLOW);
tft.setCursor(128, 128);
tft.setTextSize (2);
tft.setTextColor(ILI9341_BLACK);
tft.println("Power");
// tft.setCursor(128, 146);
//tft.println("Storage");
// tft.setCursor(157, 190);
// tft.setTextSize (1);
// tft.println("COMING");
// tft.setCursor(162, 200);
// tft.println("SOON");
// Line
tft.drawFastHLine(7, 238, 225, ILI9341_WHITE);
//Clock
tft.setCursor(9, 255);
tft.setTextSize (4);
tft.setTextColor(ILI9341_WHITE);
tft.println("14:14");
//Location
tft.setCursor(13, 300);
tft.setTextSize (1);
tft.setTextColor(ILI9341_WHITE);
tft.println("Serangan, Bali");
//Menu
tft.fillRoundRect(138,248, 90, 25,
25, ILI9341_WHITE);
tft.setCursor(158, 255);
tft.setTextSize (2);
tft.setTextColor(ILI9341_BLACK);
tft.println("Menu");
//Help
tft.fillRoundRect(138,280, 90, 25,
25, ILI9341_WHITE);
tft.setCursor(158, 289);
tft.setTextSize (2);
tft.setTextColor(ILI9341_BLACK);
tft.println("Help");
}
void drawSemiCircleGauge(int x, int y, int radius, int minAngle, int maxAngle) {
int center_x = x;
int center_y = y + radius; // Center of the semi-circle is shifted downwards
// Draw semi-circle
int startAngle = minAngle;
int endAngle = maxAngle;
int segments = 30; // Number of line segments to approximate the arc
float angleIncrement = (endAngle - startAngle) / (float)segments;
for (int i = 0; i <= segments; i++) {
float angle = startAngle + angleIncrement * i;
int x1 = center_x + cos(angle * PI / 180) * radius;
int y1 = center_y - sin(angle * PI / 180) * radius;
int x2 = center_x + cos(angle * PI / 180) * (radius - 5); // Reduce the inner radius for a clearer gauge
int y2 = center_y - sin(angle * PI / 180) * (radius - 5);
tft.drawLine(x1, y1, x2, y2, ILI9341_BLACK);
}
}
void drawNeedleGauge() {
static float previousValue = 0;
float currentValue = newData.loadV;
// Clear the previous needle
drawNeedle(62, 160 + 45, 45, previousValue, 0, 180, ILI9341_BLACK);
// Draw the new needle
drawNeedle(62, 160 + 45, 45, currentValue, 0, 180, ILI9341_RED);
// Display the voltage value
tft.setCursor(43, 210);
tft.setTextSize(1);
tft.setTextColor(ILI9341_BLACK);
tft.fillRect(43, 207, 70, 24, ILI9341_WHITE); // Clear the previous value area
tft.printf("%.2f V", currentValue);
// Save the current value as the previous value for the next update
previousValue = currentValue;
}
void drawNeedle(int x, int y, int radius, float value, int minAngle, int maxAngle, uint16_t color) {
// Map the voltage range to the angle range
float minValue = 1.0; // Minimum voltage
float maxValue = 5.0; // Maximum voltage
int angle = map(value, minValue, maxValue, maxAngle, minAngle); // Map the value within the desired range
int needleLength = radius * 0.8;
int x_end = x + cos(angle * PI / 180) * needleLength;
int y_end = y - sin(angle * PI / 180) * needleLength;
// Draw thicker needle by drawing multiple lines with a slight offset
for (int i = -2; i <= 2; i++) {
tft.drawLine(x + i, y, x_end + i, y_end, color);
}
}
Outcome
Overall, everything is working, data transmitted into the Rx board shows values and is displayed within the graphics. However I still need to figure out mapping values with screen refresh mechanism and make the whole interface more refined. The loop function was where it all got very tricky. Remember that in the loop - the code runs in order from top to bottom. Anytime that the values were out of range and messed up the graphical mapping, I couldn’t really figure out how to make the the TFT Display / Adafruit GFX Library refresh in the unintended areas. Here is a video displaying the problem.