mirror of
https://github.com/airgradienthq/arduino.git
synced 2025-06-26 08:11:33 +02:00
Compare commits
37 Commits
3.3.1
...
feat/local
Author | SHA1 | Date | |
---|---|---|---|
227bd518c9 | |||
d0caee99aa | |||
3162030800 | |||
6b6116ab6d | |||
15dec1713d | |||
70e626cbc9 | |||
c003912d7a | |||
902797ceb0 | |||
430e908d88 | |||
6cb06986c3 | |||
e3156d438c | |||
4ae0206e6b | |||
83a4eddc37 | |||
67b71f583b | |||
e2798f1193 | |||
f4357cca7e | |||
20dcea20ad | |||
cfe6fa9fd5 | |||
391186dd59 | |||
a9f7f72871 | |||
9a3f71b33c | |||
d8f433bd3e | |||
da414bf3fc | |||
d225af623a | |||
b7d22c2136 | |||
6cd5e9f4b8 | |||
0cec71ceb6 | |||
424d1d89fa | |||
6186e3eca0 | |||
b79c4e74e2 | |||
baa8601b5c | |||
a9fa7b6e63 | |||
1034f1892a | |||
859c1a7e92 | |||
bce46445d6 | |||
be7ca28a0e | |||
12e6f72b85 |
BIN
docs/epoch.png
Normal file
BIN
docs/epoch.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 221 KiB |
56
docs/local-storage-experimental.md
Normal file
56
docs/local-storage-experimental.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
```
|
||||
|
@ -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;
|
||||
}
|
@ -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_ */
|
||||
|
@ -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);
|
||||
}
|
@ -253,7 +253,7 @@ void Configuration::loadConfig(void) {
|
||||
}
|
||||
file.close();
|
||||
} else {
|
||||
SPIFFS.format();
|
||||
// SPIFFS.format();
|
||||
}
|
||||
#endif
|
||||
toConfig(buf);
|
||||
|
@ -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;
|
||||
}
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
@ -173,6 +173,9 @@ public:
|
||||
*/
|
||||
String deviceId(void);
|
||||
|
||||
void setCurrentTime(long epochTime);
|
||||
String getCurrentTime();
|
||||
|
||||
private:
|
||||
BoardType boardType;
|
||||
};
|
||||
|
Reference in New Issue
Block a user