diff --git a/src/AgApiClient.cpp b/src/AgApiClient.cpp new file mode 100644 index 00000000..ef50092d --- /dev/null +++ b/src/AgApiClient.cpp @@ -0,0 +1,129 @@ +#include "AgApiClient.h" +#include + +void AgApiClient::printLog(String log) { + debugLog.printf("[AgApiClient] %s\r\n", log.c_str()); + // Serial.printf("[AgApiClient] %s\r\n", log.c_str()); +} + +AgApiClient::AgApiClient(Stream &debug, AgConfigure &config) + : debugLog(debug), config(config) {} + +AgApiClient::~AgApiClient() {} + +/** + * @brief Initialize the API client + * + */ +void AgApiClient::begin(void) { + getConfigFailed = false; + postToServerFailed = false; + printLog("Begin"); +} + +/** + * @brief Get configuration from AirGradient cloud + * + * @param deviceId Device ID + * @return true Success + * @return false Failure + */ +bool AgApiClient::fetchServerConfiguration(String deviceId) { + if (config.getConfigurationControl() == + ConfigurationControl::ConfigurationControlLocal) { + printLog("Ignore fetch server configuratoin"); + + // Clear server configuration failed flag, cause it's ignore but not + // really failed + getConfigFailed = false; + return false; + } + + String uri = "http://hw.airgradient.com/sensors/airgradient:" + deviceId + + "/one/config"; + + /** Init http client */ + HTTPClient client; + if (client.begin(uri) == false) { + getConfigFailed = true; + return false; + } + + /** Get data */ + int retCode = client.GET(); + if (retCode != 200) { + client.end(); + getConfigFailed = true; + return false; + } + + /** clear failed */ + getConfigFailed = false; + + /** Get response string */ + String respContent = client.getString(); + client.end(); + + printLog("Server configuration: " + respContent); + + /** Parse configuration and return result */ + return config.parse(respContent, false); +} + +/** + * @brief Post data to AirGradient cloud + * + * @param deviceId Device Id + * @param data String JSON + * @return true Success + * @return false Failure + */ +bool AgApiClient::postToServer(String deviceId, String data) { + if (config.isPostDataToAirGradient() == false) { + printLog("Ignore post data to server"); + return true; + } + + if (WiFi.isConnected() == false) { + return false; + } + + String uri = + "http://hw.airgradient.com/sensors/airgradient:" + deviceId + "/measures"; + printLog("Post uri: " + uri); + printLog("Post data: " + data); + + WiFiClient wifiClient; + HTTPClient client; + if (client.begin(wifiClient, uri.c_str()) == false) { + return false; + } + client.addHeader("content-type", "application/json"); + int retCode = client.POST(data); + client.end(); + + if ((retCode == 200) || (retCode == 429)) { + postToServerFailed = false; + return true; + } else { + printLog("Post response failed code: " + String(retCode)); + } + postToServerFailed = true; + return false; +} + +/** + * @brief Get failed status when get configuration from AirGradient cloud + * + * @return true Success + * @return false Failure + */ +bool AgApiClient::isFetchConfigureFailed(void) { return getConfigFailed; } + +/** + * @brief Get failed status when post data to AirGradient cloud + * + * @return true Success + * @return false Failure + */ +bool AgApiClient::isPostToServerFailed(void) { return postToServerFailed; } diff --git a/src/AgApiClient.h b/src/AgApiClient.h new file mode 100644 index 00000000..d50a8fad --- /dev/null +++ b/src/AgApiClient.h @@ -0,0 +1,38 @@ +/** + * @file AgApiClient.h + * @brief HTTP client connect post data to Aigradient cloud. + * @version 0.1 + * @date 2024-Apr-02 + * + * @copyright Copyright (c) 2024 + * + */ + +#ifndef _AG_API_CLIENT_H_ +#define _AG_API_CLIENT_H_ + +#include "AgConfigure.h" +#include + +class AgApiClient { +private: + Stream &debugLog; + AgConfigure &config; + + bool getConfigFailed;; + bool postToServerFailed; + + void printLog(String log); + +public: + AgApiClient(Stream &stream, AgConfigure &config); + ~AgApiClient(); + + void begin(void); + bool fetchServerConfiguration(String deviceId); + bool postToServer(String deviceId, String data); + bool isFetchConfigureFailed(void); + bool isPostToServerFailed(void); +}; + +#endif /** _AG_API_CLIENT_H_ */ diff --git a/src/AgConfigure.cpp b/src/AgConfigure.cpp new file mode 100644 index 00000000..c2f93686 --- /dev/null +++ b/src/AgConfigure.cpp @@ -0,0 +1,452 @@ +#include "AgConfigure.h" +#include "EEPROM.h" + +const char *CONFIGURATION_CONTROL_NAME[] = { + [ConfigurationControlLocal] = "local", + [ConfigurationControlCloud] = "cloud", + [ConfigurationControlBoth] = "both"}; + +void AgConfigure::printLog(String log) { + debugLog.printf("[AgConfigure] %s\r\n", log.c_str()); +} + +String AgConfigure::getLedBarModeName(LedBarMode mode) { + LedBarMode ledBarMode = mode; + if (ledBarMode == LedBarModeOff) { + return String("off"); + } else if (ledBarMode == LedBarModePm) { + return String("pm"); + } else if (ledBarMode == LedBarModeCO2) { + return String("co2"); + } else { + return String("off"); + } +} + +void AgConfigure::saveConfig(void) { + config._check = 0; + int len = sizeof(config) - sizeof(config._check); + uint8_t *data = (uint8_t *)&config; + for (int i = 0; i < len; i++) { + config._check += data[i]; + } + EEPROM.writeBytes(0, &config, sizeof(config)); + EEPROM.commit(); + printLog("Save Config"); +} + +void AgConfigure::loadConfig(void) { + if (EEPROM.readBytes(0, &config, sizeof(config)) != sizeof(config)) { + printLog("Load configure failed"); + defaultConfig(); + } else { + uint32_t sum = 0; + uint8_t *data = (uint8_t *)&config; + int len = sizeof(config) - sizeof(config._check); + for (int i = 0; i < len; i++) { + sum += data[i]; + } + + if (sum != config._check) { + printLog("Configure validate invalid"); + defaultConfig(); + } + } +} +void AgConfigure::defaultConfig(void) { + // Default country is null + memset(config.country, 0, sizeof(config.country)); + // Default MQTT broker is null. + memset(config.mqttBroker, 0, sizeof(config.mqttBroker)); + + config.configurationControl = ConfigurationControl::ConfigurationControlBoth; + config.inUSAQI = false; // pmStandard = ugm3 + config.inF = false; + config.postDataToAirGradient = true; + config.displayMode = true; + config.useRGBLedBar = LedBarMode::LedBarModeCO2; + config.abcDays = 7; + config.tvocLearningOffset = 12; + config.noxLearningOffset = 12; + config.temperatureUnit = 'c'; + + saveConfig(); +} + +void AgConfigure::printConfig(void) { printLog(toString()); } + +AgConfigure::AgConfigure(Stream &debugLog) : debugLog(debugLog) {} + +AgConfigure::~AgConfigure() {} + +bool AgConfigure::begin(void) { + EEPROM.begin(512); + loadConfig(); + printConfig(); + + return true; +} + +/** + * @brief Parse JSON configura string to local configure + * + * @param data JSON string data + * @param isLocal true of data got from local, otherwise get from Aigradient + * server + * @return true Success + * @return false Failure + */ +bool AgConfigure::parse(String data, bool isLocal) { + JSONVar root = JSON.parse(data); + if (JSON.typeof_(root) == "undefined") { + printLog("Configuration JSON invalid"); + return false; + } + printLog("Parse configure success"); + + /** Is configuration changed */ + bool changed = false; + + /** Get ConfigurationControl */ + if (JSON.typeof_(root["configurationControl"]) == "string") { + String configurationControl = root["configurationControl"]; + if (configurationControl == + String(CONFIGURATION_CONTROL_NAME + [ConfigurationControl::ConfigurationControlLocal])) { + config.configurationControl = + (uint8_t)ConfigurationControl::ConfigurationControlLocal; + changed = true; + } else if (configurationControl == + String(CONFIGURATION_CONTROL_NAME + [ConfigurationControl::ConfigurationControlCloud])) { + config.configurationControl = + (uint8_t)ConfigurationControl::ConfigurationControlCloud; + changed = true; + } else if (configurationControl == + String(CONFIGURATION_CONTROL_NAME + [ConfigurationControl::ConfigurationControlBoth])) { + config.configurationControl = + (uint8_t)ConfigurationControl::ConfigurationControlBoth; + changed = true; + } else { + printLog("'configurationControl' value '" + configurationControl + + "' invalid"); + return false; + } + } else { + return false; + } + + if ((config.configurationControl == + (byte)ConfigurationControl::ConfigurationControlCloud)) { + printLog("Ignore, cause ConfigurationControl is " + + String(CONFIGURATION_CONTROL_NAME[config.configurationControl])); + return false; + } + + if (JSON.typeof_(root["country"]) == "string") { + String country = root["country"]; + if (country.length() == 2) { + if (country != String(config.country)) { + changed = true; + snprintf(config.country, sizeof(config.country), country.c_str()); + printLog("Set country: " + country); + } + + // Update temperature unit if get configuration from server + if (isLocal == false) { + if (country == "US") { + if (config.temperatureUnit == 'c') { + changed = true; + config.temperatureUnit = 'f'; + } + } else { + if (config.temperatureUnit == 'f') { + changed = true; + config.temperatureUnit = 'c'; + } + } + } + } else { + printLog("Country name " + country + + " invalid. Find details here (ALPHA-2): " + "https://www.iban.com/country-codes"); + } + } + + if (JSON.typeof_(root["pmStandard"]) == "string") { + String pmStandard = root["pmStandard"]; + bool inUSAQI = true; + if (pmStandard == "ugm3") { + inUSAQI = false; + } + + if (inUSAQI != config.inUSAQI) { + config.inUSAQI = inUSAQI; + changed = true; + printLog("Set PM standard: " + pmStandard); + } + } + + if (JSON.typeof_(root["co2CalibrationRequested"]) == "boolean") { + co2CalibrationRequested = root["co2CalibrationRequested"]; + printLog("Set co2CalibrationRequested: " + String(co2CalibrationRequested)); + } + + if (JSON.typeof_(root["ledBarTestRequested"]) == "boolean") { + ledBarTestRequested = root["ledBarTestRequested"]; + printLog("Set ledBarTestRequested: " + String(ledBarTestRequested)); + } + + if (JSON.typeof_(root["ledBarMode"]) == "string") { + String mode = root["ledBarMode"]; + uint8_t ledBarMode = config.useRGBLedBar; + if (mode == "co2") { + ledBarMode = LedBarModeCO2; + } else if (mode == "pm") { + ledBarMode = LedBarModePm; + } else if (mode == "off") { + ledBarMode = LedBarModeOff; + } else { + ledBarMode = config.useRGBLedBar; + printLog("ledBarMode value '" + mode + "' invalid"); + } + + if (ledBarMode != config.useRGBLedBar) { + config.useRGBLedBar = ledBarMode; + changed = true; + printLog("Set ledBarMode: " + mode); + } + } + + if (JSON.typeof_(root["displayMode"]) == "string") { + String mode = root["displayMode"]; + bool displayMode = false; + if (mode == "on") { + displayMode = true; + } else if (mode == "off") { + displayMode = false; + } else { + displayMode = config.displayMode; + printLog("displayMode '" + mode + "' invalid"); + } + + if (displayMode != config.displayMode) { + changed = true; + config.displayMode = displayMode; + printLog("Set displayMode: " + mode); + } + } + + if (JSON.typeof_(root["abcDays"]) == "number") { + int abcDays = root["abcDays"]; + if (abcDays != config.abcDays) { + config.abcDays = abcDays; + changed = true; + printLog("Set abcDays: " + String(abcDays)); + } + } + + if (JSON.typeof_(root["tvocLearningOffset"]) == "number") { + int tvocLearningOffset = root["tvocLearningOffset"]; + if (tvocLearningOffset != config.tvocLearningOffset) { + changed = true; + config.tvocLearningOffset = tvocLearningOffset; + printLog("Set tvocLearningOffset: " + String(tvocLearningOffset)); + } + } + + if (JSON.typeof_(root["noxLearningOffset"]) == "number") { + int noxLearningOffset = root["noxLearningOffset"]; + if (noxLearningOffset != config.noxLearningOffset) { + changed = true; + config.noxLearningOffset = noxLearningOffset; + printLog("Set noxLearningOffset: " + String(noxLearningOffset)); + } + } + + if (JSON.typeof_(root["mqttBrokerUrl"]) == "string") { + String broker = root["mqttBrokerUrl"]; + if (broker.length() < sizeof(config.mqttBroker)) { + if (broker != String(config.mqttBroker)) { + changed = true; + snprintf(config.mqttBroker, sizeof(config.mqttBroker), broker.c_str()); + printLog("Set mqttBrokerUrl: " + broker); + } + } else { + printLog("Error: mqttBroker length invalid: " + String(broker.length())); + } + } + + char temperatureUnit = 0; + if (JSON.typeof_(root["temperatureUnit"]) == "string") { + String unit = root["temperatureUnit"]; + if (unit == "c" || unit == "C") { + temperatureUnit = 'c'; + } else if (unit == "f" || unit == "F") { + temperatureUnit = 'f'; + } else { + temperatureUnit = 0; + } + } + + if (temperatureUnit != config.temperatureUnit) { + changed = true; + config.temperatureUnit = temperatureUnit; + if (temperatureUnit == 0) { + printLog("set temperatureUnit: null"); + } else { + printLog("set temperatureUnit: " + String(temperatureUnit)); + } + } + + if (JSON.typeof_(root["postDataToAirGradient"]) == "boolean") { + bool post = root["postDataToAirGradient"]; + if (post != config.postDataToAirGradient) { + changed = true; + config.postDataToAirGradient = post; + printLog("Set postDataToAirGradient: " + String(post)); + } + } + + /** Parse data only got from AirGradient server */ + if (isLocal == false) { + if (JSON.typeof_(root["model"]) == "string") { + String model = root["model"]; + if (model.length() < sizeof(config.model)) { + if (model != String(config.model)) { + changed = true; + snprintf(config.model, sizeof(config.model), model.c_str()); + } + } else { + printLog("Error: modal name length invalid: " + String(model.length())); + } + } + } + + if (changed) { + saveConfig(); + } + printConfig(); + + return true; +} + +String AgConfigure::toString(void) { + JSONVar root; + + /** "country" */ + root["Country"] = String(config.country); + + /** "pmStandard" */ + if (config.inUSAQI) { + root["pmStandard"] = "USAQI"; + } else { + root["pmStandard"] = "ugm3"; + } + + /** co2CalibrationRequested */ + /** ledBarTestRequested */ + + /** "ledBarMode" */ + root["ledBarMode"] = getLedBarModeName(); + + /** "displayMode" */ + root["displayMode"] = config.displayMode; + + /** "abcDays" */ + root["abcDays"] = config.abcDays; + + /** "tvocLearningOffset" */ + root["tvocLearningOffset"] = config.tvocLearningOffset; + + /** "noxLearningOffset" */ + root["noxLearningOffset"] = config.noxLearningOffset; + + /** "mqttBrokerUrl" */ + root["mqttBrokerUrl"] = String(config.mqttBroker); + + /** "temperatureUnit" */ + root["temperatureUnit"] = String(config.temperatureUnit); + + /** configurationControl */ + root["configurationControl"] = + String(CONFIGURATION_CONTROL_NAME[config.configurationControl]); + + /** "postDataToAirGradient" */ + root["postDataToAirGradient"] = config.postDataToAirGradient; + + return JSON.stringify(root); +} + +bool AgConfigure::isTemperatureUnitInF(void) { + return (config.temperatureUnit == 'f'); +} + +String AgConfigure::getCountry(void) { return String(config.country); } + +bool AgConfigure::isPmStandardInUSAQI(void) { return config.inUSAQI; } + +int AgConfigure::getCO2CalirationAbcDays(void) { return config.abcDays; } + +LedBarMode AgConfigure::getLedBarMode(void) { + return (LedBarMode)config.useRGBLedBar; +} + +String AgConfigure::getLedBarModeName(void) { + return getLedBarModeName((LedBarMode)config.useRGBLedBar); +} + +bool AgConfigure::getDisplayMode(void) { return config.displayMode; } + +String AgConfigure::getMqttBrokerUri(void) { return String(config.mqttBroker); } + +bool AgConfigure::isPostDataToAirGradient(void) { + return config.postDataToAirGradient; +} + +ConfigurationControl AgConfigure::getConfigurationControl(void) { + return (ConfigurationControl)config.configurationControl; +} + +/** + * @brief CO2 manual calib request, the request flag will clear after get. Must + * call this after parse success + * + * @return true Requested + * @return false Not requested + */ +bool AgConfigure::isCo2CalibrationRequested(void) { + bool requested = co2CalibrationRequested; + co2CalibrationRequested = false; // clear requested + return requested; +} + +/** + * @brief LED bar test request, the request flag will clear after get. Must call + * this function after parse success + * + * @return true Requested + * @return false Not requested + */ +bool AgConfigure::isLedBarTestRequested(void) { + bool requested = ledBarTestRequested; + ledBarTestRequested = false; + return requested; +} + +/** + * @brief Reset default configure + */ +void AgConfigure::reset(void) { + defaultConfig(); + printLog("Reset to default configure"); + printConfig(); +} + +/** + * @brief Get model name, it's usage for offline mode + * + * @return String + */ +String AgConfigure::getModel(void) { return String(config.model); } diff --git a/src/AgConfigure.h b/src/AgConfigure.h new file mode 100644 index 00000000..2b398765 --- /dev/null +++ b/src/AgConfigure.h @@ -0,0 +1,66 @@ +#ifndef _AG_CONFIG_H_ +#define _AG_CONFIG_H_ + +#include +#include +#include + +class AgConfigure { +private: + struct Config { + char model[20]; + char country[3]; /** Country name has only 2 character, ex: TH = Thailand */ + char mqttBroker[256]; /** MQTT broker URI */ + bool inUSAQI; /** If PM standard "ugm3" inUSAQI = false, otherwise is true + */ + bool inF; /** Temperature unit F */ + bool postDataToAirGradient; /** If true, monitor will not POST data to + airgradient server. Make sure no error + message shown on monitor */ + uint8_t configurationControl; /** If true, configuration from airgradient server + will be ignored */ + bool displayMode; /** true if enable display */ + uint8_t useRGBLedBar; + uint8_t abcDays; + int tvocLearningOffset; + int noxLearningOffset; + char temperatureUnit; // 'f' or 'c' + + uint32_t _check; + }; + struct Config config; + bool co2CalibrationRequested; + bool ledBarTestRequested; + Stream &debugLog; + + void printLog(String log); + String getLedBarModeName(LedBarMode mode); + void saveConfig(void); + void loadConfig(void); + void defaultConfig(void); + void printConfig(void); + +public: + AgConfigure(Stream &debugLog); + ~AgConfigure(); + + bool begin(void); + bool parse(String data, bool isLocal); + String toString(void); + bool isTemperatureUnitInF(void); + String getCountry(void); + bool isPmStandardInUSAQI(void); + int getCO2CalirationAbcDays(void); + LedBarMode getLedBarMode(void); + String getLedBarModeName(void); + bool getDisplayMode(void); + String getMqttBrokerUri(void); + bool isPostDataToAirGradient(void); + ConfigurationControl getConfigurationControl(void); + bool isCo2CalibrationRequested(void); + bool isLedBarTestRequested(void); + void reset(void); + String getModel(void); +}; + +#endif /** _AG_CONFIG_H_ */ diff --git a/src/AgSchedule.cpp b/src/AgSchedule.cpp new file mode 100644 index 00000000..434c6883 --- /dev/null +++ b/src/AgSchedule.cpp @@ -0,0 +1,21 @@ +#include "AgSchedule.h" + +AgSchedule::AgSchedule(int period, void (*handler)(void)) + : period(period), handler(handler) {} + +AgSchedule::~AgSchedule() {} + +void AgSchedule::run(void) { + uint32_t ms = (uint32_t)(millis() - count); + if (ms >= period) { + handler(); + count = millis(); + } +} + +/** + * @brief Set schedule period + * + * @param period Period in ms + */ +void AgSchedule::setPeriod(int period) { this->period = period; } diff --git a/src/AgSchedule.h b/src/AgSchedule.h new file mode 100644 index 00000000..cdecb4d3 --- /dev/null +++ b/src/AgSchedule.h @@ -0,0 +1,19 @@ +#ifndef _AG_SCHEDULE_H_ +#define _AG_SCHEDULE_H_ + +#include + +class AgSchedule { +private: + int period; + void (*handler)(void); + uint32_t count; + +public: + AgSchedule(int period, void (*handler)(void)); + ~AgSchedule(); + void run(void); + void setPeriod(int period); +}; + +#endif /** _AG_SCHEDULE_H_ */ diff --git a/src/AgStateMachine.h b/src/AgStateMachine.h new file mode 100644 index 00000000..32b00da0 --- /dev/null +++ b/src/AgStateMachine.h @@ -0,0 +1,60 @@ +#ifndef _AG_STATE_MACHINE_H_ +#define _AG_STATE_MACHINE_H_ + +/** + * @brief Application state machine state + * + */ +enum AgStateMachine { + /** In WiFi Manger Mode */ + AgStateMachineWiFiManagerMode, + + /** WiFi Manager has connected to mobile phone */ + AgStateMachineWiFiManagerPortalActive, + + /** After SSID and PW entered and OK clicked, connection to WiFI network is + attempted*/ + AgStateMachineWiFiManagerStaConnecting, + + /** Connecting to WiFi worked */ + AgStateMachineWiFiManagerStaConnected, + + /** Once connected to WiFi an attempt to reach the server is performed */ + AgStateMachineWiFiOkServerConnecting, + + /** Server is reachable, all fine */ + AgStateMachineWiFiOkServerConnected, + + /** =================================== * + * Exceptions during WIFi Setup * + * =================================== **/ + /** Cannot connect to WiFi (e.g. wrong password, WPA Enterprise etc.) */ + AgStateMachineWiFiManagerConnectFailed, + + /** Connected to WiFi but server not reachable, e.g. firewall + block/whitelisting needed etc. */ + AgStateMachineWiFiOkServerConnectFailed, + + /** Server reachable but sensor not configured correctly*/ + AgStateMachineWiFiOkServerOkSensorConfigFailed, + + /** =================================== * + * During Normal Operation * + * =================================== **/ + + /** Connection to WiFi network failed credentials incorrect encryption not + supported etc. */ + AgStateMachineWiFiLost, + + /** Connected to WiFi network but the server cannot be reached through the + internet, e.g. blocked by firewall */ + AgStateMachineServerLost, + + /** Server is reachable but there is some configuration issue to be fixed on + the server side */ + AgStateMachineSensorConfigFailed, + + AgStateMachineNormal, +}; + +#endif /** _AG_STATE_MACHINE_H_ */ diff --git a/src/AirGradient.cpp b/src/AirGradient.cpp index ff4ad552..d966f570 100644 --- a/src/AirGradient.cpp +++ b/src/AirGradient.cpp @@ -2,6 +2,24 @@ #define AG_LIB_VER "3.0.9" +const char *AgFirmwareModeName(AgFirmwareMode mode) { + switch (mode) { + case FW_MODE_I_9PSL: + return "I-9PSL"; + case FW_MODE_O_1PP: + return "O-1PP"; + case FW_MODE_O_1PPT: + return "O-1PPT"; + case FW_MODE_O_1PST: + return "O-1PST"; + case FW_MDOE_O_1PS: + return "0-1PS"; + default: + break; + } + return "UNKNOWN"; +} + AirGradient::AirGradient(BoardType type) : pms5003(type), pms5003t_1(type), pms5003t_2(type), s8(type), sgp41(type), display(type), boardType(type), button(type), statusLed(type), diff --git a/src/AirGradient.h b/src/AirGradient.h index a7eea275..306e9b86 100644 --- a/src/AirGradient.h +++ b/src/AirGradient.h @@ -15,19 +15,37 @@ /** * @brief RGB LED bar mode for ONE_INDOOR board - * */ -enum UseLedBar { - UseLedBarOff, /** Don't use LED bar */ - UseLedBarPM, /** Use LED bar for PMS */ - UseLedBarCO2, /** Use LED bar for CO2 */ +enum LedBarMode { + /** Don't use LED bar */ + LedBarModeOff, + + /** Use LED bar for show PM2.5 value level */ + LedBarModePm, + + /** Use LED bar for show CO2 value level */ + LedBarModeCO2, }; enum ConfigurationControl { - Local, /** Allow set configuration from local over HTTP server */ - Cloud, /** Allow set configuration from Airgradient webserver */ - Both /** Allow set configuration from Local and Cloud */ + /** Allow set configuration from local over device HTTP server */ + ConfigurationControlLocal, + + /** Allow set configuration from Airgradient cloud */ + ConfigurationControlCloud, + + /** Allow set configuration from Local and Cloud */ + ConfigurationControlBoth +}; + +enum AgFirmwareMode { + FW_MODE_I_9PSL, /** ONE_INDOOR */ + FW_MODE_O_1PST, /** PMS5003T, S8 and SGP41 */ + FW_MODE_O_1PPT, /** PMS5003T_1, PMS5003T_2, SGP41 */ + FW_MODE_O_1PP, /** PMS5003T_1, PMS5003T_2 */ + FW_MDOE_O_1PS /** PMS5003T, S8 */ }; +const char* AgFirmwareModeName(AgFirmwareMode mode); /** * @brief Class with define all the sensor has supported by Airgradient. Each diff --git a/src/MqttClient.cpp b/src/MqttClient.cpp new file mode 100644 index 00000000..60916eab --- /dev/null +++ b/src/MqttClient.cpp @@ -0,0 +1,170 @@ +#include "MqttClient.h" + +static void __mqtt_event_handler(void *handler_args, esp_event_base_t base, + int32_t event_id, void *event_data); + +MqttClient::MqttClient(Stream &debugLog) : debugLog(debugLog) {} + +MqttClient::~MqttClient() {} + +bool MqttClient::begin(String uri) { + if (isBegin) { + _printLog("Already begin, calll 'end' and try again"); + return true; + } + if (uri.isEmpty()) { + Serial.println("Mqtt uri is empty"); + return false; + } + + this->uri = uri; + _printLog("Init uri: " + uri); + + /** config esp_mqtt client */ + esp_mqtt_client_config_t config = { + .uri = this->uri.c_str(), + }; + + /** init client */ + client = esp_mqtt_client_init(&config); + if (client == NULL) { + _printLog("Init client failed"); + return false; + } + + /** Register event */ + if (esp_mqtt_client_register_event(client, MQTT_EVENT_ANY, __mqtt_event_handler, + this) != ESP_OK) { + _printLog("Register event failed"); + return false; + } + + if (esp_mqtt_client_start(client) != ESP_OK) { + _printLog("Client start failed"); + return false; + } + + isBegin = true; + connectionFailedCount = 0; + return true; +} + +void MqttClient::end(void) { + if (!isBegin) { + _printLog("Already end, call 'begin' and try again"); + return; + } + + esp_mqtt_client_disconnect(client); + esp_mqtt_client_stop(client); + esp_mqtt_client_destroy(client); + client = NULL; + isBegin = false; + + Serial.println("De-init"); +} + +void MqttClient::_printLog(String log) { + debugLog.println("[MqttClient]" + log); +} + +void MqttClient::_updateConnected(bool connected) { + this->connected = connected; + if (connected) { + connectionFailedCount = 0; + } else { + connectionFailedCount++; + _printLog("Connection failed count " + String(connectionFailedCount)); + } +} + +bool MqttClient::publish(String &topic, String &payload) { + if (!isBegin) { + _printLog("Error: No-initialized"); + return false; + } + if (!connected) { + _printLog("Error: Client disconnected"); + return false; + } + + if (esp_mqtt_client_publish(client, topic.c_str(), payload.c_str(), + payload.length(), 0, 0) == ESP_OK) { + _printLog("Publish topic: " + topic); + _printLog("Publish payload: " + payload); + return true; + } + _printLog("Error: publish"); + return false; +} + +/** + * @brief Check that URI is same as current initialized URI + * + * @param uri Target URI + * @return true Same + * @return false Difference + */ +bool MqttClient::isCurrentUri(String &uri) { + if (this->uri == uri) { + return true; + } + return false; +} + +/** + * @brief Get MQTT client connected status + * + * @return true Connected + * @return false Disconnected + */ +bool MqttClient::isConnected(void) { return connected; } + +/** + * @brief Get number of connection failed + * + * @return int + */ +int MqttClient::getConnectionFailedCount(void) { return connectionFailedCount; } + +static void __mqtt_event_handler(void *handler_args, esp_event_base_t base, + int32_t event_id, void *event_data) { + MqttClient *mqtt = (MqttClient *)handler_args; + + esp_mqtt_event_handle_t event = (esp_mqtt_event_handle_t)event_data; + esp_mqtt_client_handle_t client = event->client; + + int msg_id; + switch ((esp_mqtt_event_id_t)event_id) { + case MQTT_EVENT_CONNECTED: + mqtt->_printLog("MQTT_EVENT_CONNECTED"); + mqtt->_updateConnected(true); + break; + case MQTT_EVENT_DISCONNECTED: + mqtt->_printLog("MQTT_EVENT_DISCONNECTED"); + mqtt->_updateConnected(false); + break; + case MQTT_EVENT_SUBSCRIBED: + break; + case MQTT_EVENT_UNSUBSCRIBED: + break; + case MQTT_EVENT_PUBLISHED: + break; + case MQTT_EVENT_DATA: + break; + case MQTT_EVENT_ERROR: + Serial.println("MQTT_EVENT_ERROR"); + if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) { + mqtt->_printLog("Reported from esp-tls: " + + String(event->error_handle->esp_tls_last_esp_err)); + mqtt->_printLog("Reported from tls stack: " + + String(event->error_handle->esp_tls_stack_err)); + mqtt->_printLog("Captured as transport's socket errno: " + + String(event->error_handle->esp_transport_sock_errno)); + } + break; + default: + Serial.printf("Other event id:%d\r\n", event->event_id); + break; + } +} diff --git a/src/MqttClient.h b/src/MqttClient.h new file mode 100644 index 00000000..575e5583 --- /dev/null +++ b/src/MqttClient.h @@ -0,0 +1,30 @@ +#ifndef _AG_MQTT_CLIENT_H_ +#define _AG_MQTT_CLIENT_H_ + +#include "mqtt_client.h" +#include + +class MqttClient { +private: + Stream &debugLog; + bool isBegin = false; + String uri; + esp_mqtt_client_handle_t client; + bool connected = false; + int connectionFailedCount = 0; + +public: + MqttClient(Stream &debugLog); + ~MqttClient(); + + bool begin(String uri); + void end(void); + void _printLog(String log); + void _updateConnected(bool connected); + bool publish(String &topic, String &payload); + bool isCurrentUri(String &uri); + bool isConnected(void); + int getConnectionFailedCount(void); +}; + +#endif /** _AG_MQTT_CLIENT_H_ */