I'm creating a code for an ozone analyser project, which reads RS232 data, parses the data, and displays it in the form of a live graph on an LCD display. The issue I'm having is plotting at 1Hz redraws the full graph when the buffer is full, as it is a circular buffer. Once the buffer is full, the whole graph needs redrawn to move the points back 1 index in the buffer. This causes the screen to flicker when redrawing, which makes it difficult to properly view the data. Can someone help me reduce this flicker? Here is my code:
Main:
#include "libraries.h"
#include "config.h"
// Function to check and return the status as "OK" or "NOT OK"
String getStatus(const String &hexValue) {
if (hexValue == "0000") {
return "OK";
} else {
return "NOT OK";
}
}
float stringToFloat(String str) {
int spaceIndex = str.indexOf(' ');
if (spaceIndex != -1) {
str = str.substring(0, spaceIndex);
}
return str.toFloat();
}
float mapFloat(float x, float in_min, float in_max, float out_min, float out_max) {
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
String cleanValue(const String &valueStr) {
float val = valueStr.toFloat();
// Use 1 decimal place; adjust as needed
return String(val, 2);
}
// =================================================================================================
// DataPacket Class
// =================================================================================================
class DataPacket {
public:
String date;
String timeStr;
String concentration;
String pressure;
String dirtiness;
String status;
DataPacket()
: date(""),
timeStr(""),
concentration(""),
pressure(""),
dirtiness(""),
status("") {}
void parse(String packet) {
String sections[6];
int fieldCount = splitPacket(packet, sections);
if (fieldCount == 6) {
date = sections[0];
timeStr = sections[1];
concentration = sections[2];
pressure = sections[3];
dirtiness = sections[4];
status = sections[5];
} else {
Serial.println(" Error: Malformed packet. Expected 6 fields, received " + fieldCount);
}
}
int splitPacket(String packet, String sections[]) {
int fieldCount = 0;
int lastCommaIndex = -1;
while (fieldCount <= 6) { // Now properly checks for all 6 fields
int commaIndex = packet.indexOf(',', lastCommaIndex + 1);
if (commaIndex == -1) {
// If there's no more comma, the rest of the string is the last section
sections[fieldCount++] = packet.substring(lastCommaIndex + 1);
break;
}
sections[fieldCount++] = packet.substring(lastCommaIndex + 1, commaIndex);
lastCommaIndex = commaIndex;
}
return fieldCount;
}
void printParsedData() {
Serial.println("Parsed Data:");
Serial.println("Date: " + date);
Serial.println("Time: " + timeStr);
Serial.println("Concentration: " + concentration);
Serial.println("Pressure: " + pressure);
Serial.println("Dirtiness: " + dirtiness);
Serial.println("Status: " + status);
}
};
DataPacket data;
// Function to display all RS232 data
void printRS232Data(const String &date, const String &time, const String &concentration,
const String &pressure, const String &dirtiness, const String &status) {
static String lastDate = "", lastTime = "", lastConcentration = "", lastPressure = "",
lastDirtiness = "", lastStatus = "";
static bool titlesPrinted = false;
if (currentPage == 1) {
if (!titlesPrinted) {
// Print labels only once
printData(10, 50, "Date:", "", WHITE);
printData(10, 80, "Time:", "", WHITE);
printData(10, 110, "Concentration:", "", WHITE);
printData(10, 140, "Pressure:", "", WHITE);
printData(10, 170, "Dirtiness:", "", WHITE);
printData(10, 200, "Status:", "", WHITE);
titlesPrinted = true;
}
// Define value positions (adjust X to avoid overwriting label)
int valueX = 250;
if (date != lastDate) {
display.fillRect(valueX, 50, 190, 30, BLACK); // Clear only the value area
printData(valueX, 50, "", date, WHITE);
lastDate = date;
}
if (time != lastTime) {
display.fillRect(valueX, 80, 190, 30, BLACK);
printData(valueX, 80, "", time, WHITE);
lastTime = time;
}
if (concentration != lastConcentration) {
display.fillRect(valueX, 110, 190, 30, BLACK);
printData(valueX, 110, "", concentration, WHITE);
lastConcentration = concentration;
}
if (pressure != lastPressure) {
display.fillRect(valueX, 140, 190, 30, BLACK);
printData(valueX, 140, "", pressure, WHITE);
lastPressure = pressure;
}
if (dirtiness != lastDirtiness) {
display.fillRect(valueX, 170, 190, 30, BLACK);
printData(valueX, 170, "", dirtiness, WHITE);
lastDirtiness = dirtiness;
}
if (status != lastStatus) {
display.fillRect(valueX, 200, 190, 30, BLACK);
printData(valueX, 200, "", status, WHITE);
lastStatus = status;
}
} else {
titlesPrinted = false; // Reset when switching pages
}
}
class displayData {
public:
// Function to check if the "Next" button was pressed
bool nextButtonPressed(int touchX, int touchY) {
if (touchX >= nextButtonX1 && touchX <= nextButtonX2 && touchY >= nextButtonY1 && touchY <= nextButtonY2) {
return true; // "Next" button was pressed
}
return false; // "Next" button was not pressed
}
// Function to check if the "Previous" button was pressed
bool prevButtonPressed(int touchX, int touchY) {
if (touchX >= prevButtonX1 && touchX <= prevButtonX2 && touchY >= prevButtonY1 && touchY <= prevButtonY2) {
return true; // "Previous" button was pressed
}
return false; // "Previous" button was not pressed
}
void overviewPage(DataPacket &data) {
pageIsGraph = false;
String HEADER = "BMT965 ST Data"; // Text to display
display.setTextSize(2); // Set text size, adjust as needed
display.setTextColor(WHITE); // Text color
// Calculate the text bounds
int16_t textX = 200, textY = 20; // Start from x = 0, y = 10 for top
uint16_t textWidth, textHeight;
display.getTextBounds(HEADER, textX, textY, &textX, &textY, &textWidth, &textHeight);
// Calculate the x-coordinate to center the text
int centerX = (display.width() - textWidth) / 2;
// Set cursor to the centered position
display.setCursor(centerX, textY);
// Draw the text
display.print(HEADER);
// Determine if the status is OK or NOT OK
String status = getStatus(data.status);
// Set text size and color
display.setTextSize(2);
display.setTextColor(WHITE); // Text color
// Print the RS232 data in sections
printRS232Data(cleanValue(data.date), cleanValue(data.timeStr), cleanValue(data.concentration), cleanValue(data.pressure), cleanValue(data.dirtiness), status);
}
void pageSelect(DataPacket &data) {
static int previousPage = -1; // Remember the previous page
// Check if the page is a graph page and if the function hasn't been called yet
if (pageIsGraph && !isCalled) {
drawGraph();
isCalled = true;
}
if (pageIsGraph) plotGraph();
FT6336U_TouchPointType touchPoints = touchScreen.scan();
if (touchPoints.touch_count > 0) {
int touchX = touchPoints.tp[0].x;
int touchY = touchPoints.tp[0].y;
// for touch position debugging
Serial.print("x = ");
Serial.print(touchX);
Serial.print(" y = ");
Serial.println(touchY);
if (nextButtonPressed(touchX, touchY)) {
currentPage++;
if (currentPage > totalPages) currentPage = 1;
}
if (prevButtonPressed(touchX, touchY)) {
currentPage--;
if (currentPage < 1) currentPage = totalPages;
}
}
// If page changed, redraw content
if (currentPage != previousPage) {
display.fillScreen(BLACK); // Clear the screen before drawing new content
drawNav();
pageIsGraph = false;
isCalled = false;
switch (currentPage) {
case 1:
overviewPage(data);
break;
case 2:
concentrationPage(data);
break;
case 3:
pressurePage(data);
break;
case 4:
dirtinessPage(data);
break;
case 5:
statusInfoPage(data);
break;
default:
currentPage = 1;
overviewPage(data);
break;
}
dataCount = 0;
for (int i = 0; i < maxDataPoints; i++) {
dataValues[i] = 0;
}
if (pageIsGraph) {
// Clear the graph area (but not the axes or grid)
display.fillRect(graphX - 5, graphY - 5, graphWidth + 10, graphHeight + 10, BLACK);
drawGraph();
}
previousPage = currentPage;
}
display.setTextSize(1);
display.setCursor(400, 300);
display.setTextColor(WHITE);
display.print("Page ");
display.print(currentPage);
display.print("/");
display.print(totalPages);
}
void drawNav() {
display.fillRect(10, 10, 20, 20, GREY);
display.fillRect(450, 10, 20, 20, GREY);
display.drawLine(14, 20, 26, 14, WHITE);
display.drawLine(14, 20, 26, 26, WHITE);
display.drawLine(454, 14, 466, 20, WHITE);
display.drawLine(454, 26, 466, 20, WHITE);
}
void drawGraph() {
// Draw the title at the top of the graph
display.setCursor(graphX + (graphWidth / 4), graphY - 30); // Adjust title position
display.setTextColor(WHITE);
display.setTextSize(2); // Title size
// Draw the gridlines (horizontal and vertical)
for (int i = 0; i <= gridYNum; i++) {
int yPosition = graphY + i * (graphHeight / gridYNum); // Y position for horizontal gridlines
display.drawLine(graphX, yPosition, graphX + graphWidth, yPosition, GREY);
}
for (int i = 0; i <= gridXNum; i++) {
int xPosition = graphX + i * (graphWidth / gridXNum); // X position for vertical gridlines
display.drawLine(xPosition, graphY, xPosition, graphY + graphHeight, GREY);
}
// Thick X axis (bottom)
for (int i = 0; i < axisThickness; i++) {
display.drawFastHLine(graphX, graphY + graphHeight + i, graphWidth, WHITE);
}
// Thick Y axis (left)
for (int i = 0; i < axisThickness; i++) {
display.drawFastVLine(graphX + i, graphY, graphHeight, WHITE);
}
display.setCursor(220, 290);
display.print("Time");
// Draw Y-axis labels (min and max)
display.setTextSize(1); // Smaller text for axis labels
display.setTextColor(WHITE);
// Max Y value (top-left of graph)
display.setCursor(graphX - 30, graphY - 5); // Adjust -30 if needed for width
display.print(maxY);
// Min Y value (bottom-left of graph)
display.setCursor(graphX - 30, graphY + graphHeight - 5); // Same x, lower y
display.print("0");
}
void plotGraph() {
int spacing = 6; // Spacing between each data point (40px)
// Redraw the updated graph title
display.setCursor(graphX + (graphWidth / 4), graphY - 30);
display.setTextColor(WHITE);
display.setTextSize(2);
// Check if it's time to add a new data point (every 1 second)
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
// Clear the title area (adjust coordinates and size as needed)
display.fillRect(graphX + (graphWidth / 4), graphY - 40, 250, 30, BLACK);
display.print(graphTitle);
// Shift all data points to the left
if (dataCount >= maxDataPoints) {
display.fillRect(graphX - 5, graphY - 5, graphWidth + 10, graphHeight + 10, BLACK);
drawGraph();
for (int i = 0; i < maxDataPoints - 1; i++) {
dataValues[i] = dataValues[i + 1];
}
dataValues[maxDataPoints - 1] = plotValue;
} else {
// If we haven't filled the array yet, just add the new value
dataValues[dataCount] = plotValue;
dataCount++;
}
}
// Plot all data points and connect them with lines
for (int i = 0; i < dataCount; i++) {
if (i == 0 && dataValues[i] == 0) continue;
int x = graphX + i * spacing;
int y = (int)mapFloat(dataValues[i], 0.0, (float)maxY, graphY + graphHeight, graphY);
display.fillCircle(x, y, 1, RED);
if (i > 0) {
int prevX = graphX + (i - 1) * spacing;
int prevY = (int)mapFloat(dataValues[i - 1], 0.0, (float)maxY, graphY + graphHeight, graphY);
display.drawLine(prevX, prevY, x, y, WHITE);
}
}
}
void concentrationPage(DataPacket &data) {
pageIsGraph = true;
// Parse the concentration value from the data packet
float graphConcentrationValue = stringToFloat(data.concentration);
graphTitle = "Ozone: " + cleanValue(data.concentration) + "g/Nm3";
plotValue = graphConcentrationValue;
maxY = 150;
}
void pressurePage(DataPacket &data) {
pageIsGraph = true;
// Parse the concentration value from the data packet
float graphPressureValue = stringToFloat(data.pressure);
graphTitle = "Pressure: " + cleanValue(data.pressure) + " bar";
plotValue = graphPressureValue;
maxY = 4;
}
void dirtinessPage(DataPacket &data) {
pageIsGraph = true;
// Parse the concentration value from the data packet
float graphDirtinessValue = stringToFloat(data.dirtiness);
graphTitle = "Dirtiness: " + cleanValue(data.dirtiness) + "%";
plotValue = graphDirtinessValue;
maxY = 100;
}
void statusInfoPage(DataPacket &data) {
pageIsGraph = false;
String HEADER = "Status Information"; // Text to display
display.setTextSize(2); // Set text size, adjust as needed
display.setTextColor(WHITE); // Text color
// Calculate the text bounds
int16_t textX = 200, textY = 20; // Start from x = 0, y = 10 for top
uint16_t textWidth, textHeight;
display.getTextBounds(HEADER, textX, textY, &textX, &textY, &textWidth, &textHeight);
// Calculate the x-coordinate to center the text
int centerX = (display.width() - textWidth) / 2;
// Set cursor to the centered position
display.setCursor(centerX, textY);
// Draw the text
display.print(HEADER);
String statusHex = data.status; // Get the status value from the data object
unsigned long statusValue = strtoul(statusHex.c_str(), NULL, 16); // Convert hex string to unsigned long
display.setTextSize(2);
display.setTextColor(WHITE);
int yPos = 30; // Starting Y position for the text
int xPos = 10; // Starting X position for the text
int lineHeight = 30; // Space between lines
// Print each status bit's information
display.setCursor(xPos, yPos);
yPos += lineHeight;
// Check each bit and print the corresponding status
if (statusValue & 0x0001) { // Bit 0: Lamp Low Warning
display.setCursor(xPos, yPos);
display.print("Bit 0: Lamp Low Warning");
yPos += lineHeight;
}
if (statusValue & 0x0002) { // Bit 1: Lamp Low Error
display.setCursor(xPos, yPos);
display.print("Bit 1: Lamp Low Error");
yPos += lineHeight;
}
if (statusValue & 0x0004) { // Bit 2: Lamp Off Error
display.setCursor(xPos, yPos);
display.print("Bit 2: Lamp Off Error");
yPos += lineHeight;
}
if (statusValue & 0x0008) { // Bit 3: Dirty Warning
display.setCursor(xPos, yPos);
display.print("Bit 3: Dirty Warning");
yPos += lineHeight;
}
if (statusValue & 0x0010) { // Bit 4: Dirty Error
display.setCursor(xPos, yPos);
display.print("Bit 4: Dirty Error");
yPos += lineHeight;
}
if (statusValue & 0x0020) { // Bit 5: Overpressure Error
display.setCursor(xPos, yPos);
display.print("Bit 5: Overpressure Error");
yPos += lineHeight;
}
if (statusValue & 0x0040) { // Bit 6: Overrange Error
display.setCursor(xPos, yPos);
display.print("Bit 6: Overrange Error");
yPos += lineHeight;
}
if (statusValue & 0x0080) { // Bit 7: EEPROM Error
display.setCursor(xPos, yPos);
display.print("Bit 7: EEPROM Error");
yPos += lineHeight;
}
if (statusValue & 0x0100) { // Bit 8: Zeroing
display.setCursor(xPos, yPos);
display.print("Bit 8: Zeroing");
yPos += lineHeight;
}
if (statusValue & 0x0200) { // Bit 9: Warmup
display.setCursor(xPos, yPos);
display.print("Bit 9: Warmup");
yPos += lineHeight;
}
if (statusValue & 0x0400) { // Bit 10: Lamp High Error
display.setCursor(xPos, yPos);
display.print("Bit 10: Lamp High Error");
yPos += lineHeight;
}
if (statusValue & 0x4000) { // Bit 14: Low Alarm
display.setCursor(xPos, yPos);
display.print("Bit 14: Low Alarm");
yPos += lineHeight;
}
if (statusValue & 0x8000) { // Bit 15: High Alarm
display.setCursor(xPos, yPos);
display.print("Bit 15: High Alarm");
yPos += lineHeight;
}
if (statusValue == 0x0000) {
display.setCursor(xPos, yPos);
display.print("All systems OK.");
}
}
void updatePlotValue(DataPacket &data) {
switch (currentPage) {
case 2:
plotValue = stringToFloat(data.concentration);
graphTitle = "Ozone: " + cleanValue(data.concentration) + "g/Nm3";
maxY = 150;
break;
case 3:
plotValue = stringToFloat(data.pressure);
graphTitle = "Pressure: " + cleanValue(data.pressure) + " bar";
maxY = 4;
break;
case 4:
plotValue = stringToFloat(data.dirtiness);
graphTitle = "Dirtiness: " + cleanValue(data.dirtiness) + "%";
maxY = 100;
break;
default:
break;
}
}
};
displayData displayManager;
// =================================================================================================
// SerialReader Class (uses Serial1 directly)
// =================================================================================================
class SerialReader {
public:
String serialString = "";
bool read() {
while (Serial1.available() > 0) {
char incomingByte = Serial1.read();
if (incomingByte == '\r') {
return true;
}
if (isPrintable(incomingByte)) {
serialString += incomingByte;
}
}
return false;
}
String getPacket() {
String packet = serialString;
serialString = "";
return packet;
}
void processData(DataPacket &data, displayData &displayManager) {
if (read()) {
String packet = getPacket();
if (packet.startsWith("*")) {
Serial.println("Command response: " + packet);
return;
}
Serial.print("Raw Data: ");
Serial.println(packet);
data.parse(packet);
if (data.date != "") {
data.printParsedData();
String status = getStatus(data.status);
printRS232Data(data.date, data.timeStr, data.concentration, data.pressure, data.dirtiness, status);
displayManager.updatePlotValue(data); // Call updatePlotValue here
}
}
}
};
SerialReader reader;
// =================================================================================================
// BitmapDrawer Class
// =================================================================================================
class BitmapDrawer {
public:
Adafruit_ST7796S &display;
BitmapDrawer(Adafruit_ST7796S &disp)
: display(disp) {}
void draw(const char *filename, int x, int y) {
File bmpFile = SD.open(filename);
if (!bmpFile) return;
if (read16(bmpFile) != 0x4D42) return;
read32(bmpFile); // Skip headers
read32(bmpFile);
uint32_t bmpImageoffset = read32(bmpFile);
uint32_t headerSize = read32(bmpFile);
if (headerSize != 40) return;
int bmpWidth = read32(bmpFile);
int bmpHeight = read32(bmpFile);
if (read16(bmpFile) != 1) return;
if (read16(bmpFile) != 24) return;
read32(bmpFile);
read32(bmpFile);
read32(bmpFile);
read32(bmpFile);
read32(bmpFile);
read32(bmpFile);
uint8_t rowPad = (4 - (bmpWidth * 3) % 4) % 4;
if (bmpHeight < 0) bmpHeight = -bmpHeight;
bmpFile.seek(bmpImageoffset);
float scaleX = 480.0 / bmpWidth;
float scaleY = 320.0 / bmpHeight;
float scale = min(scaleX, scaleY);
int scaledWidth = bmpWidth * scale;
int scaledHeight = bmpHeight * scale;
for (int row = scaledHeight - 1; row >= 0; row--) {
for (int col = scaledWidth - 1; col >= 0; col--) {
uint8_t b = bmpFile.read();
uint8_t g = bmpFile.read();
uint8_t r = bmpFile.read();
uint16_t color = display.color565(r, g, b);
display.drawPixel(x + col, y + (scaledHeight - 1 - row), color);
}
for (int p = 0; p < rowPad; p++) bmpFile.read();
}
bmpFile.close();
}
private:
uint16_t read16(File &f) {
uint16_t result;
((uint8_t *)&result)[0] = f.read();
((uint8_t *)&result)[1] = f.read();
return result;
}
uint32_t read32(File &f) {
uint32_t result;
((uint8_t *)&result)[0] = f.read();
((uint8_t *)&result)[1] = f.read();
((uint8_t *)&result)[2] = f.read();
((uint8_t *)&result)[3] = f.read();
return result;
}
};
void setupDisplay() {
display.init(320, 480, 0, 0, ST7796S_RGB);
display.setRotation(1);
display.fillScreen(BLACK);
BitmapDrawer drawer(display);
drawer.draw("/TRIOGEN.BMP", 0, 0);
delay(1000);
display.fillScreen(BLACK);
}
void printData(int x, int y, const String &label, const String &data, uint16_t color) {
if (currentPage == 1) {
display.setCursor(x, y);
display.setTextColor(WHITE);
display.setTextSize(2);
display.print(label);
display.print("");
display.print(data);
}
}
void setup() {
Serial1.begin(9600, SERIAL_8N1, RX_PIN, TX_PIN);
SD.begin(SD_CS);
touchScreen.begin();
setupDisplay();
display.setRotation(3);
Serial.begin(9600);
}
void loop() {
currentMillis = millis();
tp = touchScreen.scan();
displayManager.pageSelect(data);
reader.processData(data, displayManager);
}
Config:
// config.h
#ifndef CONFIG_H
#define CONFIG_H
// Pin definitions
#define SD_CS 5
#define CPT_INT 6
#define CPT_RST 7
#define TFT_DC 8
#define TFT_RST 9
#define TFT_CS 10
#define CPT_SDA 11
#define CPT_SCL 12
#define TX_PIN 17
#define RX_PIN 18
// SPI configuration
#define SPI_FREQUENCY 16000000
// display objects (LCD and CPT touch screen)
Adafruit_ST7796S display(TFT_CS, TFT_DC, TFT_RST);
FT6336U touchScreen(CPT_SDA, CPT_SCL, CPT_RST, CPT_INT);
// structure for storing touch data
FT6336U_TouchPointType tp;
uint16_t GREY = display.color565(180, 180, 180);
uint16_t BLACK = display.color565(255, 255, 255);
uint16_t WHITE = display.color565(0, 0, 0);
uint16_t RED = display.color565(255, 255, 0);
uint16_t GREEN = display.color565(255, 0, 255);
uint16_t BLUE = display.color565(0, 255, 255);
uint16_t MAGENTA = display.color565(0, 255, 0);
const int graphX = 60; // X position of the graph (left margin)
const int graphY = 40; // Y position of the graph (top margin)
const int graphWidth = 360; // Graph width
const int graphHeight = 240; // Graph height
const int gridXNum = 10; // Number of gridlines on X axis (columns)
const int gridYNum = 10; // Number of gridlines on Y axis (rows)
// Coordinates for Previous and Next buttons
int prevButtonX1 = 0;
int prevButtonY1 = 380;
int prevButtonX2 = 100;
int prevButtonY2 = 480;
int nextButtonX1 = 0;
int nextButtonY1 = 0;
int nextButtonX2 = 100;
int nextButtonY2 = 100;
const int axisThickness = 2; // Thickness of the axes
String graphTitle = ""; // Default to empty string
// Variable to track whether you're on the graph page
bool pageIsGraph = false; // Default to false, change to true when you're on the graph page
bool isCalled = false; //Default to false, checks if drawGraph() func has already been called
int currentPage = 1;
int totalPages = 5; // Total number of pages
float plotValue;
int maxY;
unsigned long previousMillis = 0;
unsigned long currentMillis;
const long interval = 1000; // 1 second interval
const int maxDataPoints = 61;
float dataValues[maxDataPoints];
int dataCount = 0;
#endif // CONFIG_H
(Yes, I know I'm using a lot of global variables)
Libraries:
// libraries.h
#ifndef LIBRARIES_H
#define LIBRARIES_H
#include <SPI.h>
#include <FS.h>
#include <SD.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7796S.h>
#include <FT6336U.h>
#include <Fonts/FreeSansBold18pt7b.h>
#endif // LIBRARIES_H
I have tried encapsulating the drawGraph() function in startWrite and endWrite for batching the commands, but this appears to brick the code.
I still need to fix comments etc, but I just want to try reduce the flicker for now. I also know my colour definitions are weird, but they work (Cheap Chinese LCD).