#include "lcd.h" #define NEEDLE_LENGTH 35 // Visible length #define NEEDLE_WIDTH 5 // Width of needle - make it an odd number #define NEEDLE_RADIUS 90 // Radius at tip #define NEEDLE_COLOR1 TFT_MAROON // Needle periphery colour #define NEEDLE_COLOR2 TFT_RED // Needle centre colour #define DIAL_CENTRE_X 120 #define DIAL_CENTRE_Y 120 #define MAXGUAGE 240 #define MINGUAGE 0 #define DISPLAY_ROTATE 15 //sec #define BACKLIGHTTIMEOUT 60000 #define BACKLIGHTONBRGT 84 #define BACKLIGHTSTEP 1 #define AA_FONT_LARGE NotoSansBold36 enum DISPLAY_STATE { DISPLAY_PM1P0, DISPLAY_PM2P5, DISPLAY_PM10P0, DISPLAY_TEMP, DISPLAY_HUM, DISPLAY_CO2, DISPLAY_LAST }; // sprite elements TFT_eSPI tft = TFT_eSPI(); TFT_eSprite needle = TFT_eSprite(&tft); // Sprite object for needle TFT_eSprite spr = TFT_eSprite(&tft); // Sprite for meter reading TFT_eSprite nameSpr = TFT_eSprite(&tft); TFT_eSprite unitSpr = TFT_eSprite(&tft); TFT_eSprite ProgressBar = TFT_eSprite(&tft); uint16_t *tft_buffer; bool buffer_loaded = false; uint16_t spr_width = 0; uint16_t name_spr_width = 0; bool progessbarActive = false; DISPLAY_STATE displaystate = DISPLAY_PM1P0; uint32_t display_last_update = 0; uint32_t backlightTimeout = 0; uint8_t backlightBrightness = BACKLIGHTONBRGT; // ======================================================================================= // Declarations // ======================================================================================= void createNeedle(void); void plotNeedle(int16_t angle, uint16_t ms_delay); void setBacklight(uint8_t value); bool tft_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t *bitmap); void backlightRefresh(void); // ======================================================================================= // implementation // ======================================================================================= bool tft_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t *bitmap) { // Stop further decoding as image is running off bottom of screen if (y >= tft.height()) return 0; // This function will clip the image block rendering automatically at the TFT boundaries tft.pushImage(x, y, w, h, bitmap); // Return 1 to decode next block return 1; } void setBacklight(uint8_t value) { ledcWrite(TFT_BL_PWMCHANNEL, value); } void ProgressbarVisible(bool visible) { progessbarActive = visible; if (!visible) { ProgressBar.fillSprite(TFT_BLACK); ProgressBar.pushSprite(10, 270); } } void setOTAProgress(uint8_t value) { uint16_t progress = map(value, 0, 100, 0, 212); ProgressBar.drawRoundRect(0, 2, 220, 22, 5, TFT_BLUE); ProgressBar.fillRoundRect(4, 5, progress, 15, 3, TFT_BLUE); if (progessbarActive) { ProgressBar.pushSprite(10, 270); } } void backlightRefresh(void) { backlightTimeout = millis(); } void plotNeedle(int16_t angle, uint16_t ms_delay, uint32_t value) { static int16_t old_angle = -120; // Starts at -120 degrees // Bounding box parameters static int16_t min_x; static int16_t min_y; static int16_t max_x; static int16_t max_y; if (angle < 0) angle = 0; // Limit angle to emulate needle end stops if (angle > 240) angle = 240; angle -= 120; // Starts at -120 degrees // Move the needle until new angle reached while (angle != old_angle || !buffer_loaded) { if (old_angle < angle) old_angle++; else old_angle--; // Only plot needle at even values to improve plotting performance if ((old_angle & 1) == 0) { if (buffer_loaded) { // Paste back the original needle free image area tft.pushRect(min_x, min_y, 1 + max_x - min_x, 1 + max_y - min_y, tft_buffer); } if (needle.getRotatedBounds(old_angle, &min_x, &min_y, &max_x, &max_y)) { // Grab a copy of the area before needle is drawn tft.readRect(min_x, min_y, 1 + max_x - min_x, 1 + max_y - min_y, tft_buffer); buffer_loaded = true; } // Draw the needle in the new postion, black in needle image is transparent needle.pushRotated(old_angle, TFT_BLACK); // Wait before next update delay(ms_delay); } // Update the number at the centre of the dial spr.drawNumber(value, spr_width / 2, spr.fontHeight() / 2); spr.pushSprite(120 - spr_width / 2, 120 - spr.fontHeight() / 2); // Slow needle down slightly as it approaches the new position if (abs(old_angle - angle) < 10) ms_delay += ms_delay / 5; } } // ======================================================================================= // Setup // ======================================================================================= void createProgressBar(void) { ProgressBar.createSprite(220, 30); ProgressBar.fillSprite(TFT_BLACK); ProgressBar.drawRoundRect(0, 2, 220, 22, 5, TFT_BLUE); ProgressBar.pushSprite(10, 270); } void createNameSprite(void) { nameSpr.setTextFont(2); name_spr_width = nameSpr.textWidth("---Sensor Name---"); nameSpr.createSprite(name_spr_width, nameSpr.fontHeight() * 2 + 2); uint16_t bg_color = tft.readPixel(120, 120); // Get colour from dial centre nameSpr.fillSprite(bg_color); nameSpr.setTextColor(TFT_WHITE, bg_color); nameSpr.setTextDatum(MC_DATUM); nameSpr.setTextPadding(name_spr_width); nameSpr.drawString("Sensor Name", name_spr_width / 2, nameSpr.fontHeight() / 2); nameSpr.drawString("Unit", name_spr_width / 2, nameSpr.fontHeight() / 2 * 3 + 2); nameSpr.pushSprite(DIAL_CENTRE_X - name_spr_width / 2, DIAL_CENTRE_Y + 40, 2); } void createValueSprinte(void) { spr.loadFont(AA_FONT_LARGE); spr_width = spr.textWidth("277"); spr.createSprite(spr_width, spr.fontHeight()); uint16_t bg_color = tft.readPixel(120, 120); // Get colour from dial centre spr.fillSprite(bg_color); spr.setTextColor(TFT_WHITE, bg_color); spr.setTextDatum(MC_DATUM); spr.setTextPadding(spr_width); spr.drawNumber(0, spr_width / 2, spr.fontHeight() / 2); spr.pushSprite(DIAL_CENTRE_X - spr_width / 2, DIAL_CENTRE_Y - spr.fontHeight() / 2); } void createNeedle(void) { needle.setColorDepth(16); needle.createSprite(NEEDLE_WIDTH, NEEDLE_LENGTH); // create the needle Sprite needle.fillSprite(TFT_BLACK); // Fill with black // Define needle pivot point relative to top left corner of Sprite uint16_t piv_x = NEEDLE_WIDTH / 2; // pivot x in Sprite (middle) uint16_t piv_y = NEEDLE_RADIUS; // pivot y in Sprite needle.setPivot(piv_x, piv_y); // Set pivot point in this Sprite // Draw the red needle in the Sprite needle.fillRect(0, 0, NEEDLE_WIDTH, NEEDLE_LENGTH, TFT_MAROON); needle.fillRect(1, 1, NEEDLE_WIDTH - 2, NEEDLE_LENGTH - 2, TFT_RED); // Bounding box parameters to be populated int16_t min_x; int16_t min_y; int16_t max_x; int16_t max_y; // Work out the worst case area that must be grabbed from the TFT, // this is at a 45 degree rotation needle.getRotatedBounds(45, &min_x, &min_y, &max_x, &max_y); // Calculate the size and allocate the buffer for the grabbed TFT area tft_buffer = (uint16_t *)malloc(((max_x - min_x) + 2) * ((max_y - min_y) + 2) * 2); } void createDail(void) { TJpgDec.setSwapBytes(true); // The jpeg decoder must be given the exact name of the rendering function above TJpgDec.setCallback(tft_output); // Draw the dial TJpgDec.drawJpg(0, 0, dial, sizeof(dial)); tft.drawCircle(DIAL_CENTRE_X, DIAL_CENTRE_Y, NEEDLE_RADIUS - NEEDLE_LENGTH, TFT_DARKGREY); // Define where the needle pivot point is on the TFT before // creating the needle so boundary calculation is correct tft.setPivot(DIAL_CENTRE_X, DIAL_CENTRE_Y); } void initLCD() { Serial.print("InitLCD:"); // The byte order can be swapped (set true for TFT_eSPI) tft.begin(); tft.setRotation(0); tft.fillScreen(TFT_BLACK); // Create the Sprites createProgressBar(); createDail(); createValueSprinte(); createNeedle(); createNameSprite(); ledcSetup(TFT_BL_PWMCHANNEL, TFT_BL_FREQ, TFT_BL_BITS); ledcAttachPin(TFT_BL, TFT_BL_PWMCHANNEL); ledcWrite(TFT_BL_PWMCHANNEL, BACKLIGHTONBRGT); // Reset needle position to 0 plotNeedle(0, 0, 0); Serial.println("OK"); } // ======================================================================================= // Loop // ======================================================================================= void handleBacklight(void) { uint32_t timeNow = millis(); if (!backlightTimeout) { backlightRefresh(); } if (timeNow - backlightTimeout > BACKLIGHTTIMEOUT) { if (backlightBrightness > BACKLIGHTSTEP) { backlightBrightness -= BACKLIGHTSTEP; } else { backlightBrightness = 0; } } else { if (backlightBrightness < BACKLIGHTONBRGT) { backlightBrightness += BACKLIGHTSTEP; } } ledcWrite(TFT_BL_PWMCHANNEL, backlightBrightness); } sensor_e displaySensor[] = { AE_1P0, AE_2P5, AE_10P0, SCD30_temp, SCD30_hum, SCD30_co2, SGP30_TVOC, }; uint8_t displaySensorSize = 7; uint8_t displaySensorIndex = 0; void nextSensor(void) { if (displaySensorIndex < displaySensorSize - 1) { displaySensorIndex++; } else { displaySensorIndex = 0; } display_last_update = millis(); } void previousSensor(void) { if (displaySensorIndex > 0) { displaySensorIndex--; } else { displaySensorIndex = displaySensorSize - 1; } display_last_update = millis(); } void handleplotSensor(void) { if(!backlightBrightness) { return; } AQSSensor *sensor = getSensor(displaySensor[displaySensorIndex]); if (sensor == NULL) { Serial.println("LCD: getSensor=NULL!"); nextSensor(); return; } uint16_t angle = map(sensor->value(), sensor->getMin(), sensor->getMax(), MINGUAGE, MAXGUAGE); plotNeedle(angle, 15, sensor->value()); nameSpr.drawString(sensor->getName().c_str(), name_spr_width / 2, nameSpr.fontHeight() / 2); nameSpr.drawString(sensor->getUnit().c_str(), name_spr_width / 2, nameSpr.fontHeight() / 2 * 3 + 2); nameSpr.pushSprite(DIAL_CENTRE_X - name_spr_width / 2, DIAL_CENTRE_Y + 40, 2); } void handleDisplayRotate(void) { //rotate display if(!backlightBrightness) { return; } uint32_t timenow = millis(); if (timenow - display_last_update > (DISPLAY_ROTATE * 1000)) { Serial.println("LCD next State"); nextSensor(); } } void handleLCD() { handleDisplayRotate(); handleplotSensor(); handleBacklight(); } // =======================================================================================