Release v0.4.0: complete rewrite #1
9 changed files with 326 additions and 34 deletions
4
.vscode/c_cpp_properties.json
vendored
4
.vscode/c_cpp_properties.json
vendored
|
@ -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",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#ifndef __BACKEND_HPP__
|
||||
#define __BACKEND_HPP__
|
||||
#ifndef __BACKEND_COMMUNICATION_HPP__
|
||||
#define __BACKEND_COMMUNICATION_HPP__
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
|
@ -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__
|
||||
#endif // __BACKEND_COMMUNICATION_HPP__
|
48
include/display.hpp
Normal file
48
include/display.hpp
Normal file
|
@ -0,0 +1,48 @@
|
|||
#ifndef __DISPLAY_HPP__
|
||||
#define __DISPLAY_HPP__
|
||||
|
||||
#include <Adafruit_NeoPixel.h>
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
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<float> mergeJsonArraysFromTimestamp(uint32_t p_time_stamp, ArduinoJson::V710PB22::JsonArray p_array_a,
|
||||
ArduinoJson::V710PB22::JsonArray p_array_b);
|
||||
std::tuple<uint8_t, uint8_t, uint8_t> mapValueToColorRGB(float p_value);
|
||||
std::tuple<uint8_t, uint8_t, uint8_t, uint8_t> 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<uint8_t, 12U> m_precipitation_forecast_map;
|
||||
std::array<uint8_t, 12U> 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__
|
|
@ -3,10 +3,12 @@
|
|||
|
||||
#include <WiFiManager-esp32.h> // 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__
|
|
@ -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
|
||||
adafruit/Adafruit NeoPixel@^1.12.3
|
||||
|
|
|
@ -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 = "";
|
||||
|
|
185
src/display.cpp
Normal file
185
src/display.cpp
Normal file
|
@ -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>();
|
||||
uint32_t first_pollen_time = p_pollen[0]["time"].as<uint32_t>();
|
||||
|
||||
std::vector<float> 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<uint32_t>();
|
||||
float value = elem["value"].as<float>();
|
||||
|
||||
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<uint32_t>();
|
||||
float value = elem["value"].as<float>();
|
||||
|
||||
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<uint32_t>();
|
||||
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<float>());
|
||||
}
|
||||
}
|
||||
|
||||
return max_value;
|
||||
}
|
||||
|
||||
std::vector<float> 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<float> merged_vector = {};
|
||||
size_t start_point_b = 0;
|
||||
|
||||
for (auto elem_a : p_array_a) {
|
||||
uint32_t time_a = elem_a["time"].as<uint32_t>();
|
||||
|
||||
if (time_a >= p_time_stamp) {
|
||||
float merged_value = elem_a["value"].as<float>();
|
||||
|
||||
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<uint32_t>();
|
||||
|
||||
if (time_b >= time_a) {
|
||||
float value_b = p_array_b[index_b]["value"].as<float>();
|
||||
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<float>();
|
||||
merged_vector.push_back(value_b);
|
||||
}
|
||||
|
||||
return merged_vector;
|
||||
}
|
||||
|
||||
std::tuple<uint8_t, uint8_t, uint8_t> 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<uint8_t, uint8_t, uint8_t, uint8_t> 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));
|
||||
}
|
36
src/main.cpp
36
src/main.cpp
|
@ -1,6 +1,7 @@
|
|||
#include <dESPatch.h>
|
||||
|
||||
#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<const char*>(backend_communication.m_config["address"])));
|
||||
// getWeatherData();
|
||||
|
||||
backend_communication.getWeatherData();
|
||||
|
||||
String text = String(millis()) + " Precipitation:";
|
||||
for (JsonObject elem : backend_communication.m_weather_data["precipitation"].as<JsonArray>()) {
|
||||
text += " " + String(elem["value"].as<float>());
|
||||
}
|
||||
Serial.println(text);
|
||||
|
||||
text = String(millis()) + " AQI:";
|
||||
for (JsonObject elem : backend_communication.m_weather_data["AQI"].as<JsonArray>()) {
|
||||
text += " " + String(elem["value"].as<float>());
|
||||
}
|
||||
Serial.println(text);
|
||||
|
||||
text = String(millis()) + " Pollen:";
|
||||
for (JsonObject elem : backend_communication.m_weather_data["pollen"].as<JsonArray>()) {
|
||||
text += " " + String(elem["value"].as<float>());
|
||||
}
|
||||
Serial.println(text);
|
||||
text = String(millis()) + " UVI:";
|
||||
for (JsonObject elem : backend_communication.m_weather_data["UVI"].as<JsonArray>()) {
|
||||
text += " " + String(elem["value"].as<float>());
|
||||
}
|
||||
Serial.println(text);
|
||||
|
||||
uint32_t time = backend_communication.m_weather_data["time"].as<JsonInteger>();
|
||||
text = String(millis()) + " time: " + std::to_string(time).c_str();
|
||||
Serial.println(text);
|
||||
|
||||
// display.setUVIForecastLed(time, backend_communication.m_weather_data["UVI"].as<JsonArray>());
|
||||
}
|
||||
dESPatch.checkForUpdate(true);
|
||||
delay(2000);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue