#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <Adafruit_FT6206.h>
#include <math.h>
// TFT Display Pins
#define TFT_CS 10
#define TFT_RST 4
#define TFT_DC 9
// Screen dimensions
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240
// UI Constants
#define BATTERY_X 300
#define BATTERY_Y 10
#define BATTERY_WIDTH 18
#define BATTERY_HEIGHT 8
#define DATE_X 10
#define DATE_Y 10
#define CO2_LABEL_X 50
#define CO2_LABEL_Y 35
#define CO2_BOX_X 25
#define CO2_BOX_Y 60
#define CO2_BOX_WIDTH 280
#define CO2_BOX_HEIGHT 100
#define CO2_BOX_RADIUS 20
#define CO2_VALUE_X 80
#define CO2_VALUE_Y 95
#define CO2_UNIT_X 200
#define CO2_UNIT_Y 110
#define TEMP_BOX_X 25
#define TEMP_BOX_Y 170
#define TEMP_BOX_WIDTH 70
#define TEMP_BOX_HEIGHT 30
#define TEMP_BOX_RADIUS 10
#define TEMP_VALUE_X 40
#define TEMP_VALUE_Y 180
#define TEMP_LABEL_X 35
#define TEMP_LABEL_Y 205
#define HUM_BOX_X 235
#define HUM_BOX_Y 170
#define HUM_BOX_WIDTH 70
#define HUM_BOX_HEIGHT 30
#define HUM_BOX_RADIUS 10
#define HUM_VALUE_X 250
#define HUM_VALUE_Y 180
#define HUM_LABEL_X 250
#define HUM_LABEL_Y 205
// Color thresholds for CO2
#define CO2_GOOD_THRESHOLD 500 // Green if below this value
#define CO2_WARNING_THRESHOLD 2000 // Orange if below, Red if above
#define CO2_MAX_VALUE 3000 // Maximum value for color gradient
// Timing constants
#define CO2_UPDATE_INTERVAL 2000 // Update CO2 every 2 seconds
#define LOADING_ANIMATION_TIME 2000 // Loading animation duration
#define TEXT_ANIMATION_DELAY 50 // Delay between characters
#define SWIPE_DEBOUNCE_TIME 500 // Debounce time after swipe
#define SWIPE_THRESHOLD 50 // Pixel threshold to detect swipe
// Create TFT and Touch objects
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
Adafruit_FT6206 ts = Adafruit_FT6206();
// Screen States
enum ScreenState {
INIT_SCREEN,
MAIN_SCREEN,
HISTORY_SCREEN // Added for swipe functionality
};
ScreenState currentScreen = INIT_SCREEN; // Start with Initialization Screen
// CO2, Temp, and Humidity Values
uint16_t co2Color = ILI9341_GREEN;
uint16_t prevCo2Color = ILI9341_GREEN;
int co2Value = 0;
int prevCo2Value = -1; // Initialize with invalid value to force first update
int tempValue = 28;
int humidityValue = 68;
// Timing variables
unsigned long prevCo2UpdateMillis = 0;
unsigned long lastSwipeTime = 0;
// Touch tracking
static int touchStartY = -1;
static unsigned long touchStartTime = 0;
bool touchActive = false;
// Function to interpolate between two colors based on a value
uint16_t interpolateColor(uint16_t color1, uint16_t color2, float ratio) {
// Extract RGB components from 16-bit colors (ILI9341 uses 5-6-5 RGB format)
uint8_t r1 = ((color1 >> 11) & 0x1F) << 3; // Convert 5-bit to 8-bit
uint8_t g1 = ((color1 >> 5) & 0x3F) << 2; // Convert 6-bit to 8-bit
uint8_t b1 = ((color1 & 0x1F) << 3); // Convert 5-bit to 8-bit
uint8_t r2 = ((color2 >> 11) & 0x1F) << 3;
uint8_t g2 = ((color2 >> 5) & 0x3F) << 2;
uint8_t b2 = ((color2 & 0x1F) << 3);
// Interpolate in 8-bit color space
uint8_t r = r1 + ratio * (r2 - r1);
uint8_t g = g1 + ratio * (g2 - g1);
uint8_t b = b1 + ratio * (b2 - b1);
// Convert back to 5-6-5 RGB format
return tft.color565(r, g, b);
}
// Function to get color based on CO2 value
uint16_t getCO2Color(int co2Value) {
if (co2Value < CO2_GOOD_THRESHOLD) {
// Below good threshold - solid green
return ILI9341_GREEN;
} else if (co2Value < CO2_WARNING_THRESHOLD) {
// Between good and warning - gradient from green to orange
float ratio = (float)(co2Value - CO2_GOOD_THRESHOLD) /
(CO2_WARNING_THRESHOLD - CO2_GOOD_THRESHOLD);
return interpolateColor(ILI9341_GREEN, ILI9341_ORANGE, ratio);
} else {
// Above warning threshold - gradient from orange to red
// Cap at CO2_MAX_VALUE to prevent ratio > 1.0
int maxValue = CO2_MAX_VALUE;
if (co2Value >= maxValue) {
return ILI9341_RED; // Solid red for very high values
}
float ratio = (float)(co2Value - CO2_WARNING_THRESHOLD) /
(maxValue - CO2_WARNING_THRESHOLD);
return interpolateColor(ILI9341_ORANGE, ILI9341_RED, ratio);
}
}
// Function prototypes
void drawAnimatedText();
void drawSmoothLoadingAnimation();
void processTouch();
void handleSwipeUp();
void handleSwipeDown();
void drawMainScreen();
void drawUI();
void updateCO2Value(bool forceUpdate = false);
void drawHistoryScreen();
void setup() {
Serial.begin(115200);
// Initialize the display
tft.begin();
tft.setRotation(3);
tft.fillScreen(ILI9341_BLACK);
// Initialize touchscreen with retry
int touchRetries = 0;
while (!ts.begin(40) && touchRetries < 3) { // Try 3 times with sensitivity threshold 40
Serial.println("Touchscreen not found! Retrying...");
delay(500);
touchRetries++;
}
if (touchRetries >= 3) {
Serial.println("WARNING: Touchscreen not initialized. Continuing without touch.");
// Continue anyway, just without touch functionality
} else {
Serial.println("Capacitive Touchscreen Initialized.");
}
// Display animated text
drawAnimatedText();
// Display rotating ring loading animation
drawSmoothLoadingAnimation();
// Transition to the main screen
currentScreen = MAIN_SCREEN;
drawMainScreen();
prevCo2UpdateMillis = millis();
}
void loop() {
unsigned long currentMillis = millis();
// Process touch events
processTouch();
// Update CO2 value at regular intervals
if (currentMillis - prevCo2UpdateMillis >= CO2_UPDATE_INTERVAL) {
if (currentScreen == MAIN_SCREEN) {
updateCO2Value();
}
prevCo2UpdateMillis = currentMillis;
}
}
// ------------------- INITIALIZATION SCREEN -------------------
// Function to display "Thennal Air" and "Filters" letter-by-letter
void drawAnimatedText() {
tft.fillScreen(ILI9341_BLACK);
String line1 = "Thennal Air";
String line2 = "Filters";
int x1 = 80, y1 = 80;
int x2 = 110, y2 = 140;
uint16_t colors[] = { ILI9341_ORANGE, ILI9341_WHITE, ILI9341_GREEN };
// Print "Thennal Air"
tft.setTextSize(3);
for (int i = 0; i < line1.length(); i++) {
tft.setTextColor(colors[i % 3]);
tft.setCursor(x1, y1);
tft.print(line1[i]);
x1 += 18;
delay(TEXT_ANIMATION_DELAY);
}
// Print "Filters"
for (int i = 0; i < line2.length(); i++) {
tft.setTextColor(colors[i % 3]);
tft.setCursor(x2, y2);
tft.print(line2[i]);
x2 += 18;
delay(TEXT_ANIMATION_DELAY);
}
delay(TEXT_ANIMATION_DELAY * 10); // Slightly longer pause after text
}
// Function to draw a continuously rotating ring animation
void drawSmoothLoadingAnimation() {
tft.fillScreen(ILI9341_BLACK);
tft.setTextColor(ILI9341_WHITE);
tft.setTextSize(2);
tft.setCursor(50, 100);
tft.print("Initializing Setup...");
const int cx = 160, cy = 180, radius = 5;
const int totalDots = 12;
const int ringRadius = 30;
static float prevX[12], prevY[12]; // Static to save stack space
int frame = 0;
unsigned long startTime = millis();
while (millis() - startTime < LOADING_ANIMATION_TIME) { // Run for specified time
for (int i = 0; i < totalDots; i++) {
float angle = radians(frame + (i * 360 / totalDots));
int x = cx + cos(angle) * ringRadius;
int y = cy + sin(angle) * ringRadius;
int brightness = 255 - (i * 20);
uint16_t color = tft.color565(brightness, brightness, brightness);
if (frame > 0) {
tft.fillCircle(prevX[i], prevY[i], radius, ILI9341_BLACK);
}
tft.fillCircle(x, y, radius, color);
prevX[i] = x;
prevY[i] = y;
}
frame += 20;
if (frame >= 360) frame = 0;
delay(50);
}
}
// ------------------- TOUCH PROCESSING -------------------
void processTouch() {
unsigned long currentMillis = millis();
if (ts.touched()) {
TS_Point p = ts.getPoint(); // Get touch point
// Adjust rotation mapping (depends on display orientation)
int screenX = map(p.y, SCREEN_HEIGHT, 0, 0, SCREEN_HEIGHT);
int screenY = p.x;
if (!touchActive) {
// This is a new touch
touchStartY = screenY;
touchStartTime = currentMillis;
touchActive = true;
Serial.println("Touch started");
} else {
// Continuing touch - check for gestures
int deltaY = screenY - touchStartY;
// Only process swipes if enough time has passed since last swipe
if (currentMillis - lastSwipeTime > SWIPE_DEBOUNCE_TIME) {
if (deltaY > SWIPE_THRESHOLD) { // Swipe Down
handleSwipeDown();
lastSwipeTime = currentMillis;
touchActive = false;
} else if (deltaY < -SWIPE_THRESHOLD) { // Swipe Up
handleSwipeUp();
lastSwipeTime = currentMillis;
touchActive = false;
}
}
}
} else {
// No touch detected
if (touchActive) {
touchActive = false;
Serial.println("Touch released");
}
}
}
void handleSwipeUp() {
Serial.println("Swipe Up Detected");
// Switch screens based on swipe up
if (currentScreen == MAIN_SCREEN) {
currentScreen = HISTORY_SCREEN;
drawHistoryScreen();
}
}
void handleSwipeDown() {
Serial.println("Swipe Down Detected");
// Switch screens based on swipe down
if (currentScreen == HISTORY_SCREEN) {
currentScreen = MAIN_SCREEN;
drawMainScreen();
}
}
// ------------------- MAIN SCREEN FUNCTIONS -------------------
void drawMainScreen() {
tft.fillScreen(ILI9341_BLACK); // Clear the screen with black
drawUI();
updateCO2Value(true); // Force redraw of CO2 value
}
void drawUI() {
// Draw UI elements - Static parts
tft.setTextColor(ILI9341_WHITE);
tft.setTextSize(1);
tft.setCursor(DATE_X, DATE_Y);
tft.print("25/03/2025 Mon");
// Battery Icon
tft.drawRect(BATTERY_X, BATTERY_Y, BATTERY_WIDTH, BATTERY_HEIGHT + 1, ILI9341_WHITE);
tft.fillRect(BATTERY_X + BATTERY_WIDTH, BATTERY_Y + 2, 3, 4, ILI9341_WHITE);
tft.fillRect(BATTERY_X + 2, BATTERY_Y + 2, 12, 4, ILI9341_WHITE);
tft.setCursor(280, 10);
tft.print("85%"); // Dummy battery percentage
// CO2 Label
tft.setTextSize(3);
tft.setCursor(CO2_LABEL_X, CO2_LABEL_Y);
tft.print("CO2");
// Temperature & Humidity
tft.setTextSize(2);
tft.setCursor(TEMP_VALUE_X, TEMP_VALUE_Y);
tft.print(tempValue);
tft.print("C");
tft.setCursor(TEMP_LABEL_X, TEMP_LABEL_Y);
tft.print("Temp");
tft.setCursor(HUM_VALUE_X, HUM_VALUE_Y);
tft.print(humidityValue);
tft.print("%");
tft.setCursor(HUM_LABEL_X, HUM_LABEL_Y);
tft.print("HUM");
// Larger Rounded Rectangle for CO2 Value
tft.drawRoundRect(CO2_BOX_X, CO2_BOX_Y, CO2_BOX_WIDTH, CO2_BOX_HEIGHT, CO2_BOX_RADIUS, ILI9341_WHITE);
// Smaller Rounded Rectangles for Temp and Humidity
tft.drawRoundRect(TEMP_BOX_X, TEMP_BOX_Y, TEMP_BOX_WIDTH, TEMP_BOX_HEIGHT, TEMP_BOX_RADIUS, ILI9341_WHITE);
tft.drawRoundRect(HUM_BOX_X, HUM_BOX_Y, HUM_BOX_WIDTH, HUM_BOX_HEIGHT, HUM_BOX_RADIUS, ILI9341_WHITE);
}
void updateCO2Value(bool forceUpdate) {
// Simulate CO2 sensor by getting a random value
co2Value = random(300, 2500);
// Debug to serial monitor
Serial.print("CO2 Value: ");
Serial.print(co2Value);
Serial.print(" Color: 0x");
Serial.println(getCO2Color(co2Value), HEX);
// Get gradient color based on CO2 level
co2Color = getCO2Color(co2Value);
// Only update the display if something changed or force update requested
if (forceUpdate || co2Color != prevCo2Color || co2Value != prevCo2Value) {
// Clear previous value by filling the rectangle
tft.fillRoundRect(CO2_BOX_X, CO2_BOX_Y, CO2_BOX_WIDTH, CO2_BOX_HEIGHT, CO2_BOX_RADIUS, co2Color);
// Display new value
tft.setTextColor(ILI9341_WHITE, co2Color);
tft.setTextSize(4);
tft.setCursor(CO2_VALUE_X, CO2_VALUE_Y);
tft.print(co2Value);
tft.setTextSize(2);
tft.setCursor(CO2_UNIT_X, CO2_UNIT_Y);
tft.print("ppm");
// Update previous values
prevCo2Color = co2Color;
prevCo2Value = co2Value;
}
}
// ------------------- HISTORY SCREEN FUNCTIONS -------------------
void drawHistoryScreen() {
tft.fillScreen(ILI9341_BLACK);
// Draw header
tft.setTextColor(ILI9341_WHITE);
tft.setTextSize(2);
tft.setCursor(50, 20);
tft.print("CO2 History (24h)");
// Draw X and Y axes
tft.drawLine(40, 40, 40, 200, ILI9341_WHITE); // Y-axis
tft.drawLine(40, 200, 280, 200, ILI9341_WHITE); // X-axis
// Draw Y-axis labels
tft.setTextSize(1);
tft.setCursor(10, 40);
tft.print("2500");
tft.setCursor(10, 80);
tft.print("2000");
tft.setCursor(10, 120);
tft.print("1500");
tft.setCursor(10, 160);
tft.print("1000");
tft.setCursor(10, 190);
tft.print("500");
// Draw X-axis time labels
tft.setCursor(40, 210);
tft.print("24h");
tft.setCursor(160, 210);
tft.print("12h");
tft.setCursor(280, 210);
tft.print("now");
// Generate sample data (in real application, this would be historical data)
int historyData[24];
for (int i = 0; i < 24; i++) {
// Generate some dummy data that looks vaguely realistic
historyData[i] = 800 + random(-200, 400) + (i % 12) * 50;
}
// Draw data points with gradient colors
for (int i = 0; i < 24; i++) {
int x = 40 + i * 10;
int value = historyData[i];
// Map CO2 values to y-coordinates
int y = map(value, 500, 2500, 200, 40);
// Get gradient color based on value
uint16_t pointColor = getCO2Color(value);
tft.fillCircle(x, y, 2, pointColor);
// Connect points with lines (using the gradient color)
if (i > 0) {
int prevX = 40 + (i-1) * 10;
int prevValue = historyData[i-1];
int prevY = map(prevValue, 500, 2500, 200, 40);
tft.drawLine(prevX, prevY, x, y, pointColor);
}
}
// Instruction text
tft.setCursor(60, 230);
tft.print("Swipe DOWN to return to main screen");
}