Release v0.4.0: complete rewrite #1

Merged
admar merged 13 commits from Plus-Claire into main 2024-09-24 21:54:12 +02:00
7 changed files with 165 additions and 102 deletions
Showing only changes of commit 7094d20eca - Show all commits

View file

@ -8,6 +8,10 @@
#include "../include/wifi_handler.hpp" #include "../include/wifi_handler.hpp"
class BackendCommunication { class BackendCommunication {
public:
// Public enums
enum State { ERROR, STILL_VALID, UPDATED };
public: public:
// Constructor // Constructor
BackendCommunication(WifiHandler& p_wifi_handler); BackendCommunication(WifiHandler& p_wifi_handler);
@ -18,6 +22,8 @@ class BackendCommunication {
bool getWeatherData(); bool getWeatherData();
State update();
private: private:
// Private methods // Private methods
bool parseConfig(String& p_payload); bool parseConfig(String& p_payload);
@ -30,8 +36,14 @@ class BackendCommunication {
// Public member objects // Public member objects
JsonDocument m_config; JsonDocument m_config;
bool m_is_config_valid; bool m_is_config_valid;
String m_config_timestamp;
JsonDocument m_weather_data; JsonDocument m_weather_data;
int32_t m_next_call;
private:
// Private static members
constexpr static uint32_t INTERVAL_MS = 60U * 1000U;
private: private:
// Private member objects // Private member objects

View file

@ -31,14 +31,15 @@ class Display {
std::vector<float> mergeJsonArraysFromTimestampHelper(uint32_t p_time_stamp, std::vector<float> mergeJsonArraysFromTimestampHelper(uint32_t p_time_stamp,
ArduinoJson::V710PB22::JsonArray p_array_a, ArduinoJson::V710PB22::JsonArray p_array_a,
ArduinoJson::V710PB22::JsonArray p_array_b); ArduinoJson::V710PB22::JsonArray p_array_b);
float mapPrecipitationToColorIndex(const float p_rain_intensity);
std::tuple<uint8_t, uint8_t, uint8_t> mapValueToColorRGB(float p_value); 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); std::tuple<uint8_t, uint8_t, uint8_t, uint8_t> mapValueToColorRGBW(float p_value);
void setPixelToColorMappedValue(uint8_t index, float p_value); void setPixelToColorMappedValue(uint8_t index, float p_value);
private: private:
// Private static members // Private static members
constexpr static uint16_t m_NUM_LEDS_PROTO_V1 = 29U; constexpr static uint16_t NUM_LEDS_PROTO_V1 = 29U;
constexpr static int16_t m_DATA_PIN_PROTO_V1 = 14; constexpr static int16_t DATA_PIN_PROTO_V1 = 14;
private: private:
// Private members // Private members

View file

@ -8,19 +8,19 @@ class SensorManager {
// Public types // Public types
struct SensorData { struct SensorData {
__typeof__(Bsec::iaq) iaq; __typeof__(Bsec::iaq) iaq;
__typeof__(Bsec::iaqAccuracy) iaqAccuracy; __typeof__(Bsec::iaqAccuracy) iaq_accuracy;
__typeof__(Bsec::staticIaq) staticIaq; __typeof__(Bsec::staticIaq) static_iaq;
__typeof__(Bsec::co2Equivalent) co2Equivalent; __typeof__(Bsec::co2Equivalent) co2_equivalent;
__typeof__(Bsec::breathVocEquivalent) breathVocEquivalent; __typeof__(Bsec::breathVocEquivalent) breath_voc_equivalent;
__typeof__(Bsec::rawTemperature) rawTemperature; __typeof__(Bsec::rawTemperature) raw_temperature;
__typeof__(Bsec::pressure) pressure; __typeof__(Bsec::pressure) pressure;
__typeof__(Bsec::rawHumidity) rawHumidity; __typeof__(Bsec::rawHumidity) raw_humidity;
__typeof__(Bsec::gasResistance) gasResistance; __typeof__(Bsec::gasResistance) gas_resistance;
__typeof__(Bsec::stabStatus) stabStatus; __typeof__(Bsec::stabStatus) stab_status;
__typeof__(Bsec::runInStatus) runInStatus; __typeof__(Bsec::runInStatus) run_in_status;
__typeof__(Bsec::temperature) temperature; __typeof__(Bsec::temperature) temperature;
__typeof__(Bsec::humidity) humidity; __typeof__(Bsec::humidity) humidity;
__typeof__(Bsec::gasPercentage) gasPercentage; __typeof__(Bsec::gasPercentage) gas_percentage;
}; };
public: public:
@ -40,16 +40,15 @@ class SensorManager {
private: private:
// Private static members // Private static members
constexpr static uint16_t m_NUM_LEDS_PROTO_V1 = 29U; constexpr static uint8_t BME_CS = 5U;
constexpr static int16_t m_DATA_PIN_PROTO_V1 = 14; constexpr static uint8_t BSEC_CONFIG_IAQ[] = {
constexpr static uint8_t m_BME_CS = 5U;
constexpr static uint8_t m_BSEC_CONFIG_IAQ[] = {
#include "config/generic_33v_3s_4d/bsec_iaq.txt" #include "config/generic_33v_3s_4d/bsec_iaq.txt"
}; };
private: private:
// Private members // Private members
Bsec m_iaqSensor; Bsec m_iaq_sensor;
uint32_t m_next_call;
}; };
#endif // __SENSOR_MANAGER_HPP__ #endif // __SENSOR_MANAGER_HPP__

View file

@ -3,25 +3,20 @@
#include "../include/backend_communication.hpp" #include "../include/backend_communication.hpp"
#include "../include/wifi_handler.hpp" #include "../include/wifi_handler.hpp"
bool isConfigValid = false;
String configTimestamp = "";
static HTTPClient http;
JsonDocument weatherData;
BackendCommunication::BackendCommunication(WifiHandler& p_wifi_handler) : m_wifi_handler(p_wifi_handler) { return; } BackendCommunication::BackendCommunication(WifiHandler& p_wifi_handler) : m_wifi_handler(p_wifi_handler) { return; }
bool BackendCommunication::getConfig() { bool BackendCommunication::getConfig() {
HTTPClient http;
String mac = m_wifi_handler.getMac(false); String mac = m_wifi_handler.getMac(false);
String url = "https://target.luon.net/~admar/Claire/" + urlencode(mac) + "/config.json"; String url = "https://target.luon.net/~admar/Claire/" + urlencode(mac) + "/config.json";
String payload = ""; String payload = "";
bool isSuccess = false; bool is_success = false;
http.begin(url, root_ca); http.begin(url, root_ca);
if (isConfigValid) { if (m_is_config_valid) {
http.addHeader("If-Modified-Since", configTimestamp); http.addHeader("If-Modified-Since", m_config_timestamp);
} }
const char* headerKeys[] = {"Last-Modified"}; const char* headerKeys[] = {"Last-Modified"};
@ -33,46 +28,69 @@ bool BackendCommunication::getConfig() {
if (httpCode == 200) { if (httpCode == 200) {
// Content is in payload // Content is in payload
payload = http.getString(); payload = http.getString();
isSuccess = parseConfig(payload); is_success = parseConfig(payload);
if (isSuccess) { if (is_success) {
configTimestamp = http.header("Last-Modified"); m_config_timestamp = http.header("Last-Modified");
} }
} else if (httpCode == 304) { } else if (httpCode == 304) {
// Content did not change // Content did not change
isSuccess = true; is_success = true;
} else { } else {
Serial.println(String(millis()) + " Got http code " + httpCode); Serial.println(String(millis()) + " Got http code " + httpCode);
} }
http.end(); http.end();
return isSuccess; return is_success;
} }
bool BackendCommunication::getWeatherData() { bool BackendCommunication::getWeatherData() {
HTTPClient http;
String url = "https://sinoptik.luon.net/forecast?address=" + urlencode(m_config["address"]) + String url = "https://sinoptik.luon.net/forecast?address=" + urlencode(m_config["address"]) +
"&metrics=precipitation&metrics=UVI&metrics=AQI&metrics=pollen"; "&metrics=precipitation&metrics=UVI&metrics=AQI&metrics=pollen";
String payload = ""; String payload = "";
bool isSuccess = false; bool is_success = false;
http.begin(url, root_ca); http.begin(url, root_ca);
int httpCode = http.GET(); int http_code = http.GET();
if (httpCode > 0) { if (http_code > 0) {
payload = http.getString(); payload = http.getString();
isSuccess = parseWeatherData(payload); is_success = parseWeatherData(payload);
} else { } else {
Serial.println(String(millis()) + " Got http code " + httpCode); Serial.println(String(millis()) + " Got http code " + http_code);
} }
http.end(); http.end();
return isSuccess; return is_success;
}
BackendCommunication::State BackendCommunication::update() {
int32_t time = static_cast<int32_t>(millis() & 0x7FFFFFFF);
int32_t time_delta = m_next_call - time;
if (time_delta > 0) {
return STILL_VALID;
}
m_next_call = (time + INTERVAL_MS) & 0x7FFFFFFF;
if (not getConfig()) {
return ERROR;
}
if (not getWeatherData()) {
return ERROR;
}
return UPDATED;
} }
bool BackendCommunication::parseConfig(String& payload) { bool BackendCommunication::parseConfig(String& payload) {
bool isSuccess = false; bool is_success = false;
m_config.clear(); m_config.clear();
@ -85,11 +103,11 @@ bool BackendCommunication::parseConfig(String& payload) {
Serial.println(String(millis()) + " Unsupported config version"); Serial.println(String(millis()) + " Unsupported config version");
} else { } else {
m_is_config_valid = true; m_is_config_valid = true;
isSuccess = true; is_success = true;
} }
} }
return isSuccess; return is_success;
} }
bool BackendCommunication::parseWeatherData(String& payload) { bool BackendCommunication::parseWeatherData(String& payload) {

View file

@ -35,12 +35,12 @@ void Display::setPollenPlusAQIForecastLeds(uint32_t p_current_time, ArduinoJson:
} }
void Display::setPrecipitationForecastLeds(uint32_t p_current_time, ArduinoJson::V710PB22::JsonArray p_precipitation) { void Display::setPrecipitationForecastLeds(uint32_t p_current_time, ArduinoJson::V710PB22::JsonArray p_precipitation) {
float previous_value = 0.0f; float previous_value = mapPrecipitationToColorIndex(0.0f);
uint32_t index = 0U; uint32_t index = 0U;
for (JsonObject elem : p_precipitation) { for (JsonObject elem : p_precipitation) {
uint32_t time = elem["time"].as<uint32_t>(); uint32_t time = elem["time"].as<uint32_t>();
float value = elem["value"].as<float>(); float color_index = mapPrecipitationToColorIndex(elem["value"].as<float>());
if ((index == 0U) and (time > p_current_time)) { if ((index == 0U) and (time > p_current_time)) {
setPixelToColorMappedValue(m_precipitation_forecast_map.at(index), previous_value); setPixelToColorMappedValue(m_precipitation_forecast_map.at(index), previous_value);
@ -48,11 +48,11 @@ void Display::setPrecipitationForecastLeds(uint32_t p_current_time, ArduinoJson:
} }
if (index > 0U) { if (index > 0U) {
setPixelToColorMappedValue(m_precipitation_forecast_map.at(index), value); setPixelToColorMappedValue(m_precipitation_forecast_map.at(index), color_index);
index += 1U; index += 1U;
} }
previous_value = value; previous_value = color_index;
if (index >= m_precipitation_forecast_map.size()) { if (index >= m_precipitation_forecast_map.size()) {
break; break;
@ -134,13 +134,18 @@ void Display::show() {
float Display::findMaxValueInTwelveHourInterval(uint32_t p_time_stamp, ArduinoJson::V710PB22::JsonArray p_array) { float Display::findMaxValueInTwelveHourInterval(uint32_t p_time_stamp, ArduinoJson::V710PB22::JsonArray p_array) {
float max_value = 0.0f; float max_value = 0.0f;
float previous_value = 0.0f;
for (auto elem : p_array) { for (auto elem : p_array) {
uint32_t time = elem["time"].as<uint32_t>(); uint32_t time = elem["time"].as<uint32_t>();
float value = elem["value"].as<float>();
constexpr uint32_t TWELVE_HOURS_IN_SECONDS = 12U * 60U * 60U; constexpr uint32_t TWELVE_HOURS_IN_SECONDS = 12U * 60U * 60U;
if ((time >= p_time_stamp) and (time < p_time_stamp + TWELVE_HOURS_IN_SECONDS)) { if ((time >= p_time_stamp) and (time < p_time_stamp + TWELVE_HOURS_IN_SECONDS)) {
max_value = std::max(max_value, elem["value"].as<float>()); max_value = std::max(max_value, previous_value);
} }
previous_value = value;
} }
return max_value; return max_value;
@ -217,6 +222,38 @@ std::vector<float> Display::mergeJsonArraysFromTimestampHelper(uint32_t p_time_s
return merged_vector; return merged_vector;
} }
float Display::mapPrecipitationToColorIndex(const float p_rain_intensity) {
float color_index = 0.0f;
if (p_rain_intensity < 0.1f) {
color_index = 0.0f;
} else if (p_rain_intensity < 0.22f) {
color_index = 1.0f;
} else if (p_rain_intensity < 0.47f) {
color_index = 2.0f;
} else if (p_rain_intensity < 1.0f) {
color_index = 3.0f;
} else if (p_rain_intensity < 2.2f) {
color_index = 4.0f;
} else if (p_rain_intensity < 4.7f) {
color_index = 5.0f;
} else if (p_rain_intensity < 10.0f) {
color_index = 6.0f;
} else if (p_rain_intensity < 22.0f) {
color_index = 7.0f;
} else if (p_rain_intensity < 47.0f) {
color_index = 8.0f;
} else if (p_rain_intensity < 100.0f) {
color_index = 9.0f;
} else if (p_rain_intensity < 220.0f) {
color_index = 10.0f;
} else {
color_index = 11.0f;
}
return color_index;
}
std::tuple<uint8_t, uint8_t, uint8_t> Display::mapValueToColorRGB(float p_value) { std::tuple<uint8_t, uint8_t, uint8_t> Display::mapValueToColorRGB(float p_value) {
if (p_value < 1.0) { if (p_value < 1.0) {
return {0, 0, 0}; // black (good) return {0, 0, 0}; // black (good)

View file

@ -83,16 +83,13 @@ void setup() {
} }
void loop(void) { void loop(void) {
uint32_t time_start = millis(); sensor_manager.read();
display.setLocalAirQualityLed(sensor_manager.data.static_iaq);
backend_communication.getConfig(); if (backend_communication.update() == BackendCommunication::State::ERROR) {
if (not backend_communication.m_is_config_valid) {
display.setErrorLed(true); display.setErrorLed(true);
Serial.println(String("Uptime: " + String(millis())) + ". Error: invalid config"); Serial.println("Uptime: " + String(millis()) + ", backend error");
} else { } else {
backend_communication.getWeatherData();
uint32_t utc_time = backend_communication.m_weather_data["time"].as<JsonInteger>(); uint32_t utc_time = backend_communication.m_weather_data["time"].as<JsonInteger>();
Serial.println("Uptime: " + String(millis()) + ", UTC time: " + String(utc_time)); Serial.println("Uptime: " + String(millis()) + ", UTC time: " + String(utc_time));
@ -104,26 +101,12 @@ void loop(void) {
display.setUVIForecastLed(utc_time, backend_communication.m_weather_data["UVI"].as<JsonArray>()); display.setUVIForecastLed(utc_time, backend_communication.m_weather_data["UVI"].as<JsonArray>());
display.setLocalAirQualityLed(sensor_manager.data.staticIaq);
display.setWifiStatusLed(false); display.setWifiStatusLed(false);
} }
sensor_manager.read();
display.show(); display.show();
dESPatch.checkForUpdate(true); dESPatch.checkForUpdate(true);
uint32_t time_end = millis(); delay(1000U);
uint32_t time_delta = time_end - time_start;
uint32_t delay_time = 0U;
constexpr uint32_t ONE_SECOND_IN_MILLISECONDS = 1000U;
constexpr uint32_t INTERVAL = 60U * ONE_SECOND_IN_MILLISECONDS;
if (time_delta < INTERVAL) {
delay_time = INTERVAL - time_delta;
}
delay(delay_time);
} }

View file

@ -9,18 +9,18 @@ SensorManager::SensorManager() {
bool SensorManager::setup() { bool SensorManager::setup() {
SPI.begin(); SPI.begin();
m_iaqSensor.begin(m_BME_CS, SPI); m_iaq_sensor.begin(BME_CS, SPI);
String output = "BSEC library version " + String(m_iaqSensor.version.major) + "." + String output = "BSEC library version " + String(m_iaq_sensor.version.major) + "." +
String(m_iaqSensor.version.minor) + "." + String(m_iaqSensor.version.major_bugfix) + "." + String(m_iaq_sensor.version.minor) + "." + String(m_iaq_sensor.version.major_bugfix) + "." +
String(m_iaqSensor.version.minor_bugfix); String(m_iaq_sensor.version.minor_bugfix);
Serial.println(output); Serial.println(output);
if (not checkIaqSensorStatus()) { if (not checkIaqSensorStatus()) {
return false; return false;
} }
m_iaqSensor.setConfig(m_BSEC_CONFIG_IAQ); m_iaq_sensor.setConfig(BSEC_CONFIG_IAQ);
m_iaqSensor.setTemperatureOffset(13.5f); m_iaq_sensor.setTemperatureOffset(13.5f);
if (not checkIaqSensorStatus()) { if (not checkIaqSensorStatus()) {
return false; return false;
@ -40,7 +40,7 @@ bool SensorManager::setup() {
BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY, BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY,
BSEC_OUTPUT_GAS_PERCENTAGE}; BSEC_OUTPUT_GAS_PERCENTAGE};
m_iaqSensor.updateSubscription(sensorList, 13, BSEC_SAMPLE_RATE_LP); m_iaq_sensor.updateSubscription(sensorList, 13, BSEC_SAMPLE_RATE_LP);
if (checkIaqSensorStatus()) { if (checkIaqSensorStatus()) {
return false; return false;
@ -50,47 +50,60 @@ bool SensorManager::setup() {
} }
bool SensorManager::read() { bool SensorManager::read() {
if (m_iaqSensor.run()) { // If new data is available uint32_t time = millis();
data.iaq = m_iaqSensor.iaq;
data.iaqAccuracy = m_iaqSensor.iaqAccuracy; if ((m_next_call == 0) or (time < m_next_call)) {
data.staticIaq = m_iaqSensor.staticIaq; return false;
data.co2Equivalent = m_iaqSensor.co2Equivalent; }
data.breathVocEquivalent = m_iaqSensor.breathVocEquivalent;
data.rawTemperature = m_iaqSensor.rawTemperature; bool is_run_result_valid = m_iaq_sensor.run();
data.pressure = m_iaqSensor.pressure; bool is_sensor_status_valid = checkIaqSensorStatus();
data.rawHumidity = m_iaqSensor.rawHumidity;
data.gasResistance = m_iaqSensor.gasResistance; if (is_run_result_valid and is_sensor_status_valid) { // If new data is available
data.stabStatus = m_iaqSensor.stabStatus; data.iaq = m_iaq_sensor.iaq;
data.runInStatus = m_iaqSensor.runInStatus; data.iaq_accuracy = m_iaq_sensor.iaqAccuracy;
data.temperature = m_iaqSensor.temperature; data.static_iaq = m_iaq_sensor.staticIaq;
data.humidity = m_iaqSensor.humidity; data.co2_equivalent = m_iaq_sensor.co2Equivalent;
data.gasPercentage = m_iaqSensor.gasPercentage; data.breath_voc_equivalent = m_iaq_sensor.breathVocEquivalent;
} else { data.raw_temperature = m_iaq_sensor.rawTemperature;
Serial.println("sensor status: " + String(checkIaqSensorStatus())); data.pressure = m_iaq_sensor.pressure;
data.raw_humidity = m_iaq_sensor.rawHumidity;
data.gas_resistance = m_iaq_sensor.gasResistance;
data.stab_status = m_iaq_sensor.stabStatus;
data.run_in_status = m_iaq_sensor.runInStatus;
data.temperature = m_iaq_sensor.temperature;
data.humidity = m_iaq_sensor.humidity;
data.gas_percentage = m_iaq_sensor.gasPercentage;
m_next_call = static_cast<uint32_t>(m_iaq_sensor.nextCall & 0xFFFFFFFF);
Serial.println("Next call: " + String(m_next_call));
return true;
} else {
return false;
} }
return true;
} }
bool SensorManager::checkIaqSensorStatus() { bool SensorManager::checkIaqSensorStatus() {
if (m_iaqSensor.bsecStatus != BSEC_OK) { if (m_iaq_sensor.bsecStatus != BSEC_OK) {
if (m_iaqSensor.bsecStatus < BSEC_OK) { if (m_iaq_sensor.bsecStatus < BSEC_OK) {
Serial.println("BSEC error code: " + String(m_iaqSensor.bsecStatus)); Serial.println("BSEC error code: " + String(m_iaq_sensor.bsecStatus));
return false; return false;
} else { } else {
Serial.println("BSEC warning code: " + String(m_iaqSensor.bsecStatus)); Serial.println("BSEC warning code: " + String(m_iaq_sensor.bsecStatus));
return false; return false;
} }
} }
if (m_iaqSensor.bme68xStatus != BME68X_OK) { if (m_iaq_sensor.bme68xStatus != BME68X_OK) {
if (m_iaqSensor.bme68xStatus < BME68X_OK) { if (m_iaq_sensor.bme68xStatus < BME68X_OK) {
Serial.println("BME68X error code: " + String(m_iaqSensor.bme68xStatus)); Serial.println("BME68X error code: " + String(m_iaq_sensor.bme68xStatus));
return false; return false;
} else { } else {
Serial.println("BME68X warning code: " + String(m_iaqSensor.bme68xStatus)); Serial.println("BME68X warning code: " + String(m_iaq_sensor.bme68xStatus));
return false; return false;
} }