Compare commits

..

1 Commits

Author SHA1 Message Date
280ea5e997 Prepared to release 3.1.14 2024-12-04 10:38:13 +07:00
16 changed files with 117 additions and 450 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

View File

@ -1,56 +0,0 @@
*This document to explain local storage mode - experimental*
## How it works?
1. Monitor directly goes to local storage mode
2. On boot, monitor will attempt to connect to default wifi. And if connected, mdns and local server will be enabled, otherwise it will ignore and continues the measurements
3. On display, when boot it will show the mode ("local storage mode") and wifi related scenario. After that, monitor will show the measurements dashboard
4. Measurement records to the local storage every two minutes that saved on CSV file in SPIFFs partition
5. Every successful writes, monitor will blink the most left led bar to *blue* twice, but if failed it will blink *red* twice. There are two possibilities for failed write, SPIFFs partition already full or out of heap memory when load the file.
6. There are 2 endpoinds added for this mode, download measurements from local storage and reset measurement (delete old measurements file and create new one) with new timestamp. Timestamp here to set the monitor system time.
**Notes**
1. Default wifi
- ssid ➝ `airgradient`
- password ➝ `cleanair`
2. Maximum measurements file is around 113kb. If assume each measurements is 60 bytes, with write schedule 2 minutes, SPIFFS will be full in around 5 days
3. WiFi connection attempt on boot wait for 10s before considering timeout
4. Tips. If monitor not connected to wifi on boot, no need to restart the monitor for reconnection, it will automatically connect to AP once it is available
### Local Storage Endpoinds
*Make sure monitor is connected to AP, and client also connect to it. And change the serial number on the url*
**Download measurements file**
To download measurements file from local storage, just directly access following url on the browser `http://airgradient_aaaaaaaa.local/storage`, and browser should automatically download the file.
**Reset measurements**
Execute below command in terminal
```sh
curl -X PUT -H "Content-Type: text/plain" -d '1733431986' http://airgradient_aaaaaaa.local/storage/reset
```
`1733431986` this data is the time that we want to set monitor system time to. Its in epoch time format and expecting UTC+0 timezone.
To get epoch time, access this url [https://www.unixtimestamp.com/](https://www.unixtimestamp.com/), and click copy button.
![unixtimestamp website](epoch.png)
### Example measurements file content
```csv
datetime,pm0.3 count,pm1,pm2.5,pm10,temp,rhum,co2,tvoc,nox
05/12 21:10:59,869.67,11.17,20.33,21.83,26.69,72.93,417,40,1
05/12 21:11:30,834.83,11.50,19.33,20.33,26.68,73.08,413,79,1
05/12 21:12:01,829.67,10.33,19.33,22.00,26.64,73.09,412,90,1
05/12 21:12:32,831.50,10.33,18.33,20.83,26.62,73.21,411,97,1
05/12 21:13:02,887.50,12.00,20.33,21.67,26.59,73.33,412,95,1
05/12 21:13:33,785.17,8.67,18.50,19.50,26.56,73.43,414,92,1
05/12 21:14:04,827.50,10.50,18.50,19.50,26.54,73.43,415,98,1
05/12 21:14:35,815.83,10.50,19.50,19.83,26.49,73.47,413,99,1
```

View File

@ -81,8 +81,7 @@ String OpenMetrics::getPayload(void) {
if (config.hasSensorPMS1) {
pm01 = measure.get(Measurements::PM01);
float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1);
pm25 = round(correctedPm);
pm25 = measure.get(Measurements::PM25);
pm10 = measure.get(Measurements::PM10);
pm03PCount = measure.get(Measurements::PM03_PC);
}

View File

@ -81,8 +81,7 @@ String OpenMetrics::getPayload(void) {
if (config.hasSensorPMS1) {
pm01 = measure.get(Measurements::PM01);
float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1);
pm25 = round(correctedPm);
pm25 = measure.get(Measurements::PM25);
pm10 = measure.get(Measurements::PM10);
pm03PCount = measure.get(Measurements::PM03_PC);
}

View File

@ -81,8 +81,7 @@ String OpenMetrics::getPayload(void) {
if (config.hasSensorPMS1) {
pm01 = measure.get(Measurements::PM01);
float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1);
pm25 = round(correctedPm);
pm25 = measure.get(Measurements::PM25);
pm10 = measure.get(Measurements::PM10);
pm03PCount = measure.get(Measurements::PM03_PC);
}

View File

@ -9,16 +9,10 @@ LocalServer::LocalServer(Stream &log, OpenMetrics &openMetrics,
LocalServer::~LocalServer() {}
bool LocalServer::begin(void) {
server.on("/", HTTP_GET, [this]() { _GET_root(); });
server.on("/measures/current", HTTP_GET, [this]() { _GET_measure(); });
server.on(openMetrics.getApi(), HTTP_GET, [this]() { _GET_metrics(); });
server.on("/config", HTTP_GET, [this]() { _GET_config(); });
server.on("/config", HTTP_PUT, [this]() { _PUT_config(); });
server.on("/dashboard", HTTP_GET, [this]() { _GET_dashboard(); });
server.on("/storage/download", HTTP_GET, [this]() { _GET_storage(); });
server.on("/storage/reset", HTTP_POST, [this]() { _POST_storage(); });
server.on("/timestamp", HTTP_POST, [this]() { _POST_time(); });
server.begin();
if (xTaskCreate(
@ -44,13 +38,6 @@ String LocalServer::getHostname(void) {
void LocalServer::_handle(void) { server.handleClient(); }
void LocalServer::_GET_root(void) {
String body = "If you are not redirected automatically, go to <a "
"href='http://192.168.4.1/dashboard'>dashboard</a>.";
server.send(302, "text/html", htmlResponse(body, true));
}
void LocalServer::_GET_config(void) {
if(ag->isOne()) {
server.send(200, "application/json", config.toString());
@ -81,174 +68,4 @@ void LocalServer::_GET_measure(void) {
server.send(200, "application/json", toSend);
}
void LocalServer::_GET_dashboard(void) {
String timestamp = ag->getCurrentTime();
server.send(200, "text/html", htmlDashboard(timestamp));
}
void LocalServer::_GET_storage(void) {
char *data = measure.getLocalStorage();
if (data != nullptr) {
String filename =
"measurements-" + ag->deviceId().substring(8) + ".csv"; // measurements-fdsa.csv
server.sendHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
server.send_P(200, "text/plain", data);
free(data);
} else {
server.send(204, "text/plain", "No data");
}
}
void LocalServer::_POST_storage(void) {
String body;
int statusCode = 200;
if (measure.resetLocalStorage()) {
body = "Success reset storage";
} else {
body = "Failed reset local storage, unknown error";
statusCode = 500;
}
body += ". Go to <a href='http://192.168.4.1/dashboard'>dashboard</a>.";
server.send(statusCode, "text/html", htmlResponse(body, false));
}
void LocalServer::_POST_time(void) {
String epochTime = server.arg(0);
Serial.printf("Received epoch: %s \n", epochTime.c_str());
if (epochTime.isEmpty()) {
server.send(400, "text/plain", "Time query not provided");
return;
}
long _epochTime = epochTime.toInt();
if (_epochTime == 0) {
server.send(400, "text/plain", "Time format is not in epoch time");
return;
}
ag->setCurrentTime(_epochTime);
String body = "Success set new time. Go to <a href='http://192.168.4.1/dashboard'>dashboard</a>.";
server.send(200, "text/html", htmlResponse(body, false));
}
void LocalServer::setFwMode(AgFirmwareMode fwMode) { this->fwMode = fwMode; }
String LocalServer::htmlDashboard(String timestamp) {
String page = "";
page += "<!DOCTYPE html>";
page += "<html lang=\"en\">";
page += "<head>";
page += " <meta charset=\"UTF-8\">";
page += " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">";
page += " <title>AirGradient Local Storage Mode</title>";
page += " <style>";
page += " body {";
page += " font-family: Arial, sans-serif;";
page += " display: flex;";
page += " flex-direction: column;";
page += " align-items: center;";
page += " margin-top: 50px;";
page += " }";
page += "";
page += " button {";
page += " display: block;";
page += " margin: 10px 0;";
page += " padding: 10px 20px;";
page += " font-size: 16px;";
page += " cursor: pointer;";
page += " }";
page += " .datetime-container {";
page += " display: flex;";
page += " align-items: center;";
page += " margin: 10px 0;";
page += " }";
page += " .datetime-container input[type=\"datetime-local\"] {";
page += " margin-left: 10px;";
page += " padding: 5px;";
page += " font-size: 16px;";
page += " }";
page += " button.reset-button {";
page += " background-color: red;";
page += " color: white;";
page += " border: none;";
page += " padding: 10px 20px;";
page += " font-size: 16px;";
page += " cursor: pointer;";
page += " }";
page += " .spacer {";
page += " height: 50px;";
page += " }";
page += " </style>";
page += "</head>";
page += "<body>";
page += " <h2>";
page += " Device Time: ";
page += timestamp;
page += " </h2>";
page += " <h2>";
page += " Serial Number: ";
page += ag->deviceId();
page += " </h2>";
page += " <form action=\"/storage/download\" method=\"GET\">";
page += " <button type=\"submit\">Download Measurements</button>";
page += " </form>";
page += " <form id=\"timestampForm\" method=\"POST\" action=\"/timestamp\">";
page += " <input type=\"datetime-local\" id=\"timestampInput\" required>";
page += " <button type=\"submit\">Set Timestamp</button>";
page += " <input type=\"hidden\" name=\"timestamp\" id=\"epochInput\">";
page += " </form>";
page += " <div class=\"spacer\"></div>";
page += " <form action=\"/storage/reset\" method=\"POST\"";
page += " onsubmit=\"return confirm('Are you sure you want to reset the measurements? "
"This action will permanently delete the existing measurement files!');\">";
page += " <button class=\"reset-button\" type=\"submit\">Reset Measurements</button>";
page += " </form>";
page += "</body>";
page += "<script>";
page += " document.querySelector('#timestampForm').onsubmit = function (event) {";
page += " const datetimeInput = document.querySelector('#timestampInput').value;";
page += " const localDate = new Date(datetimeInput);";
page += " const epochTimeUTC = Math.floor(Date.UTC(";
page += " localDate.getFullYear(),";
page += " localDate.getMonth(),";
page += " localDate.getDate(),";
page += " localDate.getHours(),";
page += " localDate.getMinutes()";
page += " ) / 1000);";
page += " document.querySelector('#epochInput').value = epochTimeUTC;";
page += " return true;";
page += " };";
page += "</script>";
page += "</html>";
return page;
}
String LocalServer::htmlResponse(String body, bool redirect) {
String page = "";
page += "<!DOCTYPE HTML>";
page += "<html lang=\"en-US\">";
page += " <head>";
page += "<style>";
page += "p { font-size: 22px; }";
page += "</style>";
page += " <meta charset=\"UTF-8\">";
if (redirect) {
page += " <meta http-equiv=\"refresh\" content=\"0;url=/dashboard\">";
}
page += " <title>Page Redirection</title>";
page += " </head>";
page += " <body>";
page += " <p>";
page += body;
page += " </p>";
page += " </body>";
page += "</html>";
return page;
}

View File

@ -19,9 +19,6 @@ private:
WebServer server;
AgFirmwareMode fwMode;
String htmlDashboard(String timestamp);
String htmlResponse(String body, bool redirect);
public:
LocalServer(Stream &log, OpenMetrics &openMetrics, Measurements &measure,
Configuration &config, WifiConnector& wifiConnector);
@ -32,15 +29,10 @@ public:
String getHostname(void);
void setFwMode(AgFirmwareMode fwMode);
void _handle(void);
void _GET_root(void);
void _GET_config(void);
void _PUT_config(void);
void _GET_metrics(void);
void _GET_measure(void);
void _GET_dashboard(void);
void _GET_storage(void);
void _POST_storage(void);
void _POST_time(void);
};
#endif /** _LOCAL_SERVER_H_ */

View File

@ -93,7 +93,6 @@ static AgFirmwareMode fwMode = FW_MODE_I_9PSL;
static bool ledBarButtonTest = false;
static String fwNewVersion;
static bool isLocalServerInitialized = false;
static void boardInit(void);
static void failedHandler(String msg);
@ -117,29 +116,23 @@ static void displayExecuteOta(OtaState state, String msg,
int processing);
static int calculateMaxPeriod(int updateInterval);
static void setMeasurementMaxPeriod();
static void offlineStorageUpdate();
AgSchedule dispLedSchedule(DISP_UPDATE_INTERVAL, updateDisplayAndLedBar);
// AgSchedule configSchedule(SERVER_CONFIG_SYNC_INTERVAL,
// configurationUpdateSchedule);
// AgSchedule agApiPostSchedule(SERVER_SYNC_INTERVAL, sendDataToServer);
AgSchedule configSchedule(SERVER_CONFIG_SYNC_INTERVAL,
configurationUpdateSchedule);
AgSchedule agApiPostSchedule(SERVER_SYNC_INTERVAL, sendDataToServer);
AgSchedule co2Schedule(SENSOR_CO2_UPDATE_INTERVAL, co2Update);
AgSchedule pmsSchedule(SENSOR_PM_UPDATE_INTERVAL, updatePm);
AgSchedule tempHumSchedule(SENSOR_TEMP_HUM_UPDATE_INTERVAL, tempHumUpdate);
AgSchedule tvocSchedule(SENSOR_TVOC_UPDATE_INTERVAL, updateTvoc);
AgSchedule watchdogFeedSchedule(60000, wdgFeedUpdate);
AgSchedule offlineStorage((2 * 60000), offlineStorageUpdate);
// AgSchedule checkForUpdateSchedule(FIRMWARE_CHECK_FOR_UPDATE_MS, firmwareCheckForUpdate);
AgSchedule checkForUpdateSchedule(FIRMWARE_CHECK_FOR_UPDATE_MS, firmwareCheckForUpdate);
void setup() {
/** Serial for print debug message */
Serial.begin(115200);
delay(100); /** For bester show log */
// Set timezone to UTC
setenv("TZ", "UTC", 1);
tzset();
/** Print device ID into log */
Serial.println("Serial nr: " + ag->deviceId());
@ -176,34 +169,101 @@ void setup() {
setMeasurementMaxPeriod();
// Comment below line to disable debug measurement readings
measurements.setDebug(false);
measurements.setDebug(true);
// Force to offline mode
configuration.setOfflineMode(true);
/** Connecting wifi */
bool connectToWifi = false;
if (ag->isOne()) {
/** Show message confirm offline mode, should me perform if LED bar button
* test pressed */
if (ledBarButtonTest == false) {
oledDisplay.setText(
"Press now for",
configuration.isOfflineMode() ? "online mode" : "offline mode", "");
uint32_t startTime = millis();
while (true) {
if (ag->button.getState() == ag->button.BUTTON_PRESSED) {
configuration.setOfflineMode(!configuration.isOfflineMode());
oledDisplay.setText(
"Offline Mode",
configuration.isOfflineMode() ? " = True" : " = False", "");
delay(1000);
break;
}
uint32_t periodMs = (uint32_t)(millis() - startTime);
if (periodMs >= 3000) {
break;
}
}
connectToWifi = !configuration.isOfflineMode();
} else {
configuration.setOfflineModeWithoutSave(true);
}
} else {
connectToWifi = true;
}
if (connectToWifi) {
apiClient.begin();
if (wifiConnector.connect()) {
if (wifiConnector.isConnected()) {
mdnsInit();
localServer.begin();
initMqtt();
sendDataToAg();
#ifdef ESP8266
// ota not supported
#else
firmwareCheckForUpdate();
checkForUpdateSchedule.update();
#endif
apiClient.fetchServerConfiguration();
configSchedule.update();
if (apiClient.isFetchConfigureFailed()) {
if (ag->isOne()) {
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
stateMachine.displayHandle(
AgStateMachineWiFiOkServerOkSensorConfigFailed);
} else {
stateMachine.displayClearAddToDashBoard();
}
}
stateMachine.handleLeds(
AgStateMachineWiFiOkServerOkSensorConfigFailed);
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
} else {
ledBarEnabledUpdate();
}
} else {
if (wifiConnector.isConfigurePorttalTimeout()) {
oledDisplay.showRebooting();
delay(2500);
oledDisplay.setText("", "", "");
ESP.restart();
}
}
}
}
/** Set offline mode without saving, cause wifi is not configured */
if (wifiConnector.hasConfigurated() == false) {
Serial.println("Set offline mode cause wifi is not configurated");
configuration.setOfflineModeWithoutSave(true);
}
/** Show display Warning up */
if (ag->isOne()) {
oledDisplay.setText("Warming Up", "Serial Number:", ag->deviceId().c_str());
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
Serial.println("Display brightness: " + String(configuration.getDisplayBrightness()));
oledDisplay.setBrightness(configuration.getDisplayBrightness());
}
String deviceId = ag->deviceId();
// Connect to Wi-Fi network with SSID and password
Serial.print("Setting AP (Access Point)…");
// Remove the password parameter, if you want the AP (Access Point) to be open
WiFi.softAP("ag_" + deviceId, "cleanair");
IPAddress IP = WiFi.softAPIP();
Serial.print("AP IP address: ");
Serial.println(IP);
Serial.printf("SSID: ag_%s\n", deviceId.c_str());
oledDisplay.setText("", "Offline Storage Mode", "");
delay(3000);
// mdnsInit();
localServer.begin();
// Update display and led bar after finishing setup to show dashboard
updateDisplayAndLedBar();
}
@ -211,9 +271,8 @@ void setup() {
void loop() {
/** Handle schedule */
dispLedSchedule.run();
// configSchedule.run();
// agApiPostSchedule.run();
offlineStorage.run();
configSchedule.run();
agApiPostSchedule.run();
if (configuration.hasSensorS8) {
co2Schedule.run();
@ -249,8 +308,8 @@ void loop() {
watchdogFeedSchedule.run();
// /** Check for handle WiFi reconnect */
// wifiConnector.handle();
/** Check for handle WiFi reconnect */
wifiConnector.handle();
/** factory reset handle */
factoryConfigReset();
@ -259,7 +318,7 @@ void loop() {
configUpdateHandle();
/** Firmware check for update handle */
// checkForUpdateSchedule.run();
checkForUpdateSchedule.run();
}
static void co2Update(void) {
@ -377,7 +436,7 @@ static void factoryConfigReset(void) {
WiFi.disconnect(true, true);
/** Reset local config */
// configuration.reset();
configuration.reset();
if (ag->isOne()) {
oledDisplay.setText("Factory reset", "successful", "");
@ -411,8 +470,6 @@ static void factoryConfigReset(void) {
static void wdgFeedUpdate(void) {
ag->watchdog.reset();
Serial.println("External watchdog feed!");
/** Log current free heap size */
Serial.printf("Free heap: %u\n", ESP.getFreeHeap());
}
static void ledBarEnabledUpdate(void) {
@ -603,7 +660,6 @@ static void oneIndoorInit(void) {
/** Display init */
oledDisplay.begin();
oledDisplay.setBrightness(40);
/** Show boot display */
Serial.println("Firmware Version: " + ag->getVersion());
@ -907,7 +963,7 @@ static void updateDisplayAndLedBar(void) {
if (configuration.isOfflineMode()) {
// Ignore network related status when in offline mode
stateMachine.displayHandle(AgStateMachineNormal);
// stateMachine.handleLeds(AgStateMachineNormal);
stateMachine.handleLeds(AgStateMachineNormal);
return;
}
@ -1163,12 +1219,3 @@ int calculateMaxPeriod(int updateInterval) {
// 0.8 is 80% reduced interval for max period
return (SERVER_SYNC_INTERVAL - (SERVER_SYNC_INTERVAL * 0.8)) / updateInterval;
}
void offlineStorageUpdate() {
if (measurements.saveLocalStorage(*ag, configuration)) {
oledDisplay.setText("", "New Measurements", "");
} else {
oledDisplay.setText("Failed write", "Measurements", "");
}
delay(1200);
}

View File

@ -81,10 +81,7 @@ String OpenMetrics::getPayload(void) {
measure.getFloat(Measurements::Humidity, 2)) /
2.0f;
pm01 = (measure.get(Measurements::PM01, 1) + measure.get(Measurements::PM01, 2)) / 2.0f;
float correctedPm25_1 = measure.getCorrectedPM25(*ag, config, false, 1);
float correctedPm25_2 = measure.getCorrectedPM25(*ag, config, false, 2);
float correctedPm25 = (correctedPm25_1 + correctedPm25_2) / 2.0f;
pm25 = round(correctedPm25);
pm25 = (measure.get(Measurements::PM25, 1) + measure.get(Measurements::PM25, 2)) / 2.0f;
pm10 = (measure.get(Measurements::PM10, 1) + measure.get(Measurements::PM10, 2)) / 2.0f;
pm03PCount =
(measure.get(Measurements::PM03_PC, 1) + measure.get(Measurements::PM03_PC, 2)) / 2.0f;
@ -97,8 +94,7 @@ String OpenMetrics::getPayload(void) {
if (config.hasSensorPMS1) {
pm01 = measure.get(Measurements::PM01);
float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1);
pm25 = round(correctedPm);
pm25 = measure.get(Measurements::PM25);
pm10 = measure.get(Measurements::PM10);
pm03PCount = measure.get(Measurements::PM03_PC);
}
@ -107,8 +103,7 @@ String OpenMetrics::getPayload(void) {
_temp = measure.getFloat(Measurements::Temperature, 1);
_hum = measure.getFloat(Measurements::Humidity, 1);
pm01 = measure.get(Measurements::PM01, 1);
float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1);
pm25 = round(correctedPm);
pm25 = measure.get(Measurements::PM25, 1);
pm10 = measure.get(Measurements::PM10, 1);
pm03PCount = measure.get(Measurements::PM03_PC, 1);
}
@ -116,8 +111,7 @@ String OpenMetrics::getPayload(void) {
_temp = measure.getFloat(Measurements::Temperature, 2);
_hum = measure.getFloat(Measurements::Humidity, 2);
pm01 = measure.get(Measurements::PM01, 2);
float correctedPm = measure.getCorrectedPM25(*ag, config, false, 2);
pm25 = round(correctedPm);
pm25 = measure.get(Measurements::PM25, 2);
pm10 = measure.get(Measurements::PM10, 2);
pm03PCount = measure.get(Measurements::PM03_PC, 2);
}

View File

@ -1,5 +1,5 @@
name=AirGradient Air Quality Sensor
version=3.1.13
version=3.1.14
author=AirGradient <support@airgradient.com>
maintainer=AirGradient <support@airgradient.com>
sentence=ESP32-C3 / ESP8266 library for air quality monitor measuring PM, CO2, Temperature, TVOC and Humidity with OLED display.

View File

@ -253,7 +253,7 @@ void Configuration::loadConfig(void) {
}
file.close();
} else {
// SPIFFS.format();
SPIFFS.format();
}
#endif
toConfig(buf);

View File

@ -394,7 +394,7 @@ void OledDisplay::showDashboard(const char *status) {
if (config.isTemperatureUnitInF()) {
snprintf(strBuf, sizeof(strBuf), "T:%0.1f F", utils::degreeC_To_F(temp));
} else {
snprintf(strBuf, sizeof(strBuf), "T:%0.1f C", temp);
snprintf(strBuf, sizeof(strBuf), "T:%0.f1 C", temp);
}
} else {
if (config.isTemperatureUnitInF()) {

View File

@ -2,7 +2,6 @@
#include "AgConfigure.h"
#include "AirGradient.h"
#include "App/AppDef.h"
#include "SPIFFS.h"
#define json_prop_pmFirmware "firmware"
#define json_prop_pm01Ae "pm01"
@ -190,7 +189,7 @@ bool Measurements::update(MeasurementType type, int val, int ch) {
// Sanity check if measurement type is defined for integer data type or not
if (temporary == nullptr) {
Serial.printf("%s is not defined for integer data type\n", measurementTypeStr(type).c_str());
Serial.printf("%s is not defined for integer data type\n", measurementTypeStr(type));
// TODO: Just assert?
return false;
}
@ -229,7 +228,7 @@ bool Measurements::update(MeasurementType type, int val, int ch) {
// Calculate average based on how many elements on the list
temporary->update.avg = temporary->sumValues / (float)temporary->listValues.size();
if (_debug) {
Serial.printf("%s{%d}: %.2f\n", measurementTypeStr(type).c_str(), ch, temporary->update.avg);
Serial.printf("%s{%d}: %.2f\n", measurementTypeStr(type), ch, temporary->update.avg);
}
return true;
@ -261,7 +260,7 @@ bool Measurements::update(MeasurementType type, float val, int ch) {
// Sanity check if measurement type is defined for float data type or not
if (temporary == nullptr) {
Serial.printf("%s is not defined for float data type\n", measurementTypeStr(type).c_str());
Serial.printf("%s is not defined for float data type\n", measurementTypeStr(type));
// TODO: Just assert?
return false;
}
@ -300,7 +299,7 @@ bool Measurements::update(MeasurementType type, float val, int ch) {
// Calculate average based on how many elements on the list
temporary->update.avg = temporary->sumValues / (float)temporary->listValues.size();
if (_debug) {
Serial.printf("%s{%d}: %.2f\n", measurementTypeStr(type).c_str(), ch, temporary->update.avg);
Serial.printf("%s{%d}: %.2f\n", measurementTypeStr(type), ch, temporary->update.avg);
}
return true;
@ -349,7 +348,7 @@ int Measurements::get(MeasurementType type, int ch) {
// Sanity check if measurement type is defined for integer data type or not
if (temporary == nullptr) {
Serial.printf("%s is not defined for integer data type\n", measurementTypeStr(type).c_str());
Serial.printf("%s is not defined for integer data type\n", measurementTypeStr(type));
// TODO: Just assert?
return false;
}
@ -384,7 +383,7 @@ float Measurements::getFloat(MeasurementType type, int ch) {
// Sanity check if measurement type is defined for float data type or not
if (temporary == nullptr) {
Serial.printf("%s is not defined for float data type\n", measurementTypeStr(type).c_str());
Serial.printf("%s is not defined for float data type\n", measurementTypeStr(type));
// TODO: Just assert?
return false;
}
@ -435,7 +434,7 @@ float Measurements::getAverage(MeasurementType type, int ch) {
// Sanity check if measurement type is not defined
if (measurementAverage == -1000) {
Serial.printf("ERROR! %s is not defined on get average value function\n", measurementTypeStr(type).c_str());
Serial.printf("ERROR! %s is not defined on get average value function\n", measurementTypeStr(type));
delay(1000);
assert(0);
}
@ -1066,97 +1065,4 @@ JSONVar Measurements::buildPMS(AirGradient &ag, int ch, bool allCh, bool withTem
return pms;
}
void Measurements::setDebug(bool debug) { _debug = debug; }
bool Measurements::resetLocalStorage() {
if (!SPIFFS.remove(FILE_PATH)) {
Serial.println("Failed reset local storage");
return false;
}
Serial.println("Success reset local storage");
return true;
}
bool Measurements::saveLocalStorage(AirGradient &ag, Configuration &config) {
int spiffUsed = ((float)SPIFFS.usedBytes() / (float)SPIFFS.totalBytes()) * 100.0;
Serial.printf("%d | %d\n", SPIFFS.totalBytes(), SPIFFS.usedBytes());
Serial.printf("SPIFF used %d%%\n", spiffUsed);
if (spiffUsed > 98) {
Serial.println("SPIFF used already on maximum");
return false;
}
File file;
if (!SPIFFS.exists(FILE_PATH)) {
file = SPIFFS.open(FILE_PATH, FILE_APPEND, true);
file.println(
"datetime,pm0.3 count,pm2.5,temp,rhum,co2,tvoc,tvoc raw,nox,nox raw"); // csv header
Serial.println("New measurements file created");
} else {
file = SPIFFS.open(FILE_PATH, FILE_APPEND, false);
}
float pm25 = getCorrectedPM25(ag, config, true);
// Save new measurements
char buff[100] = {0};
sprintf(buff, "%s,%.2f,%.2f,%.2f,%.2f,%d,%d,%d,%d,%d\n\0", ag.getCurrentTime().c_str(),
ag.round2(_pm_03_pc[0].update.avg), ag.round2(pm25),
ag.round2(_temperature[0].update.avg), ag.round2(_humidity[0].update.avg),
(int)round(_co2.update.avg), (int)round(_tvoc.update.avg),
(int)round(_tvoc_raw.update.avg), (int)round(_nox.update.avg),
(int)round(_nox_raw.update.avg));
size_t len = strlen(buff);
if (file.write((const uint8_t *)buff, len) != len) {
Serial.println("Write new measurements failed!");
file.close();
return false;
}
file.close();
Serial.println("Success save measurements to local storage");
return true;
}
char *Measurements::getLocalStorage() {
char *buf = nullptr;
bool success = false;
if (!SPIFFS.exists(FILE_PATH)) {
Serial.println("No measurements file exists yet");
return nullptr;
}
File file = SPIFFS.open(FILE_PATH);
if (file && !file.isDirectory()) {
// Allocate memory
buf = new char[file.size() + 1];
if (buf == nullptr) {
return nullptr;
}
memset(buf, 0, file.size() + 1);
// Retrieve data from the file
if (file.readBytes(buf, file.size()) != file.size()) {
Serial.println("Reading measurements file: failed - size not match");
} else {
Serial.println("Reading measurements file: success");
success = true;
}
file.close();
}
if (!success) {
Serial.println("Reading measurements file failed");
if (buf != nullptr) {
delete buf;
}
return nullptr;
}
// NOTE: Don't forget to free
return buf;
}
void Measurements::setDebug(bool debug) { _debug = debug; }

View File

@ -142,10 +142,6 @@ public:
String toString(bool localServer, AgFirmwareMode fwMode, int rssi, AirGradient &ag,
Configuration &config);
bool resetLocalStorage();
bool saveLocalStorage(AirGradient &ag, Configuration &config);
char *getLocalStorage();
/**
* Set to true if want to debug every update value
*/
@ -177,7 +173,6 @@ private:
IntegerValue _pm_10_pc[2]; // particle count 10
bool _debug = false;
const char *FILE_PATH = "/measurements.csv"; // Local storage file path
/**
* @brief Get PMS5003 firmware version string

View File

@ -85,25 +85,3 @@ String AirGradient::deviceId(void) {
mac.toLowerCase();
return mac;
}
void AirGradient::setCurrentTime(long epochTime) {
// set current day/time
struct timeval tv;
tv.tv_sec = epochTime; // - 1020; // 17 minutes // don't know why it always off by 17 minutes
settimeofday(&tv, NULL);
Serial.println(epochTime);
Serial.printf("Set current time to %s\n", getCurrentTime().c_str());
}
String AirGradient::getCurrentTime() {
// Get time
time_t now;
char strftime_buf[64];
struct tm timeinfo;
time(&now);
// Format
localtime_r(&now, &timeinfo);
strftime(strftime_buf, sizeof(strftime_buf), "%d/%m %H:%M:%S", &timeinfo);
return String(strftime_buf);
}

View File

@ -15,7 +15,7 @@
#include "Main/utils.h"
#ifndef GIT_VERSION
#define GIT_VERSION "3.1.13-snap"
#define GIT_VERSION "3.1.14-snap"
#endif
/**
@ -173,9 +173,6 @@ public:
*/
String deviceId(void);
void setCurrentTime(long epochTime);
String getCurrentTime();
private:
BoardType boardType;
};