/* * Copyright (C) 2019 Max Regan * * 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. */ #include #include #include #include #include #include "src/weather-pixel-icons-c/images.h" #define COLOR_DEPTH 24 const uint8_t MatrixWidth = 32; const uint8_t MatrixHeight = 16; const uint8_t RefreshDepth = 36; const uint8_t DmaBufferRows = 2; const uint8_t PanelType = SMARTMATRIX_HUB75_16ROW_MOD8SCAN; const uint8_t MatrixOptions = (SMARTMATRIX_OPTIONS_NONE); const uint8_t LayerOptions = (SM_INDEXED_OPTIONS_NONE); SMARTMATRIX_ALLOCATE_BUFFERS(matrix, MatrixWidth, MatrixHeight, RefreshDepth, DmaBufferRows, PanelType, MatrixOptions); SMARTMATRIX_ALLOCATE_BACKGROUND_LAYER(layer, MatrixWidth, MatrixHeight, COLOR_DEPTH, LayerOptions); const int defaultBrightness = (8 * 255) / 100; // dim: 10% brightness, still very bright const rgb24 defaultBackgroundColor = {0, 0, 0}; const rgb24 defaultTextColor = {255, 255, 255}; // Use Seattle by default #define DEFAULT_LATITUDE "47.6062" #define DEFAULT_LONGITUDE "-122.3321" static uint32_t display_timestamp = 0; WiFiManagerParameter latitude_param("latitude", "Latitide", DEFAULT_LATITUDE, 16); WiFiManagerParameter longitude_param("longitude", "Longitude", DEFAULT_LONGITUDE, 16); WiFiManagerParameter darksky_api_token_param("api_token", "DarkSky API Token", "", 32); void setup() { // initialize the digital pin as an output. Serial.begin(115200); matrix.addLayer(&layer); matrix.begin(); matrix.setBrightness(defaultBrightness); layer.setFont(font3x5); autoConnectWifi(); } void autoConnectWifi() { WiFiManager wifi; Serial.println("Setting up WiFi"); wifi.resetSettings(); wifi.addParameter(&latitude_param); wifi.addParameter(&longitude_param); wifi.addParameter(&darksky_api_token_param); wifi.autoConnect(); Serial.println("WiFi Done"); } static const uint64_t weather_update_millis = 1000 * 60 * 10; // 10 mins static const char * darksky_url_fmt = "https://api.darksky.net/forecast/%s/%s,%s?exclude=minutely,hourly,alerts,flags&units=us"; static String weather_desc = ""; static bool is_weather_valid = false; static int weather_code = 0; static uint64_t weather_timestamp = 0; static uint64_t weather_walltime = 0; static uint64_t sunrise_walltime = 0; static uint64_t sunset_walltime = 0; static HTTPClient http; enum weather { NONE, CLEAR, RAIN, SNOW, SLEET, WIND, FOG, CLOUDY, PARTLY_CLOUDY, }; static float temp_min = 0.0; static float temp_max = 0.0; static float temp_now = 0.0; static enum weather weather_value = NONE; void update_weather() { uint64_t now = millis(); if (weather_timestamp != 0 && weather_timestamp + weather_update_millis > now) { return; } Serial.println("Updating weather"); weather_value = NONE; static char url[256]; url[0] = '\0'; snprintf(url, sizeof(url), darksky_url_fmt, darksky_api_token_param.getValue(), latitude_param.getValue(), longitude_param.getValue()); Serial.println(url); http.begin(url); int resp_code = http.GET(); if (resp_code < 0) { Serial.println("Failed to get weather"); return; } Serial.println("Got weather"); String json_resp = http.getString(); http.end(); DynamicJsonDocument json(16000); DeserializationError error = deserializeJson(json, json_resp); if (error) { Serial.println(error.c_str()); return; } Serial.println("Weather parsed"); weather_timestamp = millis(); temp_min = json["daily"]["data"][0]["temperatureLow"]; temp_max = json["daily"]["data"][0]["temperatureHigh"]; temp_now = json["currently"]["temperature"]; weather_walltime = json["currently"]["time"]; sunrise_walltime = json["daily"]["data"][0]["sunriseTime"]; sunset_walltime = json["daily"]["data"][0]["sunsetTime"]; Serial.println(String("Temp now: ") + temp_now); Serial.println(String("Temp low: ") + temp_min); Serial.println(String("Temp high: ") + temp_max); weather_code = NONE; const char *icon = json["currently"]["icon"]; Serial.println(String("Weather: ") + icon); if (!strcmp(icon, "clear-day")) { weather_code = CLEAR; } else if (!strcmp(icon, "clear-night")) { weather_code = CLEAR; } else if (!strcmp(icon, "rain")) { weather_code = RAIN; } else if (!strcmp(icon, "snow")) { weather_code = SNOW; } else if (!strcmp(icon, "sleet")) { weather_code = SLEET; } else if (!strcmp(icon, "wind")) { weather_code = WIND; } else if (!strcmp(icon, "fog")) { weather_code = FOG; } else if (!strcmp(icon, "cloudy")) { weather_code = CLOUDY; } else if (!strcmp(icon, "partly-cloudy-day")) { weather_code = PARTLY_CLOUDY; } else if (!strcmp(icon, "partly-cloudy-night")) { weather_code = PARTLY_CLOUDY; } } void draw_bitmap(const struct gimp_image *image, unsigned int x, unsigned int y) { for (unsigned int i = 0; i < image->height; i++) { for (unsigned int j = 0; j < image->width; j++) { SM_RGB pixel = { ~(image->data[(i * image->width + j) * 3 + 0]), ~(image->data[(i * image->width + j) * 3 + 1]), ~(image->data[(i * image->width + j) * 3 + 2]) }; layer.drawPixel(x + j, y + i, pixel); } } } static const struct gimp_image *images[] = { &cloud, &cloud_moon, &clouds, &cloud_sun, &cloud_wind, &cloud_wind_moon, &cloud_wind_sun, &lightning, &moon, &rain0, &rain0_sun, &rain1, &rain1_moon, &rain1_sun, &rain2, &rain_lightning, &rain_snow, &snow, &snow_moon, &snow_sun, &sun, }; static const struct gimp_image *day_images[] = { [NONE] = nullptr, [CLEAR] = &sun, [RAIN] = &rain0, [SNOW] = &snow, [SLEET] = &rain_snow, [WIND] = nullptr, //FIXME: deleted the filed? [FOG] = &clouds, [CLOUDY] = &clouds, [PARTLY_CLOUDY] = &cloud_sun, }; static const struct gimp_image *night_images[] = { [NONE] = nullptr, [CLEAR] = &moon, [RAIN] = &rain1_moon, [SNOW] = &snow_moon, [SLEET] = &rain1_moon, [WIND] = nullptr, //FIXME: deleted the filed? [FOG] = &clouds, [CLOUDY] = &clouds, [PARTLY_CLOUDY] = &cloud_moon, }; void update_frame() { layer.swapBuffers(); } void display_weather() { static uint64_t redraw_timestamp = 0; if (redraw_timestamp >= weather_timestamp) { return; } layer.fillScreen(defaultBackgroundColor); const struct gimp_image *icon = nullptr; Serial.print("Weather code:"); Serial.println(weather_code); bool is_night = (weather_walltime < sunrise_walltime) || (weather_walltime > sunset_walltime); Serial.print("It is "); Serial.println(is_night ? "night" : "day"); if (is_night) { icon = night_images[weather_code]; } else { icon = day_images[weather_code]; } if (icon) { Serial.println("Drawing weather icon."); draw_bitmap(icon, 0, 0); } else { Serial.println("No weather icon."); } char temp_text[32] = { 0 }; snprintf(temp_text, sizeof(temp_text), "%d", (int) temp_now); layer.setFont(font5x7); layer.drawString(19, 4, defaultTextColor, temp_text); layer.swapBuffers(); redraw_timestamp = millis(); } void loop() { update_weather(); display_weather(); }