TFT_eSPI/examples/ILI9341/weather-station-v6/weather-station-v6.ino

580 lines
21 KiB
C++

/**The MIT License (MIT)
Copyright (c) 2015 by Daniel Eichhorn
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 COPYBR_DATUM 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.
See more at http://blog.squix.ch
Adapted by Bodmer to use the faster TFT_eSPI library:
https://github.com/Bodmer/TFT_eSPI
Plus:
Minor changes to text placement and auto-blanking out old text with background colour padding
Moon phase text added
Forecast text lines are automatically split onto two lines at a central space (some are long!)
Time is printed with colons aligned to tidy display
Min and max forecast temperatures spaced out
The ` character has been changed to a degree symbol in the 36 point font
New smart WU jpeg splash startup screen and updated progress messages
Display does not need to be blanked between updates
Icons nudged about slightly to add wind direction + speed
*/
#include <Arduino.h>
#include <SPI.h>
#include <TFT_eSPI.h> // Hardware-specific library
// Additional UI functions
#include "GfxUi.h"
// Fonts created by http://oleddisplay.squix.ch/
#include "ArialRoundedMTBold_14.h"
#include "ArialRoundedMTBold_36.h"
// Download helper
#include "WebResource.h"
#include <ESP8266WiFi.h>
#include <ArduinoOTA.h>
#include <ESP8266mDNS.h>
#include <DNSServer.h>
#include <ESP8266WebServer.h>
// Helps with connecting to internet
#include <WiFiManager.h>
// check settings.h for adapting to your needs
#include "settings.h"
#include <JsonListener.h>
#include <WundergroundClient.h>
#include "TimeClient.h"
// HOSTNAME for OTA update
#define HOSTNAME "ESP8266-OTA-"
/*****************************
Important: see settings.h to configure your settings!!!
* ***************************/
TFT_eSPI tft = TFT_eSPI(); // Invoke custom library
boolean booted = true;
GfxUi ui = GfxUi(&tft);
WebResource webResource;
TimeClient timeClient(UTC_OFFSET);
// Set to false, if you prefere imperial/inches, Fahrenheit
WundergroundClient wunderground(IS_METRIC);
//declaring prototypes
void configModeCallback (WiFiManager *myWiFiManager);
void downloadCallback(String filename, int16_t bytesDownloaded, int16_t bytesTotal);
ProgressCallback _downloadCallback = downloadCallback;
void downloadResources();
void updateData();
void drawProgress(uint8_t percentage, String text);
void drawTime();
void drawCurrentWeather();
void drawForecast();
void drawForecastDetail(uint16_t x, uint16_t y, uint8_t dayIndex);
String getMeteoconIcon(String iconText);
void drawAstronomy();
void drawSeparator(uint16_t y);
long lastDownloadUpdate = millis();
void setup() {
Serial.begin(250000);
tft.begin();
tft.fillScreen(TFT_BLACK);
tft.setFreeFont(&ArialRoundedMTBold_14);
tft.setTextDatum(BC_DATUM);
tft.setTextColor(TFT_DARKGREY, TFT_BLACK);
tft.drawString("Original by: blog.squix.org", 120, 240);
tft.drawString("Adapted by: Bodmer", 120, 260);
tft.setTextColor(TFT_ORANGE, TFT_BLACK);
SPIFFS.begin();
//listFiles();
//Uncomment next line if you want to erase SPIFFS and update all internet resources, this takes some time!
tft.drawString("Formatting SPIFFS, so wait!", 120, 200); SPIFFS.format();
if (SPIFFS.exists("/WU.jpg") == true) ui.drawJpeg("/WU.jpg", 0, 10);
if (SPIFFS.exists("/Earth.jpg") == true) ui.drawJpeg("/Earth.jpg", 0, 320-56); // Image is 56 pixels high
delay(1000);
tft.drawString("Connecting to WiFi", 120, 200);
tft.setTextPadding(240); // Pad next drawString() text to full width to over-write old text
//WiFiManager
//Local intialization. Once its business is done, there is no need to keep it around
WiFiManager wifiManager;
// Uncomment for testing wifi manager
//wifiManager.resetSettings();
wifiManager.setAPCallback(configModeCallback);
//or use this for auto generated name ESP + ChipID
wifiManager.autoConnect();
//Manual Wifi
//WiFi.begin(WIFI_SSID, WIFI_PWD);
// OTA Setup
String hostname(HOSTNAME);
hostname += String(ESP.getChipId(), HEX);
WiFi.hostname(hostname);
ArduinoOTA.setHostname((const char *)hostname.c_str());
ArduinoOTA.begin();
// download images from the net. If images already exist don't download
tft.drawString("Downloading to SPIFFS...", 120, 200);
tft.drawString(" ", 120, 240); // Clear line
tft.drawString(" ", 120, 260); // Clear line
downloadResources();
//listFiles();
tft.drawString(" ", 120, 200); // Clear line above using set padding width
tft.drawString("Fetching weather data...", 120, 220);
//delay(500);
// load the weather information
updateData();
}
long lastDrew = 0;
void loop() {
// Handle OTA update requests
ArduinoOTA.handle();
// Check if we should update the clock
if (millis() - lastDrew > 30000 && wunderground.getSeconds() == "00") {
drawTime();
lastDrew = millis();
}
// Check if we should update weather information
if (millis() - lastDownloadUpdate > 1000 * UPDATE_INTERVAL_SECS) {
updateData();
lastDownloadUpdate = millis();
}
}
// Called if WiFi has not been configured yet
void configModeCallback (WiFiManager *myWiFiManager) {
tft.setTextDatum(BC_DATUM);
tft.setFreeFont(&ArialRoundedMTBold_14);
tft.setTextColor(TFT_ORANGE);
tft.drawString("Wifi Manager", 120, 28);
tft.drawString("Please connect to AP", 120, 42);
tft.setTextColor(TFT_WHITE);
tft.drawString(myWiFiManager->getConfigPortalSSID(), 120, 56);
tft.setTextColor(TFT_ORANGE);
tft.drawString("To setup Wifi Configuration", 120, 70);
}
// callback called during download of files. Updates progress bar
void downloadCallback(String filename, int16_t bytesDownloaded, int16_t bytesTotal) {
Serial.println(String(bytesDownloaded) + " / " + String(bytesTotal));
tft.setTextDatum(BC_DATUM);
tft.setTextColor(TFT_ORANGE, TFT_BLACK);
tft.setTextPadding(240);
int percentage = 100 * bytesDownloaded / bytesTotal;
if (percentage == 0) {
tft.drawString(filename, 120, 220);
}
if (percentage % 5 == 0) {
tft.setTextDatum(TC_DATUM);
tft.setTextPadding(tft.textWidth(" 888% "));
tft.drawString(String(percentage) + "%", 120, 245);
ui.drawProgressBar(10, 225, 240 - 20, 15, percentage, TFT_WHITE, TFT_BLUE);
}
}
// Download the bitmaps
void downloadResources() {
// tft.fillScreen(TFT_BLACK);
tft.setFreeFont(&ArialRoundedMTBold_14);
char id[5];
// Download WU graphic jpeg first and display it, then the Earth view
webResource.downloadFile((String)"http://i.imgur.com/njl1pMj.jpg", (String)"/WU.jpg", _downloadCallback);
if (SPIFFS.exists("/WU.jpg") == true) ui.drawJpeg("/WU.jpg", 0, 10);
webResource.downloadFile((String)"http://i.imgur.com/v4eTLCC.jpg", (String)"/Earth.jpg", _downloadCallback);
if (SPIFFS.exists("/Earth.jpg") == true) ui.drawJpeg("/Earth.jpg", 0, 320-56);
//webResource.downloadFile((String)"http://i.imgur.com/IY57GSv.jpg", (String)"/Horizon.jpg", _downloadCallback);
//if (SPIFFS.exists("/Horizon.jpg") == true) ui.drawJpeg("/Horizon.jpg", 0, 320-160);
//webResource.downloadFile((String)"http://i.imgur.com/jZptbtY.jpg", (String)"/Rainbow.jpg", _downloadCallback);
//if (SPIFFS.exists("/Rainbow.jpg") == true) ui.drawJpeg("/Rainbow.jpg", 0, 0);
for (int i = 0; i < 19; i++) {
sprintf(id, "%02d", i);
webResource.downloadFile("http://www.squix.org/blog/wunderground/" + wundergroundIcons[i] + ".bmp", wundergroundIcons[i] + ".bmp", _downloadCallback);
}
for (int i = 0; i < 19; i++) {
sprintf(id, "%02d", i);
webResource.downloadFile("http://www.squix.org/blog/wunderground/mini/" + wundergroundIcons[i] + ".bmp", "/mini/" + wundergroundIcons[i] + ".bmp", _downloadCallback);
}
for (int i = 0; i < 24; i++) {
webResource.downloadFile("http://www.squix.org/blog/moonphase_L" + String(i) + ".bmp", "/moon" + String(i) + ".bmp", _downloadCallback);
}
}
// Update the internet based information and update screen
void updateData() {
// booted = true; // Test only
// booted = false; // Test only
if (booted) ui.drawJpeg("/WU.jpg", 0, 10); // May have already drawn this but it does not take long
else tft.drawCircle(22, 22, 16, TFT_DARKGREY); // Outer ring - optional
if (booted) drawProgress(20, "Updating time...");
else fillSegment(22, 22, 0, (int) (20 * 3.6), 16, TFT_NAVY);
timeClient.updateTime();
if (booted) drawProgress(50, "Updating conditions...");
else fillSegment(22, 22, 0, (int) (50 * 3.6), 16, TFT_NAVY);
wunderground.updateConditions(WUNDERGRROUND_API_KEY, WUNDERGRROUND_LANGUAGE, WUNDERGROUND_COUNTRY, WUNDERGROUND_CITY);
if (booted) drawProgress(70, "Updating forecasts...");
else fillSegment(22, 22, 0, (int) (70 * 3.6), 16, TFT_NAVY);
wunderground.updateForecast(WUNDERGRROUND_API_KEY, WUNDERGRROUND_LANGUAGE, WUNDERGROUND_COUNTRY, WUNDERGROUND_CITY);
if (booted) drawProgress(90, "Updating astronomy...");
else fillSegment(22, 22, 0, (int) (90 * 3.6), 16, TFT_NAVY);
wunderground.updateAstronomy(WUNDERGRROUND_API_KEY, WUNDERGRROUND_LANGUAGE, WUNDERGROUND_COUNTRY, WUNDERGROUND_CITY);
// lastUpdate = timeClient.getFormattedTime();
// readyForWeatherUpdate = false;
if (booted) drawProgress(100, "Done...");
else fillSegment(22, 22, 0, 360, 16, TFT_NAVY);
if (booted) delay(2000);
if (booted) tft.fillScreen(TFT_BLACK);
else fillSegment(22, 22, 0, 360, 22, TFT_BLACK);
//tft.fillScreen(TFT_CYAN); // For text padding and update graphics over-write checking only
drawTime();
drawCurrentWeather();
drawForecast();
drawAstronomy();
//if (booted) screenshotToConsole(); // No supporting function in this sketch, documentation support only!
booted = false;
}
// Progress bar helper
void drawProgress(uint8_t percentage, String text) {
tft.setFreeFont(&ArialRoundedMTBold_14);
tft.setTextDatum(BC_DATUM);
tft.setTextColor(TFT_ORANGE, TFT_BLACK);
tft.setTextPadding(240);
tft.drawString(text, 120, 220);
ui.drawProgressBar(10, 225, 240 - 20, 15, percentage, TFT_WHITE, TFT_BLUE);
tft.setTextPadding(0);
}
// draws the clock
void drawTime() {
tft.setFreeFont(&ArialRoundedMTBold_14);
String date = wunderground.getDate();
tft.setTextDatum(BC_DATUM);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.setTextPadding(tft.textWidth(" Ddd, 44 Mmm 4444 ")); // String width + margin
tft.drawString(date, 120, 14);
tft.setFreeFont(&ArialRoundedMTBold_36);
String timeNow = timeClient.getHours() + ":" + timeClient.getMinutes();
tft.setTextDatum(BC_DATUM);
tft.setTextColor(TFT_YELLOW, TFT_BLACK);
tft.setTextPadding(tft.textWidth(" 44:44 ")); // String width + margin
tft.drawString(timeNow, 120, 50);
drawSeparator(52);
tft.setTextPadding(0);
}
// draws current weather information
void drawCurrentWeather() {
// Weather Icon
String weatherIcon = getMeteoconIcon(wunderground.getTodayIcon());
//uint32_t dt = millis();
ui.drawBmp(weatherIcon + ".bmp", 0, 59);
//Serial.print("Icon draw time = "); Serial.println(millis()-dt);
// Weather Text
String weatherText = wunderground.getWeatherText();
//weatherText = "Heavy Thunderstorms with Small Hail"; // Test line splitting with longest(?) string
tft.setFreeFont(&ArialRoundedMTBold_14);
tft.setTextDatum(BR_DATUM);
tft.setTextColor(TFT_ORANGE, TFT_BLACK);
int splitPoint = 0;
int xpos = 230;
splitPoint = splitIndex(weatherText);
if (splitPoint > 16) xpos = 235;
tft.setTextPadding(tft.textWidth("Heavy Thunderstorms")); // Max anticipated string width
if (splitPoint) tft.drawString(weatherText.substring(0, splitPoint), xpos, 72);
tft.setTextPadding(tft.textWidth(" with Small Hail")); // Max anticipated string width + margin
tft.drawString(weatherText.substring(splitPoint), xpos, 87);
tft.setFreeFont(&ArialRoundedMTBold_36);
tft.setTextDatum(TR_DATUM);
tft.setTextColor(TFT_ORANGE, TFT_BLACK);
// Font ASCII code 96 (0x60) modified to make "`" a degree symbol
tft.setTextPadding(tft.textWidth("-88`")); // Max width of vales
weatherText = wunderground.getCurrentTemp();
if (weatherText.indexOf(".")) weatherText = weatherText.substring(0, weatherText.indexOf(".")); // Make it integer temperature
if (weatherText == "") weatherText = "?"; // Handle null return
tft.drawString(weatherText + "`", 221, 100);
tft.setFreeFont(&ArialRoundedMTBold_14);
tft.setTextDatum(TL_DATUM);
tft.setTextPadding(0);
if (IS_METRIC) tft.drawString("C ", 221, 100);
else tft.drawString("F ", 221, 100);
weatherText = wunderground.getWindDir() + " ";
weatherText += String((int)(wunderground.getWindSpeed().toInt() * WIND_SPEED_SCALING)) + WIND_SPEED_UNITS;
tft.setTextPadding(tft.textWidth("Variable 888 mph ")); // Max string length?
tft.drawString(weatherText, 114, 136);
weatherText = wunderground.getWindDir();
int windAngle = 0;
String compassCardinal = "";
switch (weatherText.length()) {
case 1:
compassCardinal = "N E S W "; // Not used, see default below
windAngle = 90 * compassCardinal.indexOf(weatherText) / 2;
break;
case 2:
compassCardinal = "NE SE SW NW";
windAngle = 45 + 90 * compassCardinal.indexOf(weatherText) / 3;
break;
case 3:
compassCardinal = "NNE ENE ESE SSE SSW WSW WNW NNW";
windAngle = 22 + 45 * compassCardinal.indexOf(weatherText) / 4; // 22 should be 22.5 but accuracy is not needed!
break;
default:
if (weatherText == "Variable") windAngle = -1;
else {
// v23456v23456v23456v23456 character ruler
compassCardinal = "North East South West"; // Possible strings
windAngle = 90 * compassCardinal.indexOf(weatherText) / 6;
}
break;
}
tft.fillCircle(128, 110, 23, TFT_BLACK); // Erase old plot, radius + 1 to delete stray pixels
tft.drawCircle(128, 110, 22, TFT_DARKGREY); // Outer ring - optional
if ( windAngle >= 0 ) fillSegment(128, 110, windAngle - 15, 30, 22, TFT_GREEN); // Might replace this with a bigger rotating arrow
tft.drawCircle(128, 110, 6, TFT_RED);
drawSeparator(153);
tft.setTextPadding(0); // Reset padding width to none
}
// draws the three forecast columns
void drawForecast() {
drawForecastDetail(10, 171, 0);
drawForecastDetail(95, 171, 2);
drawForecastDetail(180, 171, 4);
drawSeparator(171 + 69);
}
// helper for the forecast columns
void drawForecastDetail(uint16_t x, uint16_t y, uint8_t dayIndex) {
tft.setFreeFont(&ArialRoundedMTBold_14);
String day = wunderground.getForecastTitle(dayIndex).substring(0, 3);
day.toUpperCase();
tft.setTextDatum(BC_DATUM);
tft.setTextColor(TFT_ORANGE, TFT_BLACK);
tft.setTextPadding(tft.textWidth("WWW"));
tft.drawString(day, x + 25, y);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.setTextPadding(tft.textWidth("-88 -88"));
tft.drawString(wunderground.getForecastHighTemp(dayIndex) + " " + wunderground.getForecastLowTemp(dayIndex), x + 25, y + 14);
String weatherIcon = getMeteoconIcon(wunderground.getForecastIcon(dayIndex));
ui.drawBmp("/mini/" + weatherIcon + ".bmp", x, y + 15);
tft.setTextPadding(0); // Reset padding width to none
}
// draw moonphase and sunrise/set and moonrise/set
void drawAstronomy() {
tft.setFreeFont(&ArialRoundedMTBold_14);
tft.setTextDatum(BC_DATUM);
tft.setTextColor(TFT_ORANGE, TFT_BLACK);
tft.setTextPadding(tft.textWidth(" Waxing Crescent "));
tft.drawString(wunderground.getMoonPhase(), 120, 260 - 2);
int moonAgeImage = 24 * wunderground.getMoonAge().toInt() / 30.0;
ui.drawBmp("/moon" + String(moonAgeImage) + ".bmp", 120 - 30, 260);
tft.setTextDatum(BC_DATUM);
tft.setTextColor(TFT_ORANGE, TFT_BLACK);
tft.setTextPadding(0); // Reset padding width to none
tft.drawString("Sun", 40, 280);
tft.setTextDatum(BR_DATUM);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.setTextPadding(tft.textWidth(" 88:88 "));
int dt = rightOffset(wunderground.getSunriseTime(), ":"); // Draw relative to colon to them aligned
tft.drawString(wunderground.getSunriseTime(), 40 + dt, 300);
dt = rightOffset(wunderground.getSunsetTime(), ":");
tft.drawString(wunderground.getSunsetTime(), 40 + dt, 315);
tft.setTextDatum(BC_DATUM);
tft.setTextColor(TFT_ORANGE, TFT_BLACK);
tft.setTextPadding(0); // Reset padding width to none
tft.drawString("Moon", 200, 280);
tft.setTextDatum(BR_DATUM);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.setTextPadding(tft.textWidth(" 88:88 "));
dt = rightOffset(wunderground.getMoonriseTime(), ":"); // Draw relative to colon to them aligned
tft.drawString(wunderground.getMoonriseTime(), 200 + dt, 300);
dt = rightOffset(wunderground.getMoonsetTime(), ":");
tft.drawString(wunderground.getMoonsetTime(), 200 + dt, 315);
tft.setTextPadding(0); // Reset padding width to none
}
// Helper function, should be part of the weather station library and should disappear soon
String getMeteoconIcon(String iconText) {
if (iconText == "F") return "chanceflurries";
if (iconText == "Q") return "chancerain";
if (iconText == "W") return "chancesleet";
if (iconText == "V") return "chancesnow";
if (iconText == "S") return "chancetstorms";
if (iconText == "B") return "clear";
if (iconText == "Y") return "cloudy";
if (iconText == "F") return "flurries";
if (iconText == "M") return "fog";
if (iconText == "E") return "hazy";
if (iconText == "Y") return "mostlycloudy";
if (iconText == "H") return "mostlysunny";
if (iconText == "H") return "partlycloudy";
if (iconText == "J") return "partlysunny";
if (iconText == "W") return "sleet";
if (iconText == "R") return "rain";
if (iconText == "W") return "snow";
if (iconText == "B") return "sunny";
if (iconText == "0") return "tstorms";
return "unknown";
}
// if you want separators, uncomment the tft-line
void drawSeparator(uint16_t y) {
tft.drawFastHLine(10, y, 240 - 2 * 10, 0x4228);
}
// determine the "space" split point in a long string
int splitIndex(String text)
{
int index = 0;
while ( (text.indexOf(' ', index) >= 0) && ( index <= text.length() / 2 ) ) {
index = text.indexOf(' ', index) + 1;
}
if (index) index--;
return index;
}
// Calculate coord delta from start of text String to start of sub String contained within that text
// Can be used to vertically right align text so for example a colon ":" in the time value is always
// plotted at same point on the screen irrespective of different proportional character widths,
// could also be used to align decimal points for neat formatting
int rightOffset(String text, String sub)
{
int index = text.indexOf(sub);
return tft.textWidth(text.substring(index));
}
// Calculate coord delta from start of text String to start of sub String contained within that text
// Can be used to vertically left align text so for example a colon ":" in the time value is always
// plotted at same point on the screen irrespective of different proportional character widths,
// could also be used to align decimal points for neat formatting
int leftOffset(String text, String sub)
{
int index = text.indexOf(sub);
return tft.textWidth(text.substring(0, index));
}
// Draw a segment of a circle, centred on x,y with defined start_angle and subtended sub_angle
// Angles are defined in a clockwise direction with 0 at top
// Segment has radius r and it is plotted in defined colour
// Can be used for pie charts etc, in this sketch it is used for wind direction
#define DEG2RAD 0.0174532925 // Degrees to Radians conversion factor
#define INC 2 // Minimum segment subtended angle and plotting angle increment (in degrees)
void fillSegment(int x, int y, int start_angle, int sub_angle, int r, unsigned int colour)
{
// Calculate first pair of coordinates for segment start
float sx = cos((start_angle - 90) * DEG2RAD);
float sy = sin((start_angle - 90) * DEG2RAD);
uint16_t x1 = sx * r + x;
uint16_t y1 = sy * r + y;
// Draw colour blocks every INC degrees
for (int i = start_angle; i < start_angle + sub_angle; i += INC) {
// Calculate pair of coordinates for segment end
int x2 = cos((i + 1 - 90) * DEG2RAD) * r + x;
int y2 = sin((i + 1 - 90) * DEG2RAD) * r + y;
tft.fillTriangle(x1, y1, x2, y2, x, y, colour);
// Copy segment end to sgement start for next segment
x1 = x2;
y1 = y2;
}
}