Compare commits

...

37 Commits

Author SHA1 Message Date
227bd518c9 Fix response data is big 2024-12-09 18:22:05 +07:00
d0caee99aa Fix close file after write
Better error handling when write and load measurement file
comment out spiffs format
2024-12-08 03:09:28 +07:00
3162030800 Increase html font text size 2024-12-08 01:24:29 +07:00
6b6116ab6d Format csv file
remove pm1.0 and pm10
Add tvoc raw and nox raw
2024-12-08 01:23:51 +07:00
15dec1713d Display serial number to dashboard 2024-12-08 01:06:28 +07:00
70e626cbc9 Display serial number to dashboard 2024-12-08 01:05:43 +07:00
c003912d7a post storage and time return html 2024-12-08 01:05:21 +07:00
902797ceb0 Redirect root path to dashboard 2024-12-08 01:03:53 +07:00
430e908d88 Downloaded filename and AP ssid
ap ssid format have serial number
filname have last 4 digit serial number
2024-12-07 23:51:54 +07:00
6cb06986c3 Remove set time reduce by 17 minutes 2024-12-07 23:32:38 +07:00
e3156d438c Hotspot mode 2024-12-07 05:41:39 +07:00
4ae0206e6b Switch button position 2024-12-07 05:40:43 +07:00
83a4eddc37 Local storage mode using esp32 as AP 2024-12-07 05:39:59 +07:00
67b71f583b Add esp32 timestamp to dashboard page
Hotfix timestamp off by 17 minutes when set system time
2024-12-07 05:13:19 +07:00
e2798f1193 Dashboard page 2024-12-07 04:59:25 +07:00
f4357cca7e Fix timezone 2024-12-07 04:16:00 +07:00
20dcea20ad Notify write succes on oled
Disable led bar
Decrease oled brightness
2024-12-07 02:21:11 +07:00
cfe6fa9fd5 Seperate reset and set time endpoints 2024-12-07 02:05:43 +07:00
391186dd59 PM2.5 correction 2024-12-06 20:00:43 +07:00
a9f7f72871 Fix typo 2024-12-06 19:40:32 +07:00
9a3f71b33c Fix typo 2024-12-06 19:39:32 +07:00
d8f433bd3e WiFi reconnection with indicator 2024-12-06 19:38:34 +07:00
da414bf3fc Add tips to docs 2024-12-06 19:14:39 +07:00
d225af623a Add local storage docs 2024-12-06 19:06:28 +07:00
b7d22c2136 Fix SPIFFS usage percentage 2024-12-06 15:19:03 +07:00
6cd5e9f4b8 Handle if spiffs full 2024-12-06 04:26:18 +07:00
0cec71ceb6 Attempt connect to default wifi on boot
notify led when new measurement inserted to local storage
2024-12-06 03:57:49 +07:00
424d1d89fa Init timezone on boot 2024-12-06 03:55:55 +07:00
6186e3eca0 Fix get storage allocate based on size 2024-12-06 03:12:37 +07:00
b79c4e74e2 Add timestamp to local storage measurements 2024-12-06 03:00:23 +07:00
baa8601b5c Reset storage endpoints 2024-12-06 02:20:46 +07:00
a9fa7b6e63 Delete local storage function 2024-12-06 02:20:07 +07:00
1034f1892a Set and get system time 2024-12-06 02:19:25 +07:00
859c1a7e92 Local server to get local storage measurements 2024-12-05 04:09:17 +07:00
bce46445d6 Disable unnecessary scheduler 2024-12-05 04:07:57 +07:00
be7ca28a0e Scheduler to run save measurements to local storage 2024-12-05 04:07:00 +07:00
12e6f72b85 Save and get function local storage measurements 2024-12-05 04:05:42 +07:00
10 changed files with 424 additions and 100 deletions

BIN
docs/epoch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

View File

@ -0,0 +1,56 @@
*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

@ -9,10 +9,16 @@ 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(
@ -38,6 +44,13 @@ 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());
@ -68,4 +81,174 @@ 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,6 +19,9 @@ 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);
@ -29,10 +32,15 @@ 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,6 +93,7 @@ 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);
@ -116,23 +117,29 @@ 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 checkForUpdateSchedule(FIRMWARE_CHECK_FOR_UPDATE_MS, firmwareCheckForUpdate);
AgSchedule offlineStorage((2 * 60000), offlineStorageUpdate);
// 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());
@ -169,101 +176,34 @@ void setup() {
setMeasurementMaxPeriod();
// Comment below line to disable debug measurement readings
measurements.setDebug(true);
measurements.setDebug(false);
/** 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);
}
// Force to offline mode
configuration.setOfflineMode(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();
}
@ -271,8 +211,9 @@ void setup() {
void loop() {
/** Handle schedule */
dispLedSchedule.run();
configSchedule.run();
agApiPostSchedule.run();
// configSchedule.run();
// agApiPostSchedule.run();
offlineStorage.run();
if (configuration.hasSensorS8) {
co2Schedule.run();
@ -308,8 +249,8 @@ void loop() {
watchdogFeedSchedule.run();
/** Check for handle WiFi reconnect */
wifiConnector.handle();
// /** Check for handle WiFi reconnect */
// wifiConnector.handle();
/** factory reset handle */
factoryConfigReset();
@ -318,7 +259,7 @@ void loop() {
configUpdateHandle();
/** Firmware check for update handle */
checkForUpdateSchedule.run();
// checkForUpdateSchedule.run();
}
static void co2Update(void) {
@ -436,7 +377,7 @@ static void factoryConfigReset(void) {
WiFi.disconnect(true, true);
/** Reset local config */
configuration.reset();
// configuration.reset();
if (ag->isOne()) {
oledDisplay.setText("Factory reset", "successful", "");
@ -470,6 +411,8 @@ 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) {
@ -660,6 +603,7 @@ static void oneIndoorInit(void) {
/** Display init */
oledDisplay.begin();
oledDisplay.setBrightness(40);
/** Show boot display */
Serial.println("Firmware Version: " + ag->getVersion());
@ -963,7 +907,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;
}
@ -1219,3 +1163,12 @@ 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

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

View File

@ -2,6 +2,7 @@
#include "AgConfigure.h"
#include "AirGradient.h"
#include "App/AppDef.h"
#include "SPIFFS.h"
#define json_prop_pmFirmware "firmware"
#define json_prop_pm01Ae "pm01"
@ -1065,4 +1066,97 @@ JSONVar Measurements::buildPMS(AirGradient &ag, int ch, bool allCh, bool withTem
return pms;
}
void Measurements::setDebug(bool debug) { _debug = debug; }
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;
}

View File

@ -142,6 +142,10 @@ 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
*/
@ -173,6 +177,7 @@ 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,3 +85,25 @@ 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

@ -173,6 +173,9 @@ public:
*/
String deviceId(void);
void setCurrentTime(long epochTime);
String getCurrentTime();
private:
BoardType boardType;
};