/**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 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 Barometric pressure added */ #define SERIAL_MESSAGES #include #include #include // 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 #include #include #include #include // Helps with connecting to internet #include // check settings.h for adapting to your needs #include "settings.h" #include #include #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() { #ifdef SERIAL_MESSAGES Serial.begin(250000); #endif 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 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.setTextDatum(BC_DATUM); tft.setTextPadding(240); // Pad next drawString() text to full width to over-write old text tft.drawString(" ", 120, 200); // Clear line above using set padding width tft.drawString("Fetching weather data...", 120, 200); //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, 18, 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(); 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_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, 53); 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, 16); drawSeparator(54); 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); //tft.drawString(wunderground.getPressure(), 180, 30); weatherText = ""; //wunderground.getWindDir() + " "; weatherText += String((int)(wunderground.getWindSpeed().toInt() * WIND_SPEED_SCALING)) + WIND_SPEED_UNITS; tft.setTextDatum(TC_DATUM); tft.setTextPadding(tft.textWidth(" 888 mph")); // Max string length? tft.drawString(weatherText, 128, 136); weatherText = wunderground.getPressure(); tft.setTextDatum(TR_DATUM); tft.setTextPadding(tft.textWidth(" 8888mb")); // Max string length? tft.drawString(weatherText, 230, 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.setTextDatum(TL_DATUM); // Reset datum to normal 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; } }