Created
February 20, 2021 15:34
-
-
Save codeandmake/8a26c2c7ed6f3b3d8e79e5000bca45e7 to your computer and use it in GitHub Desktop.
This code accompanies our Code for Arduino Thermometer Display project: https://codeandmake.com/post/arduino-thermometer-display
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright 2021 Code and Make (codeandmake.com) | |
* | |
* Permission is hereby granted, free of charge, to any person obtaining a copy | |
* of this software and associated documentation files (the "Software"), to deal | |
* in the Software without restriction, including without limitation the rights | |
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
* copies of the Software, and to permit persons to whom the Software is | |
* furnished to do so, subject to the following conditions: | |
* | |
* The above copyright notice and this permission notice shall be included in | |
* all copies or substantial portions of the Software. | |
* | |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
* SOFTWARE. | |
*/ | |
/* | |
* Code for Arduino Thermometer Display; an Arduino-based thermometer and humidity display | |
* | |
* This code accompanies the following project: | |
* | |
* https://codeandmake.com/post/arduino-thermometer-display | |
*/ | |
#include <DHT.h> | |
#include <math.h> | |
#include <Adafruit_GFX.h> | |
#include <Adafruit_TFTLCD.h> | |
#include <MCUFRIEND_kbv.h> | |
#include <Fonts/FreeSansBold18pt7b.h> | |
#define DHTPIN 12 | |
#define DHTTYPE DHT22 | |
DHT dht(DHTPIN, DHTTYPE); | |
MCUFRIEND_kbv tft; | |
#define LCD_CS A3 | |
#define LCD_CD A2 | |
#define LCD_WR A1 | |
#define LCD_RD A0 | |
#define LCD_RESET A4 | |
// 16-bit colors | |
#define BLACK 0x0000 | |
#define BLUE 0x001F | |
#define CYAN 0x07FF | |
#define DARKCYAN 0x05F7 | |
#define DARKGREEN 0x05E0 | |
#define DARKORANGE 0xC3E0 | |
#define DARKRED 0x7800 | |
#define DARKYELLOW 0x7BE0 | |
#define GREEN 0x07C0 | |
#define MAGENTA 0xF81F | |
#define ORANGE 0xFD00 | |
#define PURPLE 0x780F | |
#define RED 0xF800 | |
#define WHITE 0xFFFF | |
#define YELLOW 0xFFE0 | |
#define POLLING_INTERVAL 3000 | |
#define POLLS_PER_GRAPH_UPDATE 60 // about 12 hours on screen | |
#define TFT_WIDTH 240 | |
#define TFT_HEIGHT 320 | |
const boolean rotateScreen = false; | |
const boolean darkMode = true; | |
const unsigned long refreshInterval = POLLING_INTERVAL; | |
unsigned long previousTime = 0; | |
byte pollTick = 0; | |
byte graphTick = 0; | |
const float a = 17.62; | |
const float b = 243.12; | |
float previousC = -128; | |
float previousHic = -128; | |
float previousH = -128; | |
float previousDewC = -128; | |
char temps[TFT_WIDTH]; | |
char feels[TFT_WIDTH]; | |
char hums[TFT_WIDTH]; | |
char dews[TFT_WIDTH]; | |
uint16_t tempGraphColor = (darkMode ? RED : DARKRED); | |
uint16_t feelGraphColor = (darkMode ? YELLOW : DARKYELLOW); | |
uint16_t humidityGraphColor = (darkMode ? MAGENTA : PURPLE); | |
uint16_t dewPointGraphColor = (darkMode ? CYAN : DARKCYAN); | |
void drawCentreString(const char *buf, int x, int y) | |
{ | |
int16_t x1, y1; | |
uint16_t w, h; | |
tft.getTextBounds(buf, x, y, &x1, &y1, &w, &h); //calc width of new string | |
tft.setCursor(x - (w / 2), y); | |
tft.print(buf); | |
} | |
void drawValue(float value, char *unit, byte precision, GFXfont *font, uint16_t color, float x, float y) { | |
char buf[(precision ? 8 : 6)]; | |
dtostrf(value, 1, precision, buf); | |
strcat(buf, unit); | |
drawText(buf, font, color, x, y); | |
} | |
void drawText(char *text, GFXfont *font, uint16_t color, float x, float y) { | |
tft.setTextColor(color); | |
tft.setFont(font); | |
if(font == NULL) { | |
tft.setTextSize(2); | |
drawCentreString(text, TFT_WIDTH * x, (TFT_HEIGHT * y) - (6 * 2)); | |
tft.setTextSize(1); | |
} else { | |
drawCentreString(text, TFT_WIDTH * x, TFT_HEIGHT * y); | |
} | |
} | |
// Clear section | |
void clearSection(float x, float y) { | |
tft.fillRect(TFT_WIDTH * (x - 0.25), TFT_HEIGHT * (y - 0.085), | |
TFT_WIDTH * 0.5, TFT_HEIGHT * 0.17, darkMode ? BLACK : WHITE); | |
} | |
// Draw number section with number sub-section | |
void drawSection(float value, char *unit, byte precision, float subValue, char *subUnit, uint16_t color, float x, float y) { | |
clearSection(x, y); | |
drawValue(value, unit, precision, &FreeSansBold18pt7b, color, x, y); | |
drawValue(subValue, subUnit, precision, NULL, color, x, (y + 0.07)); | |
} | |
// Draw number section with text sub-section | |
void drawSectionTextSub(float value, char *unit, byte precision, char *subValue, uint16_t color, float x, float y) { | |
clearSection(x, y); | |
drawValue(value, unit, precision, &FreeSansBold18pt7b, color, x, y); | |
drawText(subValue, NULL, color, x, y + 0.07); | |
} | |
// Draw text section with no sub-section | |
void drawSectionText(char *text, uint16_t color, float x, float y) { | |
clearSection(x, y); | |
drawText(text, &FreeSansBold18pt7b, color, x, y); | |
} | |
uint16_t getTemperatureColor(int value) { | |
if(value < 0) { | |
return (darkMode ? BLUE : DARKCYAN); | |
} | |
if(value < 20) { | |
return blend((darkMode ? BLUE : DARKCYAN), (darkMode ? GREEN : DARKGREEN), value, 0, 20); | |
} | |
if(value < 40) { | |
return blend((darkMode ? GREEN : DARKGREEN), (darkMode ? RED : DARKRED), value, 20, 40); | |
} | |
return (darkMode ? RED : DARKRED); | |
} | |
uint16_t getHumidityColor(int value) { | |
if(value < 25) { | |
return (darkMode ? RED : DARKRED); | |
} | |
if(value < 30) { | |
return blend((darkMode ? RED : DARKRED), (darkMode ? YELLOW : DARKYELLOW), value, 25, 30); | |
} | |
if(value < 45) { | |
return blend((darkMode ? YELLOW : DARKYELLOW), (darkMode ? GREEN : DARKGREEN), value, 30, 45); | |
} | |
if(value < 60) { | |
return blend((darkMode ? GREEN : DARKGREEN), (darkMode ? YELLOW : DARKYELLOW), value, 45, 60); | |
} | |
if(value < 70) { | |
return blend((darkMode ? YELLOW : DARKYELLOW), (darkMode ? RED : DARKRED), value, 60, 70); | |
} | |
return (darkMode ? RED : DARKRED); | |
} | |
char* getHumidityText(int value) { | |
if(value < 25) { | |
return "V. Dry"; | |
} | |
if(value < 30) { | |
return "Dry"; | |
} | |
if(value < 60) { | |
return "Good"; | |
} | |
if(value < 70) { | |
return "Humid"; | |
} | |
return "V. Humid"; | |
} | |
unsigned short blendChannel(unsigned short v1, unsigned short v2, float mix) { | |
return ((v2 * mix) + (v1 * (255 - mix))) / 255; | |
} | |
unsigned short blend(unsigned short color1, unsigned short color2, float mix) { | |
// split color1 into channels | |
unsigned color1r = color1 >> 11; | |
unsigned color1g = (color1 >> 5) & ((1u << 6) - 1); | |
unsigned color1b = color1 & ((1u << 5) - 1); | |
// split color2 into channels | |
unsigned color2r = color2 >> 11; | |
unsigned color2g = (color2 >> 5) & ((1u << 6) - 1); | |
unsigned color2b = color2 & ((1u << 5) - 1); | |
// blend each channel | |
unsigned mixedR = blendChannel(color1r, color2r, mix); | |
unsigned mixedG = blendChannel(color1g, color2g, mix); | |
unsigned mixedB = blendChannel(color1b, color2b, mix); | |
// pack | |
return (unsigned short) ((mixedR << 11) | (mixedG << 5) | mixedB); | |
} | |
unsigned short blend(unsigned short color1, unsigned short color2, float value, float lower, float upper) { | |
unsigned char mix = (value - lower) / (upper - lower) * 255; | |
return blend(color1, color2, mix); | |
} | |
void setup() { | |
for(int i = 0; i < TFT_WIDTH ; i++) { | |
temps[i] = -128; | |
feels[i] = -128; | |
hums[i] = -128; | |
dews[i] = -128; | |
} | |
dht.begin(); | |
tft.reset(); | |
tft.begin(tft.readID()); | |
tft.fillScreen(darkMode ? BLACK : WHITE); | |
tft.setTextWrap(false); | |
tft.setRotation((rotateScreen == false ? 0 : 2)); | |
// text | |
drawText("Temp", NULL, blend(BLACK, WHITE, 128), 0.25, 0.07); | |
drawText("Feel", NULL, blend(BLACK, WHITE, 128), 0.75, 0.07); | |
drawText("Humidity", NULL, blend(BLACK, WHITE, 128), 0.25, 0.41); | |
drawText("Dew Point", NULL, blend(BLACK, WHITE, 128), 0.75, 0.41); | |
// footer | |
drawText("codeandmake.com", NULL, blend(BLACK, WHITE, (darkMode ? 64 : 192)), 0.5, 0.97); | |
// key dots | |
tft.setTextSize(2); | |
drawText("-", NULL, tempGraphColor, 0.25, 0.32); | |
drawText("-", NULL, feelGraphColor, 0.75, 0.32); | |
drawText("-", NULL, humidityGraphColor, 0.25, 0.66); | |
drawText("-", NULL, dewPointGraphColor, 0.75, 0.66); | |
tft.setTextSize(1); | |
// initial update | |
update(); | |
} | |
void loop() { | |
unsigned long now = millis(); | |
if (now - previousTime >= refreshInterval) { | |
update(); | |
previousTime = now; | |
} | |
} | |
void update() { | |
// status indicator | |
tft.fillCircle(TFT_WIDTH - 6, TFT_HEIGHT - 6, 3, (darkMode ? ORANGE : DARKORANGE)); | |
// read values from sensor | |
float h = dht.readHumidity(); | |
float c = dht.readTemperature(); | |
float f = dht.readTemperature(true); | |
// clear status indicator | |
tft.fillCircle(TFT_WIDTH - 6, TFT_HEIGHT - 6, 3, darkMode ? BLACK : WHITE); | |
// check if any reads failed and exit early (to try again). | |
if (isnan(h) || isnan(c) || isnan(f)) { | |
tft.fillCircle(TFT_WIDTH - 6, TFT_HEIGHT - 6, 3, (darkMode ? RED : DARKRED)); | |
return; | |
} | |
float hif = dht.computeHeatIndex(f, h); | |
float hic = dht.computeHeatIndex(c, h, false); | |
// compute dew point | |
float alpha = log(h / 100) + (a * c / (b + c)); | |
float dewC = (b * alpha) / (a - alpha); | |
float dewF = (dewC * (9.0/5)) + 32; | |
// temp | |
if(previousC != c) { | |
previousC = c; | |
drawSection(c, "C", 1, f, "F", getTemperatureColor(c), 0.25, 0.19); | |
} | |
// feel | |
if(previousHic != hic) { | |
previousHic = hic; | |
drawSection(hic, "C", 1, hif, "F", getTemperatureColor(hic), 0.75, 0.19); | |
} | |
// humidity | |
if(previousH != h) { | |
previousH = h; | |
drawSectionTextSub(h, "%", 0, getHumidityText(h), getHumidityColor(h), 0.25, 0.53); | |
} | |
// dew point | |
if(previousDewC != dewC) { | |
previousDewC = dewC; | |
if(!isnan(dewC)) { | |
drawSection(dewC, "C", 1, dewF, "F", getTemperatureColor(dewC), 0.75, 0.53); | |
} else { | |
drawSectionText("N/A", (darkMode ? WHITE : BLACK), 0.75, 0.52); | |
} | |
} | |
// graph | |
if(pollTick == 0) { | |
temps[graphTick] = (!isnan(c) && c >= -50 && c <= 100 ? c : -128); | |
feels[graphTick] = (!isnan(hic) && hic >= -50 && hic <= 100 ? hic : -128); | |
hums[graphTick] = (!isnan(h) && h >= -50 && h <= 100 ? h : -128); | |
dews[graphTick] = (!isnan(dewC) && dewC >= -50 && dewC <= 100 ? dewC : -128); | |
for (byte x = 0; x < TFT_WIDTH; x++) { | |
int screenX = (TFT_WIDTH + (x - graphTick) - 1) % TFT_WIDTH; | |
tft.drawFastVLine(screenX, TFT_HEIGHT * 0.89, -TFT_HEIGHT * 0.2, darkMode ? BLACK : WHITE); | |
// vertical graph lines | |
if(screenX != 0 && screenX % (TFT_WIDTH / 12) == 0) { | |
tft.drawFastVLine(screenX, TFT_HEIGHT * 0.89, -TFT_HEIGHT * 0.2, blend(BLACK, WHITE, (darkMode ? 64 : 192))); | |
} | |
// horizontal graph lines | |
for (byte y = 0; y < 7; y++) { | |
if(y == 2 && screenX % 2 == 0) { | |
// dotted line at 0 | |
tft.drawPixel(screenX, (TFT_HEIGHT * 0.89) - (TFT_HEIGHT * 0.2 * (y / 6.0)), blend(BLACK, WHITE, 128)); | |
} else { | |
tft.drawPixel(screenX, (TFT_HEIGHT * 0.89) - (TFT_HEIGHT * 0.2 * (y / 6.0)), blend(BLACK, WHITE, (darkMode ? 64 : 192))); | |
} | |
} | |
// calculate colors | |
int dewsY; | |
uint16_t dewColor; | |
int humsY; | |
uint16_t humsColor; | |
int feelsY; | |
uint16_t feelsColor; | |
int tempsY; | |
uint16_t tempsColor; | |
if(dews[x] > -128) { | |
dewColor = dewPointGraphColor; | |
dewsY = (TFT_HEIGHT * 0.89) - (TFT_HEIGHT * 0.2 * ((dews[x] + 50) / 150.0)); | |
} | |
if(hums[x] > -128) { | |
humsColor = humidityGraphColor; | |
humsY = (TFT_HEIGHT * 0.89) - (TFT_HEIGHT * 0.2 * ((hums[x] + 50) / 150.0)); | |
} | |
if(feels[x] > -128) { | |
feelsColor = feelGraphColor; | |
feelsY = (TFT_HEIGHT * 0.89) - (TFT_HEIGHT * 0.2 * ((feels[x] + 50) / 150.0)); | |
} | |
if(temps[x] > -128) { | |
tempsColor = tempGraphColor; | |
tempsY = (TFT_HEIGHT * 0.89) - (TFT_HEIGHT * 0.2 * ((temps[x] + 50) / 150.0)); | |
} | |
// draw | |
if(x % 4 == 0) { | |
if(dews[x] > -128) { | |
tft.drawPixel(screenX, dewsY, dewColor); | |
} | |
if(hums[x] > -128) { | |
tft.drawPixel(screenX, humsY, humsColor); | |
} | |
if(feels[x] > -128) { | |
tft.drawPixel(screenX, feelsY, feelsColor); | |
} | |
if(temps[x] > -128) { | |
tft.drawPixel(screenX, tempsY, tempsColor); | |
} | |
} | |
if(x % 4 == 1) { | |
if(temps[x] > -128) { | |
tft.drawPixel(screenX, tempsY, tempsColor); | |
} | |
if(feels[x] > -128) { | |
tft.drawPixel(screenX, feelsY, feelsColor); | |
} | |
if(hums[x] > -128) { | |
tft.drawPixel(screenX, humsY, humsColor); | |
} | |
if(dews[x] > -128) { | |
tft.drawPixel(screenX, dewsY, dewColor); | |
} | |
} | |
if(x % 4 == 2) { | |
if(feels[x] > -128) { | |
tft.drawPixel(screenX, feelsY, feelsColor); | |
} | |
if(temps[x] > -128) { | |
tft.drawPixel(screenX, tempsY, tempsColor); | |
} | |
if(dews[x] > -128) { | |
tft.drawPixel(screenX, dewsY, dewColor); | |
} | |
if(hums[x] > -128) { | |
tft.drawPixel(screenX, humsY, humsColor); | |
} | |
} | |
if(x % 4 == 3) { | |
if(hums[x] > -128) { | |
tft.drawPixel(screenX, humsY, humsColor); | |
} | |
if(dews[x] > -128) { | |
tft.drawPixel(screenX, dewsY, dewColor); | |
} | |
if(temps[x] > -128) { | |
tft.drawPixel(screenX, tempsY, tempsColor); | |
} | |
if(feels[x] > -128) { | |
tft.drawPixel(screenX, feelsY, feelsColor); | |
} | |
} | |
} | |
graphTick++; | |
if(graphTick >= TFT_WIDTH) { | |
graphTick = 0; | |
} | |
} | |
pollTick++; | |
if(pollTick >= POLLS_PER_GRAPH_UPDATE) { | |
pollTick = 0; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment