|
|
|
@ -26,7 +26,6 @@ https://forum.airgradient.com/
|
|
|
|
|
CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include "AgConfigure.h"
|
|
|
|
|
#include "AgSchedule.h"
|
|
|
|
|
#include "AgStateMachine.h"
|
|
|
|
@ -37,6 +36,7 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
|
|
|
|
|
#include "Arduino.h"
|
|
|
|
|
#include "EEPROM.h"
|
|
|
|
|
#include "ESPmDNS.h"
|
|
|
|
|
#include "Libraries/airgradient-client/src/common.h"
|
|
|
|
|
#include "LocalServer.h"
|
|
|
|
|
#include "MqttClient.h"
|
|
|
|
|
#include "OpenMetrics.h"
|
|
|
|
@ -45,6 +45,7 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
|
|
|
|
|
#include <HardwareSerial.h>
|
|
|
|
|
#include <WebServer.h>
|
|
|
|
|
#include <WiFi.h>
|
|
|
|
|
#include <cstdint>
|
|
|
|
|
#include <string>
|
|
|
|
|
|
|
|
|
|
#include "Libraries/airgradient-client/src/agSerial.h"
|
|
|
|
@ -65,7 +66,7 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
|
|
|
|
|
#define WIFI_TRANSMISSION_INTERVAL 1 * 60000 /** ms */
|
|
|
|
|
#define CELLULAR_SERVER_CONFIG_SYNC_INTERVAL 30 * 60000 /** ms */
|
|
|
|
|
#define CELLULAR_MEASUREMENT_INTERVAL 3 * 60000 /** ms */
|
|
|
|
|
#define CELLULAR_TRANSMISSION_INTERVAL 9 * 60000 /** ms */
|
|
|
|
|
#define CELLULAR_TRANSMISSION_INTERVAL 3 * 60000 /** ms */
|
|
|
|
|
#define MQTT_SYNC_INTERVAL 60000 /** ms */
|
|
|
|
|
#define SENSOR_CO2_CALIB_COUNTDOWN_MAX 5 /** sec */
|
|
|
|
|
#define SENSOR_TVOC_UPDATE_INTERVAL 1000 /** ms */
|
|
|
|
@ -74,7 +75,10 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
|
|
|
|
|
#define SENSOR_TEMP_HUM_UPDATE_INTERVAL 6000 /** ms */
|
|
|
|
|
#define DISPLAY_DELAY_SHOW_CONTENT_MS 2000 /** ms */
|
|
|
|
|
#define FIRMWARE_CHECK_FOR_UPDATE_MS (60 * 60 * 1000) /** ms */
|
|
|
|
|
#define TIME_TO_START_POWER_CYCLE_CELLULAR_MODULE (1 * 60) /** minutes */
|
|
|
|
|
#define TIMEOUT_WAIT_FOR_CELLULAR_MODULE_READY (2 * 60) /** minutes */
|
|
|
|
|
|
|
|
|
|
#define MEASUREMENT_TRANSMIT_CYCLE 3
|
|
|
|
|
#define MAXIMUM_MEASUREMENT_CYCLE_QUEUE 80
|
|
|
|
|
#define RESERVED_MEASUREMENT_CYCLE_CAPACITY 10
|
|
|
|
|
|
|
|
|
@ -88,6 +92,8 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
|
|
|
|
|
#define GPIO_EXPANSION_CARD_POWER 4
|
|
|
|
|
#define GPIO_IIC_RESET 3
|
|
|
|
|
|
|
|
|
|
#define MINUTES() ((uint32_t)(esp_timer_get_time() / 1000 / 1000 / 60))
|
|
|
|
|
|
|
|
|
|
static MqttClient mqttClient(Serial);
|
|
|
|
|
static TaskHandle_t mqttTask = NULL;
|
|
|
|
|
static Configuration configuration(Serial);
|
|
|
|
@ -102,7 +108,7 @@ static OpenMetrics openMetrics(measurements, configuration, wifiConnector);
|
|
|
|
|
static LocalServer localServer(Serial, openMetrics, measurements, configuration,
|
|
|
|
|
wifiConnector);
|
|
|
|
|
static AgSerial *agSerial;
|
|
|
|
|
static CellularModule *cell;
|
|
|
|
|
static CellularModule *cellularCard;
|
|
|
|
|
static AirgradientClient *agClient;
|
|
|
|
|
|
|
|
|
|
enum NetworkOption {
|
|
|
|
@ -111,12 +117,17 @@ enum NetworkOption {
|
|
|
|
|
};
|
|
|
|
|
NetworkOption networkOption;
|
|
|
|
|
TaskHandle_t handleNetworkTask = NULL;
|
|
|
|
|
static bool otaInProgress = false;
|
|
|
|
|
static bool firmwareUpdateInProgress = false;
|
|
|
|
|
|
|
|
|
|
static uint32_t factoryBtnPressTime = 0;
|
|
|
|
|
static AgFirmwareMode fwMode = FW_MODE_I_9PSL;
|
|
|
|
|
static bool ledBarButtonTest = false;
|
|
|
|
|
static String fwNewVersion;
|
|
|
|
|
static int lastCellSignalQuality = 99; // CSQ
|
|
|
|
|
|
|
|
|
|
// Default value is 0, indicate its not started yet
|
|
|
|
|
// In minutes
|
|
|
|
|
uint32_t agCeClientProblemDetectedTime = 0;
|
|
|
|
|
|
|
|
|
|
SemaphoreHandle_t mutexMeasurementCycleQueue;
|
|
|
|
|
static std::vector<Measurements::Measures> measurementCycleQueue;
|
|
|
|
@ -145,6 +156,7 @@ static void displayExecuteOta(AirgradientOTA::OtaResult result, String msg, int
|
|
|
|
|
static int calculateMaxPeriod(int updateInterval);
|
|
|
|
|
static void setMeasurementMaxPeriod();
|
|
|
|
|
static void newMeasurementCycle();
|
|
|
|
|
static void restartIfCeClientIssueOverTwoHours();
|
|
|
|
|
static void networkSignalCheck();
|
|
|
|
|
static void networkingTask(void *args);
|
|
|
|
|
|
|
|
|
@ -297,11 +309,17 @@ void setup() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void loop() {
|
|
|
|
|
if (networkOption == UseCellular) {
|
|
|
|
|
// Check if cellular client not ready until certain time
|
|
|
|
|
// Redundant check in both task to make sure its executed
|
|
|
|
|
restartIfCeClientIssueOverTwoHours();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Schedule to feed external watchdog
|
|
|
|
|
watchdogFeedSchedule.run();
|
|
|
|
|
|
|
|
|
|
if (otaInProgress) {
|
|
|
|
|
// OTA currently in progress, temporarily disable running sensor schedules
|
|
|
|
|
if (firmwareUpdateInProgress) {
|
|
|
|
|
// Firmare update currently in progress, temporarily disable running sensor schedules
|
|
|
|
|
delay(10000);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
@ -334,7 +352,7 @@ void loop() {
|
|
|
|
|
static bool pmsConnected = false;
|
|
|
|
|
if (pmsConnected != ag->pms5003.connected()) {
|
|
|
|
|
pmsConnected = ag->pms5003.connected();
|
|
|
|
|
Serial.printf("PMS sensor %s ", pmsConnected?"connected":"removed");
|
|
|
|
|
Serial.printf("PMS sensor %s \n", pmsConnected?"connected":"removed");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
@ -541,31 +559,28 @@ void checkForFirmwareUpdate(void) {
|
|
|
|
|
if (networkOption == UseWifi) {
|
|
|
|
|
agOta = new AirgradientOTAWifi;
|
|
|
|
|
} else {
|
|
|
|
|
agOta = new AirgradientOTACellular(cell);
|
|
|
|
|
agOta = new AirgradientOTACellular(cellularCard);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Indicate main task that ota is performing
|
|
|
|
|
Serial.println("Check for firmware update, disabling main task");
|
|
|
|
|
otaInProgress = true;
|
|
|
|
|
if (configuration.hasSensorSGP && networkOption == UseCellular) {
|
|
|
|
|
// Only for cellular because it can disturb i2c line
|
|
|
|
|
Serial.println("Disable SGP41 task for cellular OTA");
|
|
|
|
|
ag->sgp41.end();
|
|
|
|
|
}
|
|
|
|
|
// Indicate main task that firmware update is in progress
|
|
|
|
|
firmwareUpdateInProgress = true;
|
|
|
|
|
|
|
|
|
|
agOta->setHandlerCallback(otaHandlerCallback);
|
|
|
|
|
agOta->updateIfAvailable(ag->deviceId().c_str(), GIT_VERSION);
|
|
|
|
|
|
|
|
|
|
// Only goes to this line if OTA is not success
|
|
|
|
|
String httpDomain = configuration.getHttpDomain();
|
|
|
|
|
if (httpDomain != "") {
|
|
|
|
|
Serial.printf("httpDomain configuration available, start OTA with custom domain\n",
|
|
|
|
|
httpDomain.c_str());
|
|
|
|
|
agOta->updateIfAvailable(ag->deviceId().c_str(), GIT_VERSION, httpDomain.c_str());
|
|
|
|
|
} else {
|
|
|
|
|
agOta->updateIfAvailable(ag->deviceId().c_str(), GIT_VERSION);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Only goes to this line if firmware update is not success
|
|
|
|
|
// Handled by otaHandlerCallback
|
|
|
|
|
|
|
|
|
|
otaInProgress = false;
|
|
|
|
|
if (configuration.hasSensorSGP && networkOption == UseCellular) {
|
|
|
|
|
// Re-start SGP41 task
|
|
|
|
|
if (!sgp41Init()) {
|
|
|
|
|
Serial.println("Failed re-start SGP41 task");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Indicate main task that firmware update finish
|
|
|
|
|
firmwareUpdateInProgress = false;
|
|
|
|
|
|
|
|
|
|
delete agOta;
|
|
|
|
|
Serial.println();
|
|
|
|
@ -573,14 +588,25 @@ void checkForFirmwareUpdate(void) {
|
|
|
|
|
|
|
|
|
|
void otaHandlerCallback(AirgradientOTA::OtaResult result, const char *msg) {
|
|
|
|
|
switch (result) {
|
|
|
|
|
case AirgradientOTA::Starting:
|
|
|
|
|
case AirgradientOTA::Starting: {
|
|
|
|
|
Serial.println("Firmware update starting...");
|
|
|
|
|
if (configuration.hasSensorSGP && networkOption == UseCellular) {
|
|
|
|
|
// Temporary pause SGP41 task while cellular firmware update is in progress
|
|
|
|
|
ag->sgp41.pause();
|
|
|
|
|
}
|
|
|
|
|
displayExecuteOta(result, fwNewVersion, 0);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case AirgradientOTA::InProgress:
|
|
|
|
|
Serial.printf("OTA progress: %s\n", msg);
|
|
|
|
|
displayExecuteOta(result, "", std::stoi(msg));
|
|
|
|
|
break;
|
|
|
|
|
case AirgradientOTA::Failed:
|
|
|
|
|
displayExecuteOta(result, "", 0);
|
|
|
|
|
if (configuration.hasSensorSGP && networkOption == UseCellular) {
|
|
|
|
|
ag->sgp41.resume();
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case AirgradientOTA::Skipped:
|
|
|
|
|
case AirgradientOTA::AlreadyUpToDate:
|
|
|
|
|
displayExecuteOta(result, "", 0);
|
|
|
|
@ -647,7 +673,11 @@ static void displayExecuteOta(AirgradientOTA::OtaResult result, String msg, int
|
|
|
|
|
}
|
|
|
|
|
delay(1000);
|
|
|
|
|
}
|
|
|
|
|
oledDisplay.setAirGradient(0);
|
|
|
|
|
|
|
|
|
|
if (ag->isOne()) {
|
|
|
|
|
oledDisplay.setAirGradient(0);
|
|
|
|
|
oledDisplay.setBrightness(0);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
@ -925,8 +955,8 @@ void initializeNetwork() {
|
|
|
|
|
if (agSerial->open()) {
|
|
|
|
|
Serial.println("Cellular module found");
|
|
|
|
|
// Initialize cellular module and use cellular as agClient
|
|
|
|
|
cell = new CellularModuleA7672XX(agSerial, GPIO_POWER_MODULE_PIN);
|
|
|
|
|
agClient = new AirgradientCellularClient(cell);
|
|
|
|
|
cellularCard = new CellularModuleA7672XX(agSerial, GPIO_POWER_MODULE_PIN);
|
|
|
|
|
agClient = new AirgradientCellularClient(cellularCard);
|
|
|
|
|
networkOption = UseCellular;
|
|
|
|
|
} else {
|
|
|
|
|
Serial.println("Cellular module not available, using wifi");
|
|
|
|
@ -1340,7 +1370,10 @@ void postUsingWifi() {
|
|
|
|
|
Serial.printf("Free heap: %u\n", ESP.getFreeHeap());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void postUsingCellular() {
|
|
|
|
|
/**
|
|
|
|
|
* forcePost to force post without checking transmit cycle
|
|
|
|
|
*/
|
|
|
|
|
void postUsingCellular(bool forcePost) {
|
|
|
|
|
// Aquire queue mutex to get queue size
|
|
|
|
|
xSemaphoreTake(mutexMeasurementCycleQueue, portMAX_DELAY);
|
|
|
|
|
|
|
|
|
@ -1352,6 +1385,14 @@ void postUsingCellular() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check queue size if its ready to transmit
|
|
|
|
|
// It is ready if size is divisible by 3
|
|
|
|
|
if (!forcePost && (queueSize % MEASUREMENT_TRANSMIT_CYCLE) > 0) {
|
|
|
|
|
Serial.printf("Not ready to transmit, queue size are %d\n", queueSize);
|
|
|
|
|
xSemaphoreGive(mutexMeasurementCycleQueue);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build payload include all measurements from queue
|
|
|
|
|
std::string payload;
|
|
|
|
|
payload += std::to_string(CELLULAR_MEASUREMENT_INTERVAL / 1000); // Convert to seconds
|
|
|
|
@ -1393,7 +1434,7 @@ void sendDataToServer(void) {
|
|
|
|
|
if (networkOption == UseWifi) {
|
|
|
|
|
postUsingWifi();
|
|
|
|
|
} else if (networkOption == UseCellular) {
|
|
|
|
|
postUsingCellular();
|
|
|
|
|
postUsingCellular(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -1472,26 +1513,54 @@ void networkSignalCheck() {
|
|
|
|
|
if (networkOption == UseWifi) {
|
|
|
|
|
Serial.printf("WiFi RSSI %d\n", wifiConnector.RSSI());
|
|
|
|
|
} else if (networkOption == UseCellular) {
|
|
|
|
|
auto result = cell->retrieveSignal();
|
|
|
|
|
auto result = cellularCard->retrieveSignal();
|
|
|
|
|
if (result.status != CellReturnStatus::Ok) {
|
|
|
|
|
agClient->setClientReady(false);
|
|
|
|
|
lastCellSignalQuality = 99;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Save last signal quality
|
|
|
|
|
lastCellSignalQuality = result.data;
|
|
|
|
|
|
|
|
|
|
if (result.data == 99) {
|
|
|
|
|
// 99 indicate cellular not attached to network
|
|
|
|
|
agClient->setClientReady(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
Serial.printf("Cellular signal strength %d\n", result.data);
|
|
|
|
|
|
|
|
|
|
Serial.printf("Cellular signal quality %d\n", result.data);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* If in 2 hours cellular client still not ready, then restart system
|
|
|
|
|
*/
|
|
|
|
|
void restartIfCeClientIssueOverTwoHours() {
|
|
|
|
|
if (agCeClientProblemDetectedTime > 0 &&
|
|
|
|
|
(MINUTES() - agCeClientProblemDetectedTime) >
|
|
|
|
|
TIMEOUT_WAIT_FOR_CELLULAR_MODULE_READY) {
|
|
|
|
|
// Give up wait
|
|
|
|
|
Serial.println("Rebooting because CE client issues for 2 hours detected");
|
|
|
|
|
int i = 3;
|
|
|
|
|
while (i != 0) {
|
|
|
|
|
if (ag->isOne()) {
|
|
|
|
|
String tmp = "Rebooting in " + String(i);
|
|
|
|
|
oledDisplay.setText("CE error", "since 2h", tmp.c_str());
|
|
|
|
|
} else {
|
|
|
|
|
Serial.println("Rebooting... " + String(i));
|
|
|
|
|
}
|
|
|
|
|
i = i - 1;
|
|
|
|
|
delay(1000);
|
|
|
|
|
}
|
|
|
|
|
oledDisplay.setBrightness(0);
|
|
|
|
|
esp_restart();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void networkingTask(void *args) {
|
|
|
|
|
// OTA check on boot
|
|
|
|
|
#ifdef ESP8266
|
|
|
|
|
// ota not supported
|
|
|
|
|
#else
|
|
|
|
|
// because cellular it takes too long, watchdog triggered
|
|
|
|
|
#ifndef ESP8266
|
|
|
|
|
checkForFirmwareUpdate();
|
|
|
|
|
checkForUpdateSchedule.update();
|
|
|
|
|
#endif
|
|
|
|
@ -1501,8 +1570,9 @@ void networkingTask(void *args) {
|
|
|
|
|
if (networkOption == UseCellular) {
|
|
|
|
|
Serial.println("Prepare first measures cycle to send on boot for 20s");
|
|
|
|
|
delay(20000);
|
|
|
|
|
networkSignalCheck();
|
|
|
|
|
newMeasurementCycle();
|
|
|
|
|
sendDataToServer();
|
|
|
|
|
postUsingCellular(true);
|
|
|
|
|
measurementSchedule.update();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -1521,14 +1591,43 @@ void networkingTask(void *args) {
|
|
|
|
|
}
|
|
|
|
|
else if (networkOption == UseCellular) {
|
|
|
|
|
if (agClient->isClientReady() == false) {
|
|
|
|
|
Serial.println("Cellular client not ready, ensuring connection...");
|
|
|
|
|
// Start time if value still default
|
|
|
|
|
if (agCeClientProblemDetectedTime == 0) {
|
|
|
|
|
agCeClientProblemDetectedTime = MINUTES();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Enable at command debug
|
|
|
|
|
agSerial->setDebug(true);
|
|
|
|
|
if (agClient->ensureClientConnection() == false) {
|
|
|
|
|
Serial.println("Cellular client connection not ready, retry in 5s...");
|
|
|
|
|
delay(5000);
|
|
|
|
|
|
|
|
|
|
// Check if cellular client not ready until certain time
|
|
|
|
|
// Redundant check in both task to make sure its executed
|
|
|
|
|
restartIfCeClientIssueOverTwoHours();
|
|
|
|
|
|
|
|
|
|
// Power cycling cellular module due to network issues for more than 1 hour
|
|
|
|
|
bool resetModule = true;
|
|
|
|
|
if ((MINUTES() - agCeClientProblemDetectedTime) >
|
|
|
|
|
TIME_TO_START_POWER_CYCLE_CELLULAR_MODULE) {
|
|
|
|
|
Serial.println("The CE client hasn't recovered in more than 1 hour, "
|
|
|
|
|
"performing a power cycle");
|
|
|
|
|
cellularCard->powerOff();
|
|
|
|
|
delay(2000);
|
|
|
|
|
cellularCard->powerOn();
|
|
|
|
|
delay(10000);
|
|
|
|
|
// no need to reset module when calling ensureClientConnection()
|
|
|
|
|
resetModule = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Attempt to reconnect
|
|
|
|
|
Serial.println("Cellular client not ready, ensuring connection...");
|
|
|
|
|
if (agClient->ensureClientConnection(resetModule) == false) {
|
|
|
|
|
Serial.println("Cellular client connection not ready, retry in 30s...");
|
|
|
|
|
delay(30000); // before retry, wait for 30s
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
agSerial->setDebug(false);
|
|
|
|
|
|
|
|
|
|
// Client is ready
|
|
|
|
|
agCeClientProblemDetectedTime = 0; // reset to default
|
|
|
|
|
agSerial->setDebug(false); // disable at command debug
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -1558,7 +1657,10 @@ void newMeasurementCycle() {
|
|
|
|
|
measurementCycleQueue.erase(measurementCycleQueue.begin());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get current measures
|
|
|
|
|
auto mc = measurements.getMeasures();
|
|
|
|
|
mc.signal = cellularCard->csqToDbm(lastCellSignalQuality); // convert to RSSI
|
|
|
|
|
|
|
|
|
|
measurementCycleQueue.push_back(mc);
|
|
|
|
|
Serial.println("New measurement cycle added to queue");
|
|
|
|
|
// Release mutex
|
|
|
|
|