diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 4aa958b..ec0e346 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -11,6 +11,7 @@ "/home/admar/Documents/Arduino/buienradarklok/Claire/include", "/home/admar/Documents/Arduino/buienradarklok/Claire/src", "/home/admar/.platformio/packages/framework-arduinoespressif32/libraries/Ticker/src", + "/home/admar/Documents/Arduino/buienradarklok/Claire/.pio/libdeps/esp32dev/Adafruit NeoPixel", "/home/admar/Documents/Arduino/buienradarklok/Claire/.pio/libdeps/esp32dev/dESPatch", "/home/admar/.platformio/packages/framework-arduinoespressif32/libraries/Update/src", "/home/admar/.platformio/packages/framework-arduinoespressif32/libraries/HTTPClient/src", @@ -250,6 +251,7 @@ "/home/admar/Documents/Arduino/buienradarklok/Claire/include", "/home/admar/Documents/Arduino/buienradarklok/Claire/src", "/home/admar/.platformio/packages/framework-arduinoespressif32/libraries/Ticker/src", + "/home/admar/Documents/Arduino/buienradarklok/Claire/.pio/libdeps/esp32dev/Adafruit NeoPixel", "/home/admar/Documents/Arduino/buienradarklok/Claire/.pio/libdeps/esp32dev/dESPatch", "/home/admar/.platformio/packages/framework-arduinoespressif32/libraries/Update/src", "/home/admar/.platformio/packages/framework-arduinoespressif32/libraries/HTTPClient/src", @@ -505,7 +507,7 @@ "" ], "cStandard": "gnu99", - "cppStandard": "gnu++11", + "cppStandard": "gnu++20", "compilerPath": "/home/admar/.platformio/packages/toolchain-xtensa-esp32/bin/xtensa-esp32-elf-gcc", "compilerArgs": [ "-mlongcalls", diff --git a/include/backend_communication.hpp b/include/backend_communication.hpp index 45c14bc..322b73c 100644 --- a/include/backend_communication.hpp +++ b/include/backend_communication.hpp @@ -1,5 +1,5 @@ -#ifndef __BACKEND_HPP__ -#define __BACKEND_HPP__ +#ifndef __BACKEND_COMMUNICATION_HPP__ +#define __BACKEND_COMMUNICATION_HPP__ #include #include @@ -14,13 +14,16 @@ class BackendCommunication { public: // Public methods - bool parseJsonConfig(String& payload); bool getConfig(); bool getWeatherData(); private: // Private methods + bool parseConfig(String& p_payload); + + bool parseWeatherData(String& p_payload); + String urlencode(String str); public: @@ -35,4 +38,4 @@ class BackendCommunication { WifiHandler& m_wifi_handler; }; -#endif // __BACKEND_HPP__ \ No newline at end of file +#endif // __BACKEND_COMMUNICATION_HPP__ \ No newline at end of file diff --git a/include/display.hpp b/include/display.hpp new file mode 100644 index 0000000..6e844e6 --- /dev/null +++ b/include/display.hpp @@ -0,0 +1,48 @@ +#ifndef __DISPLAY_HPP__ +#define __DISPLAY_HPP__ + +#include +#include +#include + +class Display { + public: + // Constructor + Display(); + + public: + // Public methods + void setPollenPlusAQIForecastLeds(uint32_t p_current_time, ArduinoJson::V710PB22::JsonArray p_aqi, + ArduinoJson::V710PB22::JsonArray p_pollen); + void setPrecipitationForecastLeds(uint32_t p_current_time, ArduinoJson::V710PB22::JsonArray p_precipitation); + void setUVIForecastLed(uint32_t p_current_time, ArduinoJson::V710PB22::JsonArray p_uvi); + void setLocalAirQualityLed(); + void setWifiStatusLed(bool p_enable); + + private: + // Private methods + float findMaxValueInTwelveHourInterval(uint32_t p_time_stamp, ArduinoJson::V710PB22::JsonArray p_array); + std::vector mergeJsonArraysFromTimestamp(uint32_t p_time_stamp, ArduinoJson::V710PB22::JsonArray p_array_a, + ArduinoJson::V710PB22::JsonArray p_array_b); + std::tuple mapValueToColorRGB(float p_value); + std::tuple mapValueToColorRGBW(float p_value); + void setPixelToColorMappedValue(uint8_t index, float p_value); + + private: + // Private static members + constexpr static uint16_t m_NUM_LEDS_PROTO_V1 = 29U; + constexpr static int16_t m_DATA_PIN_PROTO_V1 = 14; + + private: + // Private members + Adafruit_NeoPixel m_leds; + std::array m_precipitation_forecast_map; + std::array m_air_quality_forecast_map; + uint8_t m_max_pollen_map; + uint8_t m_max_air_qualilty_map; + uint8_t m_uvi_map; + uint8_t m_local_air_quality_map; + uint8_t m_wifi_status_map; +}; + +#endif // __DISPLAY_HPP__ \ No newline at end of file diff --git a/include/wifi_handler.hpp b/include/wifi_handler.hpp index bac7c12..b12f8a7 100644 --- a/include/wifi_handler.hpp +++ b/include/wifi_handler.hpp @@ -3,10 +3,12 @@ #include // https://github.com/admarschoonen/WiFiManager +#include "display.hpp" + class WifiHandler { public: // Constructor - WifiHandler(uint8_t p_led_pin); + WifiHandler(uint8_t p_led_pin, Display p_display); public: // Public static methods @@ -28,6 +30,7 @@ class WifiHandler { private: // Private objects WiFiManager m_wifiManager; + Display& m_display; }; #endif // __WIFIHANDLER_HPP__ \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 621d03b..cea889a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -12,7 +12,10 @@ platform = espressif32 board = esp32dev framework = arduino +build_unflags = -std=gnu++11 +build_flags = -std=gnu++2a lib_deps = https://github.com/admarschoonen/WIFIMANAGER-ESP32.git#v0.100.0 https://github.com/admarschoonen/dESPatch.git#v0.9.5 - bblanchon/ArduinoJson @ ^7.1.0 + bblanchon/ArduinoJson@^7.1.0 + adafruit/Adafruit NeoPixel@^1.12.3 diff --git a/src/backend_communication.cpp b/src/backend_communication.cpp index d15084d..9c4ba1c 100644 --- a/src/backend_communication.cpp +++ b/src/backend_communication.cpp @@ -12,27 +12,6 @@ JsonDocument weatherData; BackendCommunication::BackendCommunication(WifiHandler& p_wifi_handler) : m_wifi_handler(p_wifi_handler) { return; } -bool BackendCommunication::parseJsonConfig(String& payload) { - bool isSuccess = false; - - m_config.clear(); - - DeserializationError error = deserializeJson(m_config, payload); - - if (error) { - Serial.print(String(millis()) + " DeserializeJson() failed: " + String(error.f_str())); - } else { - if (m_config["config version"] != "0.0.1") { - Serial.println(String(millis()) + " Unsupported config version"); - } else { - m_is_config_valid = true; - isSuccess = true; - } - } - - return isSuccess; -} - bool BackendCommunication::getConfig() { String mac = m_wifi_handler.getMac(false); String url = "https://target.luon.net/~admar/Claire/" + urlencode(mac) + "/config.json"; @@ -54,11 +33,10 @@ bool BackendCommunication::getConfig() { if (httpCode == 200) { // Content is in payload payload = http.getString(); - isSuccess = parseJsonConfig(payload); + isSuccess = parseConfig(payload); if (isSuccess) { configTimestamp = http.header("Last-Modified"); - Serial.println(String(millis()) + " ConfigTimestamp: " + configTimestamp); } } else if (httpCode == 304) { // Content did not change @@ -84,7 +62,7 @@ bool BackendCommunication::getWeatherData() { if (httpCode > 0) { payload = http.getString(); - // isSuccess = parseJsonConfig(payload); + isSuccess = parseWeatherData(payload); } else { Serial.println(String(millis()) + " Got http code " + httpCode); } @@ -93,6 +71,44 @@ bool BackendCommunication::getWeatherData() { return isSuccess; } +bool BackendCommunication::parseConfig(String& payload) { + bool isSuccess = false; + + m_config.clear(); + + DeserializationError error = deserializeJson(m_config, payload); + + if (error) { + Serial.print(String(millis()) + " DeserializeJson() failed: " + String(error.f_str())); + } else { + if (m_config["config version"] != "0.0.1") { + Serial.println(String(millis()) + " Unsupported config version"); + } else { + m_is_config_valid = true; + isSuccess = true; + } + } + + return isSuccess; +} + +bool BackendCommunication::parseWeatherData(String& payload) { + bool isSuccess = false; + + m_weather_data.clear(); + + DeserializationError error = deserializeJson(m_weather_data, payload); + + if (error) { + Serial.print(String(millis()) + " DeserializeJson() failed: " + String(error.f_str())); + } else { + m_is_config_valid = true; + isSuccess = true; + } + + return isSuccess; +} + String BackendCommunication::urlencode(String str) { // From https://circuits4you.com/2019/03/21/esp8266-url-encode-decode-example/ String encodedString = ""; diff --git a/src/display.cpp b/src/display.cpp new file mode 100644 index 0000000..4c4511d --- /dev/null +++ b/src/display.cpp @@ -0,0 +1,185 @@ +#include "../include/display.hpp" + +Display::Display() { + // m_leds = Adafruit_NeoPixel(m_NUM_LEDS_PROTO_V1, m_DATA_PIN_PROTO_V1, NEO_RGBW + NEO_KHZ800); + + m_precipitation_forecast_map = {11, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + m_air_quality_forecast_map = {23, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22}; + m_max_pollen_map = 26; + m_max_air_qualilty_map = 24; + m_uvi_map = 27; + m_local_air_quality_map = 25; + m_wifi_status_map = 28; + return; +} + +void Display::setPollenPlusAQIForecastLeds(uint32_t p_current_time, ArduinoJson::V710PB22::JsonArray p_aqi, + ArduinoJson::V710PB22::JsonArray p_pollen) { + uint32_t first_aqi_time = p_pollen[0]["aqi"].as(); + uint32_t first_pollen_time = p_pollen[0]["time"].as(); + + std::vector merged_array = {}; + + if (first_aqi_time <= first_pollen_time) { + merged_array = mergeJsonArraysFromTimestamp(p_current_time, p_aqi, p_pollen); + } else { + merged_array = mergeJsonArraysFromTimestamp(p_current_time, p_pollen, p_aqi); + } + + uint32_t index = 0; + for (float value : merged_array) { + setPixelToColorMappedValue(m_air_quality_forecast_map.at(index), value); + index++; + } + + float max_aqi = findMaxValueInTwelveHourInterval(p_current_time, p_aqi); + setPixelToColorMappedValue(m_max_air_qualilty_map, max_aqi); + + float max_pollen = findMaxValueInTwelveHourInterval(p_current_time, p_pollen); + setPixelToColorMappedValue(m_max_pollen_map, max_pollen); +} + +void Display::setPrecipitationForecastLeds(uint32_t p_current_time, ArduinoJson::V710PB22::JsonArray p_precipitation) { + float previous_value = 0.0f; + + uint32_t index = 0U; + for (JsonObject elem : p_precipitation) { + uint32_t time = elem["time"].as(); + float value = elem["value"].as(); + + if ((index == 0U) and (time > p_current_time)) { + setPixelToColorMappedValue(m_precipitation_forecast_map.at(index), previous_value); + index = 1U; + } + + if (index > 0U) { + setPixelToColorMappedValue(m_precipitation_forecast_map.at(index), value); + index += 1U; + } + + previous_value = value; + } +} + +void Display::setUVIForecastLed(uint32_t p_current_time, ArduinoJson::V710PB22::JsonArray p_uvi) { + for (JsonObject elem : p_uvi) { + uint32_t time = elem["time"].as(); + float value = elem["value"].as(); + + if (time > p_current_time) { + setPixelToColorMappedValue(m_uvi_map, value); + break; + } + } +} + +void Display::setLocalAirQualityLed() {} + +void Display::setWifiStatusLed(bool p_enable) {} + +float Display::findMaxValueInTwelveHourInterval(uint32_t p_time_stamp, ArduinoJson::V710PB22::JsonArray p_array) { + float max_value = 0.0f; + + for (auto elem : p_array) { + uint32_t time = elem["time"].as(); + constexpr uint32_t TWELVE_HOURS_IN_SECONDS = 12U * 60U * 60U; + if ((time >= p_time_stamp) and (time < p_time_stamp + TWELVE_HOURS_IN_SECONDS)) { + max_value = std::max(max_value, elem["value"].as()); + } + } + + return max_value; +} + +std::vector Display::mergeJsonArraysFromTimestamp(uint32_t p_time_stamp, + ArduinoJson::V710PB22::JsonArray p_array_a, + ArduinoJson::V710PB22::JsonArray p_array_b) { + // Assumption: timestamp in p_array_a <= p_array_b + + std::vector merged_vector = {}; + size_t start_point_b = 0; + + for (auto elem_a : p_array_a) { + uint32_t time_a = elem_a["time"].as(); + + if (time_a >= p_time_stamp) { + float merged_value = elem_a["value"].as(); + + for (size_t index_b = start_point_b; index_b < p_array_b.size(); index_b++) { + uint32_t time_b = p_array_b[index_b]["time"].as(); + + if (time_b >= time_a) { + float value_b = p_array_b[index_b]["value"].as(); + merged_value = std::max(merged_value, value_b); + merged_vector.push_back(merged_value); + start_point_b = index_b + 1U; + break; + } + } + } + } + + for (size_t index_b = start_point_b; index_b < p_array_b.size(); index_b++) { + float value_b = p_array_b[index_b]["value"].as(); + merged_vector.push_back(value_b); + } + + return merged_vector; +} + +std::tuple Display::mapValueToColorRGB(float p_value) { + if (p_value < 1.0) { + return {0, 0, 0}; // black (good) + } else if (p_value < 2.0) { + return {0, 0, 160}; // blue (good) + } else if (p_value < 3.0) { + return {0, 255, 255}; // cyan (good) + } else if (p_value < 4.0) { + return {255, 255, 255}; // white (mediocre) + } else if (p_value < 5.0) { + return {200, 200, 32}; // light yellow (mediocre) + } else if (p_value < 6.0) { + return {120, 120, 0}; // yellow (mediocre) + } else if (p_value < 7.0) { + return {200, 80, 0}; // orange (inadequate) + } else if (p_value < 8.0) { + return {255, 50, 0}; // red-orange (inadequate) + } else if (p_value < 9.0) { + return {180, 0, 0}; // red (bad) + } else if (p_value < 10.0) { + return {200, 0, 160}; // magenta (bad) + } else { + return {60, 0, 210}; // purple (terrible) + } +} + +std::tuple Display::mapValueToColorRGBW(float p_value) { + if (p_value < 1.0) { + return {0, 0, 0, 0}; // black (good) + } else if (p_value < 2.0) { + return {0, 0, 255, 0}; // blue (good) + } else if (p_value < 3.0) { + return {0, 255, 255, 0}; // cyan (good) + } else if (p_value < 4.0) { + return {255, 255, 255, 0}; // white (mediocre) + } else if (p_value < 5.0) { + return {200, 200, 32, 0}; // light yellow (mediocre) + } else if (p_value < 6.0) { + return {120, 120, 0, 0}; // yellow (mediocre) + } else if (p_value < 7.0) { + return {200, 80, 0, 0}; // orange (inadequate) + } else if (p_value < 8.0) { + return {255, 50, 0, 0}; // red-orange (inadequate) + } else if (p_value < 9.0) { + return {180, 0, 0, 0}; // red (bad) + } else if (p_value < 10.0) { + return {200, 0, 160, 0}; // magenta (bad) + } else { + return {60, 0, 210, 0}; // purple (terrible) + } +} + +void Display::setPixelToColorMappedValue(uint8_t p_index, float p_value) { + auto [red, green, blue, white] = mapValueToColorRGBW(p_value); + m_leds.setPixelColor(p_index, m_leds.Color(red, green, blue, white)); +} diff --git a/src/main.cpp b/src/main.cpp index c95c894..a680931 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,7 @@ #include #include "../include/backend_communication.hpp" +#include "../include/display.hpp" #include "../include/wifi_handler.hpp" #ifndef LED_BUILTIN @@ -14,7 +15,8 @@ #define CONNECT_BUTTON BUTTON_BUILTIN DESPatch dESPatch; -WifiHandler wifi_handler(LED_BUILTIN); +Display display; +WifiHandler wifi_handler(LED_BUILTIN, display); BackendCommunication backend_communication(wifi_handler); @@ -56,7 +58,37 @@ void loop(void) { } else { Serial.println(String(millis()) + " Address: " + String(static_cast(backend_communication.m_config["address"]))); - // getWeatherData(); + + backend_communication.getWeatherData(); + + String text = String(millis()) + " Precipitation:"; + for (JsonObject elem : backend_communication.m_weather_data["precipitation"].as()) { + text += " " + String(elem["value"].as()); + } + Serial.println(text); + + text = String(millis()) + " AQI:"; + for (JsonObject elem : backend_communication.m_weather_data["AQI"].as()) { + text += " " + String(elem["value"].as()); + } + Serial.println(text); + + text = String(millis()) + " Pollen:"; + for (JsonObject elem : backend_communication.m_weather_data["pollen"].as()) { + text += " " + String(elem["value"].as()); + } + Serial.println(text); + text = String(millis()) + " UVI:"; + for (JsonObject elem : backend_communication.m_weather_data["UVI"].as()) { + text += " " + String(elem["value"].as()); + } + Serial.println(text); + + uint32_t time = backend_communication.m_weather_data["time"].as(); + text = String(millis()) + " time: " + std::to_string(time).c_str(); + Serial.println(text); + + // display.setUVIForecastLed(time, backend_communication.m_weather_data["UVI"].as()); } dESPatch.checkForUpdate(true); delay(2000); diff --git a/src/wifi_handler.cpp b/src/wifi_handler.cpp index cf2bbf1..4cf9e70 100644 --- a/src/wifi_handler.cpp +++ b/src/wifi_handler.cpp @@ -5,7 +5,7 @@ static uint8_t g_led_pin; static Ticker g_ticker; -WifiHandler::WifiHandler(uint8_t p_led_pin) { +WifiHandler::WifiHandler(uint8_t p_led_pin, Display p_display) : m_display(p_display) { g_led_pin = p_led_pin; return; }