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
50 changed files with 1341 additions and 2423 deletions

View File

@ -17,14 +17,11 @@ jobs:
- "esp32:esp32:esp32c3"
include:
- fqbn: "esp8266:esp8266:d1_mini"
core: "esp8266:esp8266"
core_version: "3.1.2"
core: "esp8266:esp8266@3.1.2"
core_url: "https://arduino.esp8266.com/stable/package_esp8266com_index.json"
- fqbn: "esp32:esp32:esp32c3"
core: "esp32:esp32"
core_version: "2.0.17"
core_url: "https://espressif.github.io/arduino-esp32/package_esp32_index.json"
board_options: "JTAGAdapter=default,CDCOnBoot=cdc,PartitionScheme=min_spiffs,CPUFreq=160,FlashMode=qio,FlashFreq=80,FlashSize=4M,UploadSpeed=921600,DebugLevel=verbose,EraseFlash=none"
core: "esp32:esp32@2.0.11"
exclude:
- example: "BASIC"
fqbn: "esp32:esp32:esp32c3"
@ -34,30 +31,30 @@ jobs:
fqbn: "esp32:esp32:esp32c3"
- example: "OneOpenAir"
fqbn: "esp8266:esp8266:d1_mini"
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
with:
fetch-depth: 0
submodules: 'true'
- uses: arduino/compile-sketches@v1.1.2
with:
fqbn: ${{ matrix.fqbn }}
sketch-paths: |
examples/${{ matrix.example }}
libraries: |
- source-path: ./
cli-compile-flags: |
- --warnings
- none
- --board-options
- "${{ matrix.board_options }}"
platforms: |
- name: ${{ matrix.core }}
version: ${{ matrix.core_version}}
source-url: ${{ matrix.core_url }}
enable-deltas-report: true
- uses: actions/checkout@v4
- run:
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh |
sh -s 0.35.3
- run: bin/arduino-cli --verbose core install '${{ matrix.core }}'
--additional-urls '${{ matrix.core_url }}'
- run: bin/arduino-cli --verbose lib install
WiFiManager@2.0.16-rc.2
Arduino_JSON@0.2.0
U8g2@2.34.22
# In some cases, actions/checkout@v4 will check out a detached HEAD; for
# example, this happens on pull request events, where an hypothetical
# PR merge commit is checked out. This tends to confuse
# `arduino-cli lib install --git-url`, making it fail with errors such as:
# Error installing Git Library: Library install failed: object not found
# Create and check out a dummy branch to work around this issue.
- run: git checkout -b check
- run: bin/arduino-cli --verbose lib install --git-url .
env:
ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL: "true"
- run: bin/arduino-cli --verbose compile 'examples/${{ matrix.example }}'
--fqbn '${{ matrix.fqbn }}' --board-options '${{ matrix.board_options }}'
# TODO: at this point it would be a good idea to run some smoke tests on
# the resulting image (e.g. that it boots successfully and sends metrics)
# but that would either require a high fidelity device emulator, or a

5
.gitignore vendored
View File

@ -3,8 +3,3 @@ build
.vscode
/.idea/
.pio
.cache
.clangd
logs
gen_compile_commands.py
compile_commands.json

6
.gitmodules vendored
View File

@ -1,6 +0,0 @@
[submodule "src/Libraries/airgradient-client"]
path = src/Libraries/airgradient-client
url = ../../airgradienthq/airgradient-client.git
[submodule "src/Libraries/airgradient-ota"]
path = src/Libraries/airgradient-ota
url = ../../airgradienthq/airgradient-ota.git

BIN
docs/epoch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

View File

@ -1,105 +0,0 @@
# How to compile AirGradient firmware on Arduino IDE
## Prequisite
Arduino IDE version 2.x ([download](https://www.arduino.cc/en/software))
> For AirGradient model ONE and Open Air, the codebase **WILL NOT** work on the latest major version of arduino-esp32 which is *3.x* . This related to when installing "esp32 by Espressif Systems" in board manager. Instead use version **2.0.17**, please follow the first step carefully.
## Steps for ESP32C3 based board (ONE and Open Air Model)
1. Install "esp32 by Espressif Systems" in board manager with version **2.0.17** (Tools ➝ Board ➝ Boards Manager ➝ search for `"espressif"`)
![board manager](images/esp32-board.png)
2. Install AirGradient library
#### Version < 3.2.0
Using library manager install the latest version (Tools ➝ Manage Libraries... ➝ search for `"airgradient"`)
![Aigradient Library](images/ag-lib.png)
#### Version >= 3.3.0
- From your terminal, go to Arduino libraries folder (windows and mac: `Documents/Arduino/libraries` or linux: `~/Arduino/Libraries`).
- With **git** cli, execute this command `git clone --recursive https://github.com/airgradienthq/arduino.git AirGradient_Air_Quality_Sensor`
- Restart Arduino IDE
3. On tools tab, follow settings below
```
Board ➝ ESP32C3 Dev Module
USB CDC On Boot ➝ Enabled
CPU Frequency ➝ 160MHz (WiFi)
Core Debug Level ➝ Info
Erase All Flash Before Sketch Upload ➝ Enabled (or choose as needed)
Flash Frequency ➝ 80MHz
Flash Mode ➝ QIO
Flash Size ➝ 4MB (32Mb)
JTAG Adapter ➝ Disabled
Partition Scheme ➝ Minimal SPIFFS (1.9MB APP with OTA/190KB SPIFFS)
Upload Speed ➝ 921600
```
4. Open sketch to compile (File ➝ Examples ➝ AirGradient Air Quality Sensor ➝ OneOpenAir). This sketch for AirGradient ONE and Open Air monitor model
5. Compile
![compiled esp32](images/compiled.png)
## Steps for ESP8266 based board (DIY model)
1. Add esp8266 board by adding http://arduino.esp8266.com/stable/package_esp8266com_index.json into Additional Board Manager URLs field (File ➝ Preferences ➝ Additional boards manager URLs)
![additional-board](images/additional-board.png)
2. Install esp8266 board on board manager with version **3.1.2** (Tools ➝ Board ➝ Boards Manager ➝ search for `"esp8266"`)
![board manager](images/esp8266-board.png)
3. Install AirGradient library on library manager using the latest version (Tools ➝ Manage Libraries... ➝ search for `"airgradient"`)
![Aigradient Library](images/ag-lib.png)
4. On tools tab, set board to `LOLIN(WEMOS) D1 R2 & mini`, and let other settings to default
![settings esp8266](images/settings-esp8266.png)
5. Open sketch to compile (File ➝ Examples ➝ AirGradient Air Quality Sensor ➝ `<Model Option>`). Depends on the DIY model, either `BASIC`, `DiyProIndoorV3_3` and `DiyProIndoorV4_2`
6. Compile
![compiled esp8266](images/compiled-esp8266.png)
## Possible Issues
### Linux (Debian)
ModuleNotFoundError: No module named serial
![Linux Failed](images/linux-failed.png)
Make sure python pyserial module installed globally in the environment by executing:
`$ sudo apt install -y python3-pyserial`
or
`$ pip install pyserial`
Choose based on how python installed on your machine. But most user, using `apt` is better.
## How to contribute
The instructions above are the instructions for how to build an official release of the AirGradient firmware using the Arduino IDE. If you intend to make changes that will you intent to contribute back to the main project, instead of installing the AirGradient library, check out the repo at `Documents/Arduino/libraries` (for Windows and Mac), or `~/Arduino/Libraries` (Linux). If you installed the library, you can remove it from the library manager in the Arduino IDE, or just delete the directory.
**NOTE:** When cloning the repository, for version >= 3.3.0 it has submodule, please use `--recursive` flag like this: `git clone --recursive https://github.com/airgradienthq/arduino.git AirGradient_Air_Quality_Sensor`
Please follow github [contributing to a project](https://docs.github.com/en/get-started/exploring-projects-on-github/contributing-to-a-project) tutorial to contribute to this project.
There are 2 environment options to compile this project, PlatformIO and ArduinoIDE.
- For PlatformIO, it should work out of the box
- For arduino, files in `src` folder and also from `Examples` can be modified at `Documents/Arduino/libraries` for Windows and Mac, and `~/Arduino/Libraries` for Linux

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

View File

@ -2,7 +2,7 @@
From [firmware version 3.0.10](firmwares) onwards, the AirGradient ONE and Open Air monitors have below API available.
### Discovery
#### Discovery
The monitors run a mDNS discovery. So within the same network, the monitor can be accessed through:
@ -11,7 +11,7 @@ http://airgradient_{{serialnumber}}.local
The following requests are possible:
### Get Current Air Quality (GET)
#### Get Current Air Quality (GET)
With the path "/measures/current" you can get the current air quality data.
@ -80,7 +80,7 @@ You get the following response:
Compensated values apply correction algorithms to make the sensor values more accurate. Temperature and relative humidity correction is only applied on the outdoor monitor Open Air but the properties _compensated will still be send also for the indoor monitor AirGradient ONE.
### Get Configuration Parameters (GET)
#### Get Configuration Parameters (GET)
"/config" path returns the current configuration of the monitor.
@ -93,7 +93,6 @@ Compensated values apply correction algorithms to make the sensor values more ac
"tvocLearningOffset": 12,
"noxLearningOffset": 12,
"mqttBrokerUrl": "",
"httpDomain": "",
"temperatureUnit": "c",
"configurationControl": "local",
"postDataToAirGradient": true,
@ -112,7 +111,7 @@ Compensated values apply correction algorithms to make the sensor values more ac
}
```
### Set Configuration Parameters (PUT)
#### Set Configuration Parameters (PUT)
Configuration parameters can be changed with a PUT request to the monitor, e.g.
@ -132,11 +131,11 @@ Example to set monitor to Celsius
``` -d "{\"param\":\"value\"}" ```
### Avoiding Conflicts with Configuration on AirGradient Server
#### Avoiding Conflicts with Configuration on AirGradient Server
If the monitor is set up on the AirGradient dashboard, it will also receive the configuration parameters from there. In case you do not want this, please set `configurationControl` to `local`. In case you set it to `cloud` and want to change it to `local`, you need to make a factory reset.
### Configuration Parameters (GET/PUT)
#### Configuration Parameters (GET/PUT)
| Properties | Description | Type | Accepted Values | Example |
|-----------------------------------|:-----------------------------------------------------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------|
@ -147,8 +146,7 @@ If the monitor is set up on the AirGradient dashboard, it will also receive the
| `displayBrightness` | Brightness of the Display. | Number | 0-100 | `{"displayBrightness": 50}` |
| `ledBarBrightness` | Brightness of the LEDBar. | Number | 0-100 | `{"ledBarBrightness": 40}` |
| `abcDays` | Number of days for CO2 automatic baseline calibration. | Number | Maximum 200 days. Default 8 days. | `{"abcDays": 8}` |
| `mqttBrokerUrl` | MQTT broker URL. | String | Maximum 255 characters. Set value to empty string to disable mqtt connection. | `{"mqttBrokerUrl": "mqtt://192.168.0.18:1883"}` |
| `httpDomain` | Domain name for http request. (version > 3.3.2) | String | Maximum 255 characters. Set value to empty string to set http domain to default airgradient | `{"httpDomain": "sub.domain.com"}` |
| `mqttBrokerUrl` | MQTT broker URL. | String | | `{"mqttBrokerUrl": "mqtt://192.168.0.18:1883"}` |
| `temperatureUnit` | Temperature unit shown on the display. | String | `c` or `C`: Degree Celsius °C <br>`f` or `F`: Degree Fahrenheit °F | `{"temperatureUnit": "c"}` |
| `configurationControl` | The configuration source of the device. | String | `both`: Accept local and cloud configuration <br>`local`: Accept only local configuration <br>`cloud`: Accept only cloud configuration | `{"configurationControl": "both"}` |
| `postDataToAirGradient` | Send data to AirGradient cloud. | Boolean | `true`: Enabled <br>`false`: Disabled | `{"postDataToAirGradient": true}` |
@ -156,18 +154,15 @@ If the monitor is set up on the AirGradient dashboard, it will also receive the
| `ledBarTestRequested` | Can be set to trigger a test. | Boolean | `true` : LEDs will run test sequence | `{"ledBarTestRequested": true}` |
| `noxLearningOffset` | Set NOx learning gain offset. | Number | 0-720 (default 12) | `{"noxLearningOffset": 12}` |
| `tvocLearningOffset` | Set VOC learning gain offset. | Number | 0-720 (default 12) | `{"tvocLearningOffset": 12}` |
| `monitorDisplayCompensatedValues` | Set the display show the PM value with/without compensate value (only on 3.1.9) | Boolean | `false`: Without compensate (default) <br> `true`: with compensate | `{"monitorDisplayCompensatedValues": false }` |
| `corrections` | Sets correction options to display and measurement values on local server response. (version >= 3.1.11) | Object | _see corrections section_ | _see corrections section_ |
| `offlineMode` | Set monitor to run without WiFi. | Boolean | `false`: Disabled (default) <br> `true`: Enabled | `{"offlineMode": true}` |
| `monitorDisplayCompensatedValues` | Set the display show the PM value with/without compensate value (only on [3.1.9]()) | Boolean | `false`: Without compensate (default) <br> `true`: with compensate | `{"monitorDisplayCompensatedValues": false }` |
| `corrections` | Sets correction options to display and measurement values on local server response. (version >= [3.1.11]()) | Object | _see corrections section_ | _see corrections section_ |
**Notes**
- `offlineMode` : the device will disable all network operation, and only show measurements on the display and ledbar; Read-Only; Change can be apply using reset button on boot.
- `disableCloudConnection` : disable every request to AirGradient server, means features like post data to AirGradient server, configuration from AirGradient server and automatic firmware updates are disabled. This configuration overrides `configurationControl` and `postDataToAirGradient`; Read-Only; Change can be apply from wifi setup webpage.
#### Corrections
### Corrections
The `corrections` object allows configuring PM2.5, Temperature and Humidity correction algorithms and parameters locally. This affects both the display, local server response and open metrics values.
The `corrections` object allows configuring PM2.5 correction algorithms and parameters locally. This affects both the display and local server response values.
Example correction configuration:
@ -181,29 +176,11 @@ Example correction configuration:
"scalingFactor": 0,
"useEpa2021": false
}
},
"atmp": {
"correctionAlgorithm": "<Option In String>",
"slr": {
"intercept": 0,
"scalingFactor": 0
}
},
"rhum": {
"correctionAlgorithm": "<Option In String>",
"slr": {
"intercept": 0,
"scalingFactor": 0
}
},
}
}
}
```
#### PM 2.5
Field Name: `pm02`
| Algorithm | Value | Description | SLR required |
|------------|-------------|------|---------|
| Raw | `"none"` | No correction (default) | No |
@ -237,23 +214,3 @@ curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header '
```bash
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20240104","slr":{"intercept":0,"scalingFactor":0.02896,"useEpa2021":true}}}}'
```
#### Temperature & Humidity
Field Name:
- Temperature: `atmp`
- Humidity: `rhum`
| Algorithm | Value | Description | SLR required |
|------------|-------------|------|---------|
| Raw | `"none"` | No correction (default) | No |
| AirGradient Standard Correction | `"ag_pms5003t_2024"` | Using standard airgradient correction (for outdoor monitor)| No |
| Custom | `"custom"` | custom corrections constant, set `intercept` and `scalingFactor` manually | Yes |
*Table above apply for both Temperature and Humidity*
**Example**
```bash
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"atmp":{"correctionAlgorithm":"custom","slr":{"intercept":0.2,"scalingFactor":1.1}}}}'
```

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

@ -12,8 +12,10 @@ Outdoor Monitor: https://www.airgradient.com/outdoor/
Build Instructions:
https://www.airgradient.com/documentation/diy-v4/
Compile Instructions:
https://github.com/airgradienthq/arduino/blob/master/docs/howto-compile.md
Please make sure you have esp8266 board manager installed. Tested with
version 3.1.2.
Set board to "LOLIN(WEMOS) D1 R2 & mini"
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
can be set through the AirGradient dashboard.
@ -53,7 +55,7 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
static AirGradient ag(DIY_BASIC);
static Configuration configuration(Serial);
static AgApiClient apiClient(Serial, configuration);
static Measurements measurements(configuration);
static Measurements measurements;
static OledDisplay oledDisplay(configuration, measurements, Serial);
static StateMachine stateMachine(oledDisplay, Serial, measurements,
configuration);
@ -122,7 +124,6 @@ void setup() {
apiClient.setAirGradient(&ag);
openMetrics.setAirGradient(&ag);
localServer.setAirGraident(&ag);
measurements.setAirGradient(&ag);
/** Example set custom API root URL */
// apiClient.setApiRoot("https://example.custom.api");
@ -148,12 +149,9 @@ void setup() {
initMqtt();
sendDataToAg();
if (configuration.getConfigurationControl() !=
ConfigurationControl::ConfigurationControlLocal) {
apiClient.fetchServerConfiguration();
}
apiClient.fetchServerConfiguration();
configSchedule.update();
if (apiClient.isFetchConfigurationFailed()) {
if (apiClient.isFetchConfigureFailed()) {
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
stateMachine.displayHandle(
@ -318,7 +316,7 @@ static void mqttHandle(void) {
}
if (mqttClient.isConnected()) {
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI());
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI(), ag, configuration);
String topic = "airgradient/readings/" + ag.deviceId();
if (mqttClient.publish(topic.c_str(), payload.c_str(), payload.length())) {
Serial.println("MQTT sync success");
@ -333,7 +331,7 @@ static void sendDataToAg() {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting);
delay(1500);
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount())) {
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount)) {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected);
} else {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed);
@ -417,14 +415,6 @@ static void failedHandler(String msg) {
}
static void configurationUpdateSchedule(void) {
if (configuration.isOfflineMode() ||
configuration.getConfigurationControl() == ConfigurationControl::ConfigurationControlLocal) {
Serial.println("Ignore fetch server configuration. Either mode is offline "
"or configurationControl set to local");
apiClient.resetFetchConfigurationStatus();
return;
}
if (apiClient.fetchServerConfiguration()) {
configUpdateHandle();
}
@ -482,7 +472,7 @@ static void appDispHandler(void) {
if (configuration.isOfflineMode() == false) {
if (wifiConnector.isConnected() == false) {
state = AgStateMachineWiFiLost;
} else if (apiClient.isFetchConfigurationFailed()) {
} else if (apiClient.isFetchConfigureFailed()) {
state = AgStateMachineSensorConfigFailed;
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
@ -528,24 +518,19 @@ static void updatePm(void) {
static void sendDataToServer(void) {
/** Increment bootcount when send measurements data is scheduled */
int bootCount = measurements.bootCount() + 1;
measurements.setBootCount(bootCount);
measurements.bootCount++;
if (configuration.isOfflineMode() || !configuration.isPostDataToAirGradient()) {
Serial.println("Skipping transmission of data to AG server. Either mode is offline "
"or post data to server disabled");
/** Ignore send data to server if postToAirGradient disabled */
if (configuration.isPostDataToAirGradient() == false ||
configuration.isOfflineMode()) {
return;
}
if (wifiConnector.isConnected() == false) {
Serial.println("WiFi not connected, skipping data transmission to AG server");
return;
}
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI());
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI(), ag, configuration);
if (apiClient.postToServer(syncData)) {
Serial.println();
Serial.println("Online mode and isPostToAirGradient = true");
Serial.println(
"Online mode and isPostToAirGradient = true: watchdog reset");
Serial.println();
}
}

View File

@ -53,7 +53,7 @@ void LocalServer::_GET_metrics(void) {
}
void LocalServer::_GET_measure(void) {
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI());
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI(), *ag, config);
server.send(200, "application/json", toSend);
}

View File

@ -43,7 +43,7 @@ String OpenMetrics::getPayload(void) {
"1 if the AirGradient device was able to successfully fetch its "
"configuration from the server",
"gauge");
add_metric_point("", apiClient.isFetchConfigurationFailed() ? "0" : "1");
add_metric_point("", apiClient.isFetchConfigureFailed() ? "0" : "1");
add_metric(
"post_ok",
@ -66,7 +66,7 @@ String OpenMetrics::getPayload(void) {
int pm03PCount = utils::getInvalidPmValue();
int co2 = utils::getInvalidCO2();
int atmpCompensated = utils::getInvalidTemperature();
int rhumCompensated = utils::getInvalidHumidity();
int ahumCompensated = utils::getInvalidHumidity();
int tvoc = utils::getInvalidVOC();
int tvocRaw = utils::getInvalidVOC();
int nox = utils::getInvalidNOx();
@ -76,12 +76,12 @@ String OpenMetrics::getPayload(void) {
_temp = measure.getFloat(Measurements::Temperature);
_hum = measure.getFloat(Measurements::Humidity);
atmpCompensated = _temp;
rhumCompensated = _hum;
ahumCompensated = _hum;
}
if (config.hasSensorPMS1) {
pm01 = measure.get(Measurements::PM01);
float correctedPm = measure.getCorrectedPM25(false, 1);
float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10);
pm03PCount = measure.get(Measurements::PM03_PC);
@ -191,12 +191,12 @@ String OpenMetrics::getPayload(void) {
"gauge", "percent");
add_metric_point("", String(_hum));
}
if (utils::isValidHumidity(rhumCompensated)) {
if (utils::isValidHumidity(ahumCompensated)) {
add_metric("humidity_compensated",
"The compensated relative humidity as measured by the "
"AirGradient SHT / PMS sensor",
"gauge", "percent");
add_metric_point("", String(rhumCompensated));
add_metric_point("", String(ahumCompensated));
}
response += "# EOF\n";

View File

@ -12,8 +12,10 @@ Outdoor Monitor: https://www.airgradient.com/outdoor/
Build Instructions:
https://www.airgradient.com/documentation/diy-v4/
Compile Instructions:
https://github.com/airgradienthq/arduino/blob/master/docs/howto-compile.md
Please make sure you have esp8266 board manager installed. Tested with
version 3.1.2.
Set board to "LOLIN(WEMOS) D1 R2 & mini"
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
can be set through the AirGradient dashboard.
@ -53,7 +55,7 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
static AirGradient ag(DIY_PRO_INDOOR_V3_3);
static Configuration configuration(Serial);
static AgApiClient apiClient(Serial, configuration);
static Measurements measurements(configuration);
static Measurements measurements;
static OledDisplay oledDisplay(configuration, measurements, Serial);
static StateMachine stateMachine(oledDisplay, Serial, measurements,
configuration);
@ -122,7 +124,6 @@ void setup() {
apiClient.setAirGradient(&ag);
openMetrics.setAirGradient(&ag);
localServer.setAirGraident(&ag);
measurements.setAirGradient(&ag);
/** Example set custom API root URL */
// apiClient.setApiRoot("https://example.custom.api");
@ -148,12 +149,9 @@ void setup() {
initMqtt();
sendDataToAg();
if (configuration.getConfigurationControl() !=
ConfigurationControl::ConfigurationControlLocal) {
apiClient.fetchServerConfiguration();
}
apiClient.fetchServerConfiguration();
configSchedule.update();
if (apiClient.isFetchConfigurationFailed()) {
if (apiClient.isFetchConfigureFailed()) {
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
stateMachine.displayHandle(
@ -375,7 +373,7 @@ static void mqttHandle(void) {
}
if (mqttClient.isConnected()) {
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI());
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI(), ag, configuration);
String topic = "airgradient/readings/" + ag.deviceId();
if (mqttClient.publish(topic.c_str(), payload.c_str(), payload.length())) {
Serial.println("MQTT sync success");
@ -390,7 +388,7 @@ static void sendDataToAg() {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting);
delay(1500);
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount())) {
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount)) {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected);
} else {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed);
@ -469,14 +467,6 @@ static void failedHandler(String msg) {
}
static void configurationUpdateSchedule(void) {
if (configuration.isOfflineMode() ||
configuration.getConfigurationControl() == ConfigurationControl::ConfigurationControlLocal) {
Serial.println("Ignore fetch server configuration. Either mode is offline "
"or configurationControl set to local");
apiClient.resetFetchConfigurationStatus();
return;
}
if (apiClient.fetchServerConfiguration()) {
configUpdateHandle();
}
@ -534,7 +524,7 @@ static void appDispHandler(void) {
if (configuration.isOfflineMode() == false) {
if (wifiConnector.isConnected() == false) {
state = AgStateMachineWiFiLost;
} else if (apiClient.isFetchConfigurationFailed()) {
} else if (apiClient.isFetchConfigureFailed()) {
state = AgStateMachineSensorConfigFailed;
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
@ -580,24 +570,19 @@ static void updatePm(void) {
static void sendDataToServer(void) {
/** Increment bootcount when send measurements data is scheduled */
int bootCount = measurements.bootCount() + 1;
measurements.setBootCount(bootCount);
measurements.bootCount++;
if (configuration.isOfflineMode() || !configuration.isPostDataToAirGradient()) {
Serial.println("Skipping transmission of data to AG server. Either mode is offline "
"or post data to server disabled");
/** Ignore send data to server if postToAirGradient disabled */
if (configuration.isPostDataToAirGradient() == false ||
configuration.isOfflineMode()) {
return;
}
if (wifiConnector.isConnected() == false) {
Serial.println("WiFi not connected, skipping data transmission to AG server");
return;
}
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI());
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI(), ag, configuration);
if (apiClient.postToServer(syncData)) {
Serial.println();
Serial.println("Online mode and isPostToAirGradient = true");
Serial.println(
"Online mode and isPostToAirGradient = true: watchdog reset");
Serial.println();
}
}

View File

@ -53,7 +53,7 @@ void LocalServer::_GET_metrics(void) {
}
void LocalServer::_GET_measure(void) {
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI());
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI(), *ag, config);
server.send(200, "application/json", toSend);
}

View File

@ -43,7 +43,7 @@ String OpenMetrics::getPayload(void) {
"1 if the AirGradient device was able to successfully fetch its "
"configuration from the server",
"gauge");
add_metric_point("", apiClient.isFetchConfigurationFailed() ? "0" : "1");
add_metric_point("", apiClient.isFetchConfigureFailed() ? "0" : "1");
add_metric(
"post_ok",
@ -66,7 +66,7 @@ String OpenMetrics::getPayload(void) {
int pm03PCount = utils::getInvalidPmValue();
int co2 = utils::getInvalidCO2();
int atmpCompensated = utils::getInvalidTemperature();
int rhumCompensated = utils::getInvalidHumidity();
int ahumCompensated = utils::getInvalidHumidity();
int tvoc = utils::getInvalidVOC();
int tvocRaw = utils::getInvalidVOC();
int nox = utils::getInvalidNOx();
@ -76,12 +76,12 @@ String OpenMetrics::getPayload(void) {
_temp = measure.getFloat(Measurements::Temperature);
_hum = measure.getFloat(Measurements::Humidity);
atmpCompensated = _temp;
rhumCompensated = _hum;
ahumCompensated = _hum;
}
if (config.hasSensorPMS1) {
pm01 = measure.get(Measurements::PM01);
float correctedPm = measure.getCorrectedPM25(false, 1);
float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10);
pm03PCount = measure.get(Measurements::PM03_PC);
@ -192,12 +192,12 @@ String OpenMetrics::getPayload(void) {
"gauge", "percent");
add_metric_point("", String(_hum));
}
if (utils::isValidHumidity(rhumCompensated)) {
if (utils::isValidHumidity(ahumCompensated)) {
add_metric("humidity_compensated",
"The compensated relative humidity as measured by the "
"AirGradient SHT / PMS sensor",
"gauge", "percent");
add_metric_point("", String(rhumCompensated));
add_metric_point("", String(ahumCompensated));
}
response += "# EOF\n";

View File

@ -12,8 +12,10 @@ Outdoor Monitor: https://www.airgradient.com/outdoor/
Build Instructions:
https://www.airgradient.com/documentation/diy-v4/
Compile Instructions:
https://github.com/airgradienthq/arduino/blob/master/docs/howto-compile.md
Please make sure you have esp8266 board manager installed. Tested with
version 3.1.2.
Set board to "LOLIN(WEMOS) D1 R2 & mini"
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
can be set through the AirGradient dashboard.
@ -53,7 +55,7 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
static AirGradient ag(DIY_PRO_INDOOR_V4_2);
static Configuration configuration(Serial);
static AgApiClient apiClient(Serial, configuration);
static Measurements measurements(configuration);
static Measurements measurements;
static OledDisplay oledDisplay(configuration, measurements, Serial);
static StateMachine stateMachine(oledDisplay, Serial, measurements,
configuration);
@ -123,7 +125,6 @@ void setup() {
apiClient.setAirGradient(&ag);
openMetrics.setAirGradient(&ag);
localServer.setAirGraident(&ag);
measurements.setAirGradient(&ag);
/** Example set custom API root URL */
// apiClient.setApiRoot("https://example.custom.api");
@ -175,12 +176,9 @@ void setup() {
initMqtt();
sendDataToAg();
if (configuration.getConfigurationControl() !=
ConfigurationControl::ConfigurationControlLocal) {
apiClient.fetchServerConfiguration();
}
apiClient.fetchServerConfiguration();
configSchedule.update();
if (apiClient.isFetchConfigurationFailed()) {
if (apiClient.isFetchConfigureFailed()) {
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
stateMachine.displayHandle(
@ -398,7 +396,7 @@ static void mqttHandle(void) {
}
if (mqttClient.isConnected()) {
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI());
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI(), ag, configuration);
String topic = "airgradient/readings/" + ag.deviceId();
if (mqttClient.publish(topic.c_str(), payload.c_str(), payload.length())) {
Serial.println("MQTT sync success");
@ -413,7 +411,7 @@ static void sendDataToAg() {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting);
delay(1500);
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount())) {
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount)) {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected);
} else {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed);
@ -509,14 +507,6 @@ static void failedHandler(String msg) {
}
static void configurationUpdateSchedule(void) {
if (configuration.isOfflineMode() ||
configuration.getConfigurationControl() == ConfigurationControl::ConfigurationControlLocal) {
Serial.println("Ignore fetch server configuration. Either mode is offline "
"or configurationControl set to local");
apiClient.resetFetchConfigurationStatus();
return;
}
if (apiClient.fetchServerConfiguration()) {
configUpdateHandle();
}
@ -574,7 +564,7 @@ static void appDispHandler(void) {
if (configuration.isOfflineMode() == false) {
if (wifiConnector.isConnected() == false) {
state = AgStateMachineWiFiLost;
} else if (apiClient.isFetchConfigurationFailed()) {
} else if (apiClient.isFetchConfigureFailed()) {
state = AgStateMachineSensorConfigFailed;
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
@ -621,24 +611,19 @@ static void updatePm(void) {
static void sendDataToServer(void) {
/** Increment bootcount when send measurements data is scheduled */
int bootCount = measurements.bootCount() + 1;
measurements.setBootCount(bootCount);
measurements.bootCount++;
if (configuration.isOfflineMode() || !configuration.isPostDataToAirGradient()) {
Serial.println("Skipping transmission of data to AG server. Either mode is offline "
"or post data to server disabled");
/** Ignore send data to server if postToAirGradient disabled */
if (configuration.isPostDataToAirGradient() == false ||
configuration.isOfflineMode()) {
return;
}
if (wifiConnector.isConnected() == false) {
Serial.println("WiFi not connected, skipping data transmission to AG server");
return;
}
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI());
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI(), ag, configuration);
if (apiClient.postToServer(syncData)) {
Serial.println();
Serial.println("Online mode and isPostToAirGradient = true");
Serial.println(
"Online mode and isPostToAirGradient = true: watchdog reset");
Serial.println();
}
}

View File

@ -53,7 +53,7 @@ void LocalServer::_GET_metrics(void) {
}
void LocalServer::_GET_measure(void) {
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI());
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI(), *ag, config);
server.send(200, "application/json", toSend);
}

View File

@ -43,7 +43,7 @@ String OpenMetrics::getPayload(void) {
"1 if the AirGradient device was able to successfully fetch its "
"configuration from the server",
"gauge");
add_metric_point("", apiClient.isFetchConfigurationFailed() ? "0" : "1");
add_metric_point("", apiClient.isFetchConfigureFailed() ? "0" : "1");
add_metric(
"post_ok",
@ -66,7 +66,7 @@ String OpenMetrics::getPayload(void) {
int pm03PCount = utils::getInvalidPmValue();
int co2 = utils::getInvalidCO2();
int atmpCompensated = utils::getInvalidTemperature();
int rhumCompensated = utils::getInvalidHumidity();
int ahumCompensated = utils::getInvalidHumidity();
int tvoc = utils::getInvalidVOC();
int tvocRaw = utils::getInvalidVOC();
int nox = utils::getInvalidNOx();
@ -76,12 +76,12 @@ String OpenMetrics::getPayload(void) {
_temp = measure.getFloat(Measurements::Temperature);
_hum = measure.getFloat(Measurements::Humidity);
atmpCompensated = _temp;
rhumCompensated = _hum;
ahumCompensated = _hum;
}
if (config.hasSensorPMS1) {
pm01 = measure.get(Measurements::PM01);
float correctedPm = measure.getCorrectedPM25(false, 1);
float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10);
pm03PCount = measure.get(Measurements::PM03_PC);
@ -191,12 +191,12 @@ String OpenMetrics::getPayload(void) {
"gauge", "percent");
add_metric_point("", String(_hum));
}
if (utils::isValidHumidity(rhumCompensated)) {
if (utils::isValidHumidity(ahumCompensated)) {
add_metric("humidity_compensated",
"The compensated relative humidity as measured by the "
"AirGradient SHT / PMS sensor",
"gauge", "percent");
add_metric_point("", String(rhumCompensated));
add_metric_point("", String(ahumCompensated));
}
response += "# EOF\n";

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());
@ -64,8 +77,178 @@ void LocalServer::_GET_metrics(void) {
}
void LocalServer::_GET_measure(void) {
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI());
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI(), *ag, config);
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_ */

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,13 @@
#include "OpenMetrics.h"
OpenMetrics::OpenMetrics(Measurements &measure, Configuration &config,
WifiConnector &wifiConnector)
: measure(measure), config(config), wifiConnector(wifiConnector) {}
WifiConnector &wifiConnector, AgApiClient &apiClient)
: measure(measure), config(config), wifiConnector(wifiConnector),
apiClient(apiClient) {}
OpenMetrics::~OpenMetrics() {}
void OpenMetrics::setAirGradient(AirGradient *ag) {
this->ag = ag;
}
void OpenMetrics::setAirgradientClient(AirgradientClient *client) {
this->agClient = client;
}
void OpenMetrics::setAirGradient(AirGradient *ag) { this->ag = ag; }
const char *OpenMetrics::getApiContentType(void) {
return "application/openmetrics-text; version=1.0.0; charset=utf-8";
@ -48,13 +43,13 @@ String OpenMetrics::getPayload(void) {
"1 if the AirGradient device was able to successfully fetch its "
"configuration from the server",
"gauge");
add_metric_point("", agClient->isLastFetchConfigSucceed() ? "1" : "0");
add_metric_point("", apiClient.isFetchConfigureFailed() ? "0" : "1");
add_metric(
"post_ok",
"1 if the AirGradient device was able to successfully send to the server",
"gauge");
add_metric_point("", agClient->isLastPostMeasureSucceed() ? "1" : "0");
add_metric_point("", apiClient.isPostToServerFailed() ? "0" : "1");
add_metric(
"wifi_rssi",
@ -71,7 +66,7 @@ String OpenMetrics::getPayload(void) {
int pm03PCount = utils::getInvalidPmValue();
int co2 = utils::getInvalidCO2();
int atmpCompensated = utils::getInvalidTemperature();
int rhumCompensated = utils::getInvalidHumidity();
int ahumCompensated = utils::getInvalidHumidity();
int tvoc = utils::getInvalidVOC();
int tvocRaw = utils::getInvalidVOC();
int nox = utils::getInvalidNOx();
@ -86,8 +81,8 @@ 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(false, 1);
float correctedPm25_2 = measure.getCorrectedPM25(false, 2);
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);
pm10 = (measure.get(Measurements::PM10, 1) + measure.get(Measurements::PM10, 2)) / 2.0f;
@ -102,7 +97,7 @@ String OpenMetrics::getPayload(void) {
if (config.hasSensorPMS1) {
pm01 = measure.get(Measurements::PM01);
float correctedPm = measure.getCorrectedPM25(false, 1);
float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10);
pm03PCount = measure.get(Measurements::PM03_PC);
@ -112,7 +107,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(false, 1);
float correctedPm = measure.getCorrectedPM25(*ag, config, false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10, 1);
pm03PCount = measure.get(Measurements::PM03_PC, 1);
@ -121,7 +116,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(false, 2);
float correctedPm = measure.getCorrectedPM25(*ag, config, false, 2);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10, 2);
pm03PCount = measure.get(Measurements::PM03_PC, 2);
@ -142,15 +137,11 @@ String OpenMetrics::getPayload(void) {
/** Get temperature and humidity compensated */
if (ag->isOne()) {
atmpCompensated = round(measure.getCorrectedTempHum(Measurements::Temperature));
rhumCompensated = round(measure.getCorrectedTempHum(Measurements::Humidity));
atmpCompensated = _temp;
ahumCompensated = _hum;
} else {
atmpCompensated = round((measure.getCorrectedTempHum(Measurements::Temperature, 1) +
measure.getCorrectedTempHum(Measurements::Temperature, 2)) /
2.0f);
rhumCompensated = round((measure.getCorrectedTempHum(Measurements::Humidity, 1) +
measure.getCorrectedTempHum(Measurements::Humidity, 2)) /
2.0f);
atmpCompensated = ag->pms5003t_1.compensateTemp(_temp);
ahumCompensated = ag->pms5003t_1.compensateHum(_hum);
}
// Add measurements that valid to the metrics
@ -243,11 +234,11 @@ String OpenMetrics::getPayload(void) {
"gauge", "percent");
add_metric_point("", String(_hum));
}
if (utils::isValidHumidity(rhumCompensated)) {
if (utils::isValidHumidity(ahumCompensated)) {
add_metric("humidity_compensated",
"The compensated relative humidity as measured by the AirGradient SHT / PMS sensor",
"gauge", "percent");
add_metric_point("", String(rhumCompensated));
add_metric_point("", String(ahumCompensated));
}
response += "# EOF\n";

View File

@ -5,22 +5,21 @@
#include "AgValue.h"
#include "AgWiFiConnector.h"
#include "AirGradient.h"
#include "Libraries/airgradient-client/src/airgradientClient.h"
#include "AgApiClient.h"
class OpenMetrics {
private:
AirGradient *ag;
AirgradientClient *agClient;
Measurements &measure;
Configuration &config;
WifiConnector &wifiConnector;
AgApiClient &apiClient;
public:
OpenMetrics(Measurements &measure, Configuration &config,
WifiConnector &wifiConnector);
OpenMetrics(Measurements &measure, Configuration &conig,
WifiConnector &wifiConnector, AgApiClient& apiClient);
~OpenMetrics();
void setAirGradient(AirGradient *ag);
void setAirgradientClient(AirgradientClient *client);
const char *getApiContentType(void);
const char* getApi(void);
String getPayload(void);

View File

@ -0,0 +1,206 @@
#ifndef _OTA_HANDLER_H_
#define _OTA_HANDLER_H_
#include <Arduino.h>
#include <esp_err.h>
#include <esp_http_client.h>
#include <esp_ota_ops.h>
#define OTA_BUF_SIZE 1024
#define URL_BUF_SIZE 256
enum OtaUpdateOutcome {
UPDATE_PERFORMED,
ALREADY_UP_TO_DATE,
UPDATE_FAILED,
UDPATE_SKIPPED
};
enum OtaState {
OTA_STATE_BEGIN,
OTA_STATE_FAIL,
OTA_STATE_SKIP,
OTA_STATE_UP_TO_DATE,
OTA_STATE_PROCESSING,
OTA_STATE_SUCCESS
};
typedef void(*OtaHandlerCallback_t)(OtaState state,
String message);
class OtaHandler {
public:
void updateFirmwareIfOutdated(String deviceId) {
String url = "http://hw.airgradient.com/sensors/airgradient:" + deviceId +
"/generic/os/firmware.bin";
url += "?current_firmware=";
url += GIT_VERSION;
char urlAsChar[URL_BUF_SIZE];
url.toCharArray(urlAsChar, URL_BUF_SIZE);
Serial.printf("checking for new OTA update @ %s\n", urlAsChar);
esp_http_client_config_t config = {};
config.url = urlAsChar;
OtaUpdateOutcome ret = attemptToPerformOta(&config);
Serial.println(ret);
if (this->callback) {
switch (ret) {
case OtaUpdateOutcome::UPDATE_PERFORMED:
this->callback(OtaState::OTA_STATE_SUCCESS, "");
break;
case OtaUpdateOutcome::UDPATE_SKIPPED:
this->callback(OtaState::OTA_STATE_SKIP, "");
break;
case OtaUpdateOutcome::ALREADY_UP_TO_DATE:
this->callback(OtaState::OTA_STATE_UP_TO_DATE, "");
break;
case OtaUpdateOutcome::UPDATE_FAILED:
this->callback(OtaState::OTA_STATE_FAIL, "");
break;
default:
break;
}
}
}
void setHandlerCallback(OtaHandlerCallback_t callback) {
this->callback = callback;
}
private:
OtaHandlerCallback_t callback;
OtaUpdateOutcome attemptToPerformOta(const esp_http_client_config_t *config) {
esp_http_client_handle_t client = esp_http_client_init(config);
if (client == NULL) {
Serial.println("Failed to initialize HTTP connection");
return OtaUpdateOutcome::UPDATE_FAILED;
}
esp_err_t err = esp_http_client_open(client, 0);
if (err != ESP_OK) {
esp_http_client_cleanup(client);
Serial.printf("Failed to open HTTP connection: %s\n",
esp_err_to_name(err));
return OtaUpdateOutcome::UPDATE_FAILED;
}
esp_http_client_fetch_headers(client);
int httpStatusCode = esp_http_client_get_status_code(client);
if (httpStatusCode == 304) {
Serial.println("Firmware is already up to date");
cleanupHttp(client);
return OtaUpdateOutcome::ALREADY_UP_TO_DATE;
} else if (httpStatusCode != 200) {
Serial.printf("Firmware update skipped, the server returned %d\n",
httpStatusCode);
cleanupHttp(client);
return OtaUpdateOutcome::UDPATE_SKIPPED;
}
esp_ota_handle_t update_handle = 0;
const esp_partition_t *update_partition = NULL;
Serial.println("Starting OTA update ...");
update_partition = esp_ota_get_next_update_partition(NULL);
if (update_partition == NULL) {
Serial.println("Passive OTA partition not found");
cleanupHttp(client);
return OtaUpdateOutcome::UPDATE_FAILED;
}
Serial.printf("Writing to partition subtype %d at offset 0x%x\n",
update_partition->subtype, update_partition->address);
err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &update_handle);
if (err != ESP_OK) {
Serial.printf("esp_ota_begin failed, error=%d\n", err);
cleanupHttp(client);
return OtaUpdateOutcome::UPDATE_FAILED;
}
esp_err_t ota_write_err = ESP_OK;
char *upgrade_data_buf = (char *)malloc(OTA_BUF_SIZE);
if (!upgrade_data_buf) {
Serial.println("Couldn't allocate memory for data buffer");
return OtaUpdateOutcome::UPDATE_FAILED;
}
int binary_file_len = 0;
int totalSize = esp_http_client_get_content_length(client);
Serial.println("File size: " + String(totalSize) + String(" bytes"));
// Show display start update new firmware.
if (this->callback) {
this->callback(OtaState::OTA_STATE_BEGIN, "");
}
// Download file and write new firmware to OTA partition
uint32_t lastUpdate = millis();
while (1) {
int data_read =
esp_http_client_read(client, upgrade_data_buf, OTA_BUF_SIZE);
if (data_read == 0) {
if (this->callback) {
this->callback(OtaState::OTA_STATE_PROCESSING, String(100));
}
Serial.println("Connection closed, all data received");
break;
}
if (data_read < 0) {
Serial.println("Data read error");
if (this->callback) {
this->callback(OtaState::OTA_STATE_FAIL, "");
}
break;
}
if (data_read > 0) {
ota_write_err = esp_ota_write(
update_handle, (const void *)upgrade_data_buf, data_read);
if (ota_write_err != ESP_OK) {
if (this->callback) {
this->callback(OtaState::OTA_STATE_FAIL, "");
}
break;
}
binary_file_len += data_read;
int percent = (binary_file_len * 100) / totalSize;
uint32_t ms = (uint32_t)(millis() - lastUpdate);
if (ms >= 250) {
// sm.executeOTA(StateMachine::OtaState::OTA_STATE_PROCESSING, "",
// percent);
if (this->callback) {
this->callback(OtaState::OTA_STATE_PROCESSING,
String(percent));
}
lastUpdate = millis();
}
}
}
free(upgrade_data_buf);
cleanupHttp(client);
Serial.printf("# of bytes written: %d\n", binary_file_len);
esp_err_t ota_end_err = esp_ota_end(update_handle);
if (ota_write_err != ESP_OK) {
Serial.printf("Error: esp_ota_write failed! err=0x%d\n", err);
return OtaUpdateOutcome::UPDATE_FAILED;
} else if (ota_end_err != ESP_OK) {
Serial.printf("Error: esp_ota_end failed! err=0x%d. Image is invalid",
ota_end_err);
return OtaUpdateOutcome::UPDATE_FAILED;
}
err = esp_ota_set_boot_partition(update_partition);
if (err != ESP_OK) {
Serial.printf("esp_ota_set_boot_partition failed! err=0x%d\n", err);
return OtaUpdateOutcome::UPDATE_FAILED;
}
return OtaUpdateOutcome::UPDATE_PERFORMED;
}
void cleanupHttp(esp_http_client_handle_t client) {
esp_http_client_close(client);
esp_http_client_cleanup(client);
}
};
#endif

View File

@ -1,5 +1,5 @@
name=AirGradient Air Quality Sensor
version=3.3.9
version=3.1.13
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

@ -12,7 +12,7 @@
platform = espressif32
board = esp32-c3-devkitm-1
framework = arduino
build_flags = !echo '-D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 -D AG_LOG_LEVEL=AG_LOG_LEVEL_INFO -D GIT_VERSION=\\"'$(git describe --tags --always --dirty)'\\"'
build_flags = !echo '-D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 -D GIT_VERSION=\\"'$(git describe --tags --always --dirty)'\\"'
board_build.partitions = partitions.csv
monitor_speed = 115200
lib_deps =

View File

@ -34,6 +34,17 @@ void AgApiClient::begin(void) {
* @return false Failure
*/
bool AgApiClient::fetchServerConfiguration(void) {
if (config.getConfigurationControl() ==
ConfigurationControl::ConfigurationControlLocal ||
config.isOfflineMode()) {
logWarning("Ignore fetch server configuration");
// Clear server configuration failed flag, cause it's ignore but not
// really failed
getConfigFailed = false;
return false;
}
String uri = apiRoot + "/sensors/airgradient:" +
ag->deviceId() + "/one/config";
@ -47,22 +58,10 @@ bool AgApiClient::fetchServerConfiguration(void) {
}
#else
HTTPClient client;
client.setConnectTimeout(timeoutMs); // Set timeout when establishing connection to server
client.setTimeout(timeoutMs); // Timeout when waiting for response from AG server
if (apiRootChanged) {
// If apiRoot is changed, assume not using https
if (client.begin(uri) == false) {
logError("Begin HTTPClient failed (GET)");
getConfigFailed = true;
return false;
}
} else {
// By default, airgradient using https
if (client.begin(uri, AG_SERVER_ROOT_CA) == false) {
logError("Begin HTTPClient using tls failed (GET)");
getConfigFailed = true;
return false;
}
client.setTimeout(timeoutMs);
if (client.begin(uri) == false) {
getConfigFailed = true;
return false;
}
#endif
@ -91,6 +90,8 @@ bool AgApiClient::fetchServerConfiguration(void) {
String respContent = client.getString();
client.end();
// logInfo("Get configuration: " + respContent);
/** Parse configuration and return result */
return config.parse(respContent, false);
}
@ -104,39 +105,32 @@ bool AgApiClient::fetchServerConfiguration(void) {
* @return false Failure
*/
bool AgApiClient::postToServer(String data) {
String uri = apiRoot + "/sensors/airgradient:" + ag->deviceId() + "/measures";
#ifdef ESP8266
HTTPClient client;
WiFiClient wifiClient;
if (client.begin(wifiClient, uri) == false) {
getConfigFailed = true;
if (config.isPostDataToAirGradient() == false) {
logWarning("Ignore post data to server");
return true;
}
if (WiFi.isConnected() == false) {
return false;
}
#else
String uri = apiRoot + "/sensors/airgradient:" + ag->deviceId() + "/measures";
// logInfo("Post uri: " + uri);
// logInfo("Post data: " + data);
WiFiClient wifiClient;
HTTPClient client;
client.setConnectTimeout(timeoutMs); // Set timeout when establishing connection to server
client.setTimeout(timeoutMs); // Timeout when waiting for response from AG server
if (apiRootChanged) {
// If apiRoot is changed, assume not using https
if (client.begin(uri) == false) {
logError("Begin HTTPClient failed (POST)");
getConfigFailed = true;
return false;
}
} else {
// By default, airgradient using https
if (client.begin(uri, AG_SERVER_ROOT_CA) == false) {
logError("Begin HTTPClient using tls failed (POST)");
getConfigFailed = true;
return false;
}
client.setTimeout(timeoutMs);
if (client.begin(wifiClient, uri.c_str()) == false) {
logError("Init client failed");
return false;
}
#endif
client.addHeader("content-type", "application/json");
int retCode = client.POST(data);
client.end();
logInfo(String("POST: ") + uri);
// logInfo(String("DATA: ") + data);
logInfo(String("Return code: ") + String(retCode));
if ((retCode == 200) || (retCode == 429)) {
@ -155,12 +149,7 @@ bool AgApiClient::postToServer(String data) {
* @return true Success
* @return false Failure
*/
bool AgApiClient::isFetchConfigurationFailed(void) { return getConfigFailed; }
/**
* @brief Reset status of get configuration from AirGradient cloud
*/
void AgApiClient::resetFetchConfigurationStatus(void) { getConfigFailed = false; }
bool AgApiClient::isFetchConfigureFailed(void) { return getConfigFailed; }
/**
* @brief Get failed status when post data to AirGradient cloud
@ -200,10 +189,7 @@ bool AgApiClient::sendPing(int rssi, int bootCount) {
String AgApiClient::getApiRoot() const { return apiRoot; }
void AgApiClient::setApiRoot(const String &apiRoot) {
this->apiRootChanged = true;
this->apiRoot = apiRoot;
}
void AgApiClient::setApiRoot(const String &apiRoot) { this->apiRoot = apiRoot; }
/**
* @brief Set http request timeout. (Default: 10s)

View File

@ -20,18 +20,12 @@ class AgApiClient : public PrintLog {
private:
Configuration &config;
AirGradient *ag;
#ifdef ESP8266
// ESP8266 not support HTTPS
String apiRoot = "http://hw.airgradient.com";
#else
String apiRoot = "https://hw.airgradient.com";
#endif
bool apiRootChanged = false; // Indicate if setApiRoot() is called
bool getConfigFailed;
bool postToServerFailed;
bool notAvailableOnDashboard = false; // Device not setup on Airgradient cloud dashboard.
uint16_t timeoutMs = 15000; // Default set to 15s
uint16_t timeoutMs = 10000; // Default set to 10s
public:
AgApiClient(Stream &stream, Configuration &config);
@ -40,8 +34,7 @@ public:
void begin(void);
bool fetchServerConfiguration(void);
bool postToServer(String data);
bool isFetchConfigurationFailed(void);
void resetFetchConfigurationStatus(void);
bool isFetchConfigureFailed(void);
bool isPostToServerFailed(void);
bool isNotAvailableOnDashboard(void);
void setAirGradient(AirGradient *ag);

View File

@ -22,17 +22,15 @@ const char *LED_BAR_MODE_NAMES[] = {
};
const char *PM_CORRECTION_ALGORITHM_NAMES[] = {
[COR_ALGO_PM_UNKNOWN] = "-", // This is only to pass "non-trivial designated initializers" error
[COR_ALGO_PM_NONE] = "none",
[COR_ALGO_PM_EPA_2021] = "epa_2021",
[COR_ALGO_PM_SLR_CUSTOM] = "custom",
};
const char *TEMP_HUM_CORRECTION_ALGORITHM_NAMES[] = {
[COR_ALGO_TEMP_HUM_UNKNOWN] = "-", // This is only to pass "non-trivial designated initializers" error
[COR_ALGO_TEMP_HUM_NONE] = "none",
[COR_ALGO_TEMP_HUM_AG_PMS5003T_2024] = "ag_pms5003t_2024",
[COR_ALGO_TEMP_HUM_SLR_CUSTOM] = "custom",
[Unknown] = "-", // This is only to pass "non-trivial designated initializers" error
[None] = "none",
[EPA_2021] = "epa_2021",
[SLR_PMS5003_20220802] = "slr_PMS5003_20220802",
[SLR_PMS5003_20220803] = "slr_PMS5003_20220803",
[SLR_PMS5003_20220824] = "slr_PMS5003_20220824",
[SLR_PMS5003_20231030] = "slr_PMS5003_20231030",
[SLR_PMS5003_20231218] = "slr_PMS5003_20231218",
[SLR_PMS5003_20240104] = "slr_PMS5003_20240104",
};
#define JSON_PROP_NAME(name) jprop_##name
@ -46,11 +44,9 @@ JSON_PROP_DEF(abcDays);
JSON_PROP_DEF(tvocLearningOffset);
JSON_PROP_DEF(noxLearningOffset);
JSON_PROP_DEF(mqttBrokerUrl);
JSON_PROP_DEF(httpDomain);
JSON_PROP_DEF(temperatureUnit);
JSON_PROP_DEF(configurationControl);
JSON_PROP_DEF(postDataToAirGradient);
JSON_PROP_DEF(disableCloudConnection);
JSON_PROP_DEF(ledBarBrightness);
JSON_PROP_DEF(displayBrightness);
JSON_PROP_DEF(co2CalibrationRequested);
@ -58,8 +54,6 @@ JSON_PROP_DEF(ledBarTestRequested);
JSON_PROP_DEF(offlineMode);
JSON_PROP_DEF(monitorDisplayCompensatedValues);
JSON_PROP_DEF(corrections);
JSON_PROP_DEF(atmp);
JSON_PROP_DEF(rhum);
#define jprop_model_default ""
#define jprop_country_default "TH"
@ -69,11 +63,9 @@ JSON_PROP_DEF(rhum);
#define jprop_tvocLearningOffset_default 12
#define jprop_noxLearningOffset_default 12
#define jprop_mqttBrokerUrl_default ""
#define jprop_httpDomain_default ""
#define jprop_temperatureUnit_default "c"
#define jprop_configurationControl_default String(CONFIGURATION_CONTROL_NAME[ConfigurationControl::ConfigurationControlBoth])
#define jprop_postDataToAirGradient_default true
#define jprop_disableCloudConnection_default false
#define jprop_ledBarBrightness_default 100
#define jprop_displayBrightness_default 100
#define jprop_offlineMode_default false
@ -112,8 +104,8 @@ PMCorrectionAlgorithm Configuration::matchPmAlgorithm(String algorithm) {
// If the input string matches an algorithm name, return the corresponding enum value
// Else return Unknown
const size_t enumSize = COR_ALGO_PM_SLR_CUSTOM + 1; // Get the actual size of the enum
PMCorrectionAlgorithm result = COR_ALGO_PM_UNKNOWN;;
const size_t enumSize = SLR_PMS5003_20240104 + 1; // Get the actual size of the enum
PMCorrectionAlgorithm result = PMCorrectionAlgorithm::Unknown;
// Loop through enum values
for (size_t enumVal = 0; enumVal < enumSize; enumVal++) {
@ -122,63 +114,42 @@ PMCorrectionAlgorithm Configuration::matchPmAlgorithm(String algorithm) {
}
}
// If string not match from enum, check if correctionAlgorithm is one of the PM batch corrections
if (result == COR_ALGO_PM_UNKNOWN) {
// Check the substring "slr_PMS5003_xxxxxxxx"
if (algorithm.substring(0, 11) == "slr_PMS5003") {
// If it is, then its a custom correction
result = COR_ALGO_PM_SLR_CUSTOM;
}
}
return result;
}
TempHumCorrectionAlgorithm Configuration::matchTempHumAlgorithm(String algorithm) {
// Get the actual size of the enum
const int enumSize = static_cast<int>(COR_ALGO_TEMP_HUM_SLR_CUSTOM);
TempHumCorrectionAlgorithm result = COR_ALGO_TEMP_HUM_UNKNOWN;
// Loop through enum values
for (size_t enumVal = 0; enumVal <= enumSize; enumVal++) {
if (algorithm == TEMP_HUM_CORRECTION_ALGORITHM_NAMES[enumVal]) {
result = static_cast<TempHumCorrectionAlgorithm>(enumVal);
}
}
return result;
}
bool Configuration::updatePmCorrection(JSONVar &json) {
if (!json.hasOwnProperty("corrections")) {
logInfo("corrections not found");
// TODO: need to response message?
Serial.println("corrections not found");
return false;
}
JSONVar corrections = json["corrections"];
if (!corrections.hasOwnProperty("pm02")) {
logWarning("pm02 not found");
Serial.println("pm02 not found");
return false;
}
JSONVar pm02 = corrections["pm02"];
if (!pm02.hasOwnProperty("correctionAlgorithm")) {
logWarning("pm02 correctionAlgorithm not found");
Serial.println("correctionAlgorithm not found");
return false;
}
// TODO: Need to have data type check, with error message response if invalid
// Check algorithm
String algorithm = pm02["correctionAlgorithm"];
PMCorrectionAlgorithm algo = matchPmAlgorithm(algorithm);
if (algo == COR_ALGO_PM_UNKNOWN) {
logWarning("Unknown algorithm");
if (algo == Unknown) {
logInfo("Unknown algorithm");
return false;
}
logInfo("Correction algorithm: " + algorithm);
// If algo is None or EPA_2021, no need to check slr
// But first check if pmCorrection different from algo
if (algo == COR_ALGO_PM_NONE || algo == COR_ALGO_PM_EPA_2021) {
if (algo == None || algo == EPA_2021) {
if (pmCorrection.algorithm != algo) {
// Deep copy corrections from root to jconfig, so it will be saved later
jconfig[jprop_corrections]["pm02"]["correctionAlgorithm"] = algorithm;
@ -195,7 +166,7 @@ bool Configuration::updatePmCorrection(JSONVar &json) {
// Check if pm02 has slr object
if (!pm02.hasOwnProperty("slr")) {
logWarning("slr not found");
Serial.println("slr not found");
return false;
}
@ -204,7 +175,7 @@ bool Configuration::updatePmCorrection(JSONVar &json) {
// Validate required slr properties exist
if (!slr.hasOwnProperty("intercept") || !slr.hasOwnProperty("scalingFactor") ||
!slr.hasOwnProperty("useEpa2021")) {
logWarning("Missing required slr properties");
Serial.println("Missing required slr properties");
return false;
}
@ -234,87 +205,6 @@ bool Configuration::updatePmCorrection(JSONVar &json) {
return true;
}
bool Configuration::updateTempHumCorrection(JSONVar &json, TempHumCorrection &target,
const char *correctionName) {
if (!json.hasOwnProperty(jprop_corrections)) {
return false;
}
JSONVar corrections = json[jprop_corrections];
if (!corrections.hasOwnProperty(correctionName)) {
logInfo(String(correctionName) + " correction field not found on configuration");
return false;
}
JSONVar correctionTarget = corrections[correctionName];
if (!correctionTarget.hasOwnProperty("correctionAlgorithm")) {
Serial.println("correctionAlgorithm not found");
return false;
}
String algorithm = correctionTarget["correctionAlgorithm"];
TempHumCorrectionAlgorithm algo = matchTempHumAlgorithm(algorithm);
if (algo == COR_ALGO_TEMP_HUM_UNKNOWN) {
logInfo("Uknown temp/hum algorithm");
return false;
}
logInfo(String(correctionName) + " correction algorithm: " + algorithm);
// If algo is None or Standard, then no need to check slr
// But first check if target correction different from algo
if (algo == COR_ALGO_TEMP_HUM_NONE || algo == COR_ALGO_TEMP_HUM_AG_PMS5003T_2024) {
if (target.algorithm != algo) {
// Deep copy corrections from root to jconfig, so it will be saved later
jconfig[jprop_corrections][correctionName]["correctionAlgorithm"] = algorithm;
jconfig[jprop_corrections][correctionName]["slr"] = JSON.parse("{}"); // Clear slr
// Update pmCorrection with new values
target.algorithm = algo;
target.changed = true;
logInfo(String(correctionName) + " correction updated");
return true;
}
return false;
}
// Check if correction.target (atmp or rhum) has slr object
if (!correctionTarget.hasOwnProperty("slr")) {
logWarning(String(correctionName) + " slr not found");
return false;
}
JSONVar slr = correctionTarget["slr"];
// Validate required slr properties exist
if (!slr.hasOwnProperty("intercept") || !slr.hasOwnProperty("scalingFactor")) {
Serial.println("Missing required slr properties");
return false;
}
// arduino_json doesn't support float type, need to cast to double first
float intercept = (float)((double)slr["intercept"]);
float scalingFactor = (float)((double)slr["scalingFactor"]);
// Compare with current target correciont
if (target.algorithm == algo && target.intercept == intercept &&
target.scalingFactor == scalingFactor) {
return false; // No changes needed
}
// Deep copy corrections from root to jconfig, so it will be saved later
jconfig[jprop_corrections] = corrections;
// Update target with new values
target.algorithm = algo;
target.intercept = intercept;
target.scalingFactor = scalingFactor;
target.changed = true;
// Correction values were updated
logInfo(String(correctionName) + " correction updated");
return true;
}
/**
* @brief Save configure to device storage (EEPROM)
*
@ -363,7 +253,7 @@ void Configuration::loadConfig(void) {
}
file.close();
} else {
SPIFFS.format();
// SPIFFS.format();
}
#endif
toConfig(buf);
@ -379,11 +269,9 @@ void Configuration::defaultConfig(void) {
jconfig[jprop_country] = jprop_country_default;
jconfig[jprop_mqttBrokerUrl] = jprop_mqttBrokerUrl_default;
jconfig[jprop_httpDomain] = jprop_httpDomain_default;
jconfig[jprop_configurationControl] = jprop_configurationControl_default;
jconfig[jprop_pmStandard] = jprop_pmStandard_default;
jconfig[jprop_temperatureUnit] = jprop_temperatureUnit_default;
jconfig[jprop_disableCloudConnection] = jprop_disableCloudConnection_default;
jconfig[jprop_postDataToAirGradient] = jprop_postDataToAirGradient_default;
if (ag->isOne()) {
jconfig[jprop_ledBarBrightness] = jprop_ledBarBrightness_default;
@ -401,8 +289,8 @@ void Configuration::defaultConfig(void) {
jconfig[jprop_offlineMode] = jprop_offlineMode_default;
jconfig[jprop_monitorDisplayCompensatedValues] = jprop_monitorDisplayCompensatedValues_default;
// PM2.5 default correction
pmCorrection.algorithm = COR_ALGO_PM_NONE;
// PM2.5 correction
pmCorrection.algorithm = None;
pmCorrection.changed = false;
pmCorrection.intercept = 0;
pmCorrection.scalingFactor = 1;
@ -456,10 +344,6 @@ bool Configuration::begin(void) {
return true;
}
void Configuration::setConfigurationUpdatedCallback(ConfigurationUpdatedCallback_t callback) {
_callback = callback;
}
/**
* @brief Parse JSON configura string to local configure
*
@ -742,17 +626,11 @@ bool Configuration::parse(String data, bool isLocal) {
jconfig[jprop_mqttBrokerUrl] = broker;
}
} else {
failedMessage = "\"mqttBrokerUrl\" length should less than 255 character";
failedMessage = "\"mqttBrokerUrl\" length should <= 255";
jsonInvalid();
return false;
}
}
else if (JSON.typeof_(root[jprop_mqttBrokerUrl]) == "null" and !isLocal) {
// So if its not available on the json and json comes from aigradient server
// then set its value to default (empty)
jconfig[jprop_mqttBrokerUrl] = jprop_mqttBrokerUrl_default;
}
else {
} else {
if (jsonTypeInvalid(root[jprop_mqttBrokerUrl], "string")) {
failedMessage =
jsonTypeInvalidMessage(String(jprop_mqttBrokerUrl), "string");
@ -761,32 +639,6 @@ bool Configuration::parse(String data, bool isLocal) {
}
}
if (isLocal) {
if (JSON.typeof_(root[jprop_httpDomain]) == "string") {
String httpDomain = root[jprop_httpDomain];
String oldHttpDomain = jconfig[jprop_httpDomain];
if (httpDomain.length() <= 255) {
if (httpDomain != oldHttpDomain) {
changed = true;
configLogInfo(String(jprop_httpDomain), oldHttpDomain, httpDomain);
jconfig[jprop_httpDomain] = httpDomain;
}
} else {
failedMessage = "\"httpDomain\" length should less than 255 character";
jsonInvalid();
return false;
}
}
else {
if (jsonTypeInvalid(root[jprop_httpDomain], "string")) {
failedMessage =
jsonTypeInvalidMessage(String(jprop_httpDomain), "string");
jsonInvalid();
return false;
}
}
}
if (JSON.typeof_(root[jprop_temperatureUnit]) == "string") {
String unit = root[jprop_temperatureUnit];
String oldUnit = jconfig[jprop_temperatureUnit];
@ -940,33 +792,20 @@ bool Configuration::parse(String data, bool isLocal) {
}
}
// PM2.5 Corrections
// Corrections
if (updatePmCorrection(root)) {
changed = true;
}
// Temperature correction
if (updateTempHumCorrection(root, tempCorrection, jprop_atmp)) {
changed = true;
}
// Relative humidity correction
if (updateTempHumCorrection(root, rhumCorrection, jprop_rhum)) {
changed = true;
}
if (ledBarTestRequested || co2CalibrationRequested) {
commandRequested = true;
updated = true;
}
if (changed) {
updated = true;
saveConfig();
printConfig();
_callback();
} else {
if (ledBarTestRequested || co2CalibrationRequested) {
updated = true;
}
}
return true;
}
@ -1072,16 +911,6 @@ String Configuration::getMqttBrokerUri(void) {
return broker;
}
/**
* @brief Get HTTP domain for post measures and get configuration
*
* @return String http domain, might be empty string
*/
String Configuration::getHttpDomain(void) {
String httpDomain = jconfig[jprop_httpDomain];
return httpDomain;
}
/**
* @brief Get configuratoin post data to AirGradient cloud
*
@ -1166,14 +995,8 @@ bool Configuration::isUpdated(void) {
return updated;
}
bool Configuration::isCommandRequested(void) {
bool oldState = this->commandRequested;
this->commandRequested = false;
return oldState;
}
String Configuration::jsonTypeInvalidMessage(String name, String type) {
return "'" + name + "' type is invalid, expecting '" + type + "'";
return "'" + name + "' type invalid, it's should '" + type + "'";
}
String Configuration::jsonValueInvalidMessage(String name, String value) {
@ -1213,20 +1036,20 @@ void Configuration::toConfig(const char *buf) {
}
bool changed = false;
bool isConfigFieldInvalid = false;
bool isInvalid = false;
/** Validate country */
if (JSON.typeof_(jconfig[jprop_country]) != "string") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
String country = jconfig[jprop_country];
if (country.length() != 2) {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_country] = jprop_country_default;
changed = true;
logInfo("toConfig: country changed");
@ -1234,17 +1057,17 @@ void Configuration::toConfig(const char *buf) {
/** validate: PM standard */
if (JSON.typeof_(jconfig[jprop_pmStandard]) != "string") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
String standard = jconfig[jprop_pmStandard];
if (standard != getPMStandardString(true) &&
standard != getPMStandardString(false)) {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_pmStandard] = jprop_pmStandard_default;
changed = true;
logInfo("toConfig: pmStandard changed");
@ -1252,18 +1075,18 @@ void Configuration::toConfig(const char *buf) {
/** validate led bar mode */
if (JSON.typeof_(jconfig[jprop_ledBarMode]) != "string") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
String mode = jconfig[jprop_ledBarMode];
if (mode != getLedBarModeName(LedBarMode::LedBarModeCO2) &&
mode != getLedBarModeName(LedBarMode::LedBarModeOff) &&
mode != getLedBarModeName(LedBarMode::LedBarModePm)) {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_ledBarMode] = jprop_ledBarMode_default;
changed = true;
logInfo("toConfig: ledBarMode changed");
@ -1271,11 +1094,11 @@ void Configuration::toConfig(const char *buf) {
/** validate abcday */
if (JSON.typeof_(jconfig[jprop_abcDays]) != "number") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_abcDays] = jprop_abcDays_default;
changed = true;
logInfo("toConfig: abcDays changed");
@ -1283,16 +1106,16 @@ void Configuration::toConfig(const char *buf) {
/** validate tvoc learning offset */
if (JSON.typeof_(jconfig[jprop_tvocLearningOffset]) != "number") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
int value = jconfig[jprop_tvocLearningOffset];
if (value < 0) {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_tvocLearningOffset] = jprop_tvocLearningOffset_default;
changed = true;
logInfo("toConfig: tvocLearningOffset changed");
@ -1300,16 +1123,16 @@ void Configuration::toConfig(const char *buf) {
/** validate nox learning offset */
if (JSON.typeof_(jconfig[jprop_noxLearningOffset]) != "number") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
int value = jconfig[jprop_noxLearningOffset];
if (value < 0) {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_noxLearningOffset] = jprop_noxLearningOffset_default;
changed = true;
logInfo("toConfig: noxLearningOffset changed");
@ -1317,60 +1140,36 @@ void Configuration::toConfig(const char *buf) {
/** validate mqtt broker */
if (JSON.typeof_(jconfig[jprop_mqttBrokerUrl]) != "string") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
if (isConfigFieldInvalid) {
if (isInvalid) {
changed = true;
jconfig[jprop_mqttBrokerUrl] = jprop_mqttBrokerUrl_default;
logInfo("toConfig: mqttBroker changed");
}
/** validate http domain */
if (JSON.typeof_(jconfig[jprop_httpDomain]) != "string") {
isConfigFieldInvalid = true;
} else {
isConfigFieldInvalid = false;
}
if (isConfigFieldInvalid) {
changed = true;
jconfig[jprop_httpDomain] = jprop_httpDomain_default;
logInfo("toConfig: httpDomain changed");
}
/** Validate temperature unit */
if (JSON.typeof_(jconfig[jprop_temperatureUnit]) != "string") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
String unit = jconfig[jprop_temperatureUnit];
if (unit != "c" && unit != "f") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_temperatureUnit] = jprop_temperatureUnit_default;
changed = true;
logInfo("toConfig: temperatureUnit changed");
}
/** validate disableCloudConnection configuration */
if (JSON.typeof_(jconfig[jprop_disableCloudConnection]) != "boolean") {
isConfigFieldInvalid = true;
} else {
isConfigFieldInvalid = false;
}
if (isConfigFieldInvalid) {
jconfig[jprop_disableCloudConnection] = jprop_disableCloudConnection_default;
changed = true;
logInfo("toConfig: disableCloudConnection changed");
}
/** validate configuration control */
if (JSON.typeof_(jprop_configurationControl) != "string") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
String ctrl = jconfig[jprop_configurationControl];
if (ctrl != String(CONFIGURATION_CONTROL_NAME
@ -1379,12 +1178,12 @@ void Configuration::toConfig(const char *buf) {
[ConfigurationControl::ConfigurationControlLocal]) &&
ctrl != String(CONFIGURATION_CONTROL_NAME
[ConfigurationControl::ConfigurationControlCloud])) {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_configurationControl] =jprop_configurationControl_default;
changed = true;
logInfo("toConfig: configurationControl changed");
@ -1392,11 +1191,11 @@ void Configuration::toConfig(const char *buf) {
/** Validate post to airgradient cloud */
if (JSON.typeof_(jconfig[jprop_postDataToAirGradient]) != "boolean") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_postDataToAirGradient] = jprop_postDataToAirGradient_default;
changed = true;
logInfo("toConfig: postToAirGradient changed");
@ -1404,16 +1203,16 @@ void Configuration::toConfig(const char *buf) {
/** validate led bar brightness */
if (JSON.typeof_(jconfig[jprop_ledBarBrightness]) != "number") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
int value = jconfig[jprop_ledBarBrightness];
if (value < 0 || value > 100) {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_ledBarBrightness] = jprop_ledBarBrightness_default;
changed = true;
logInfo("toConfig: ledBarBrightness changed");
@ -1421,16 +1220,16 @@ void Configuration::toConfig(const char *buf) {
/** Validate display brightness */
if (JSON.typeof_(jconfig[jprop_displayBrightness]) != "number") {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
int value = jconfig[jprop_displayBrightness];
if (value < 0 || value > 100) {
isConfigFieldInvalid = true;
isInvalid = true;
} else {
isConfigFieldInvalid = false;
isInvalid = false;
}
}
if (isConfigFieldInvalid) {
if (isInvalid) {
jconfig[jprop_displayBrightness] = jprop_displayBrightness_default;
changed = true;
logInfo("toConfig: displayBrightness changed");
@ -1449,31 +1248,15 @@ void Configuration::toConfig(const char *buf) {
jprop_monitorDisplayCompensatedValues_default;
}
// PM2.5 correction
/// Set default first before parsing local config
pmCorrection.algorithm = COR_ALGO_PM_NONE;
// Set default first before parsing local config
pmCorrection.algorithm = PMCorrectionAlgorithm::None;
pmCorrection.intercept = 0;
pmCorrection.scalingFactor = 0;
pmCorrection.useEPA = false;
/// Load correction from saved config
// Load correction from saved config
updatePmCorrection(jconfig);
// Temperature correction
/// Set default first before parsing local config
tempCorrection.algorithm = COR_ALGO_TEMP_HUM_NONE;
tempCorrection.intercept = 0;
tempCorrection.scalingFactor = 0;
/// Load correction from saved config
updateTempHumCorrection(jconfig, tempCorrection, jprop_atmp);
// Relative humidity correction
/// Set default first before parsing local config
rhumCorrection.algorithm = COR_ALGO_TEMP_HUM_NONE;
rhumCorrection.intercept = 0;
rhumCorrection.scalingFactor = 0;
/// Load correction from saved config
updateTempHumCorrection(jconfig, rhumCorrection, jprop_rhum);
if (changed) {
saveConfig();
}
@ -1551,17 +1334,6 @@ void Configuration::setOfflineModeWithoutSave(bool offline) {
_offlineMode = offline;
}
bool Configuration::isCloudConnectionDisabled(void) {
bool disabled = jconfig[jprop_disableCloudConnection];
return disabled;
}
void Configuration::setDisableCloudConnection(bool disable) {
logInfo("Set DisableCloudConnection to " + String(disable ? "True" : "False"));
jconfig[jprop_disableCloudConnection] = disable;
saveConfig();
}
bool Configuration::isLedBarModeChanged(void) {
bool changed = _ledBarModeChanged;
_ledBarModeChanged = false;
@ -1597,16 +1369,14 @@ bool Configuration::isPMCorrectionChanged(void) {
*/
bool Configuration::isPMCorrectionEnabled(void) {
PMCorrection pmCorrection = getPMCorrection();
if (pmCorrection.algorithm == COR_ALGO_PM_NONE ||
pmCorrection.algorithm == COR_ALGO_PM_UNKNOWN) {
if (pmCorrection.algorithm == PMCorrectionAlgorithm::None ||
pmCorrection.algorithm == PMCorrectionAlgorithm::Unknown) {
return false;
}
return true;
}
Configuration::PMCorrection Configuration::getPMCorrection(void) { return pmCorrection; }
Configuration::TempHumCorrection Configuration::getTempCorrection(void) { return tempCorrection; }
Configuration::TempHumCorrection Configuration::getHumCorrection(void) { return rhumCorrection; }
Configuration::PMCorrection Configuration::getPMCorrection(void) {
return pmCorrection;
}

View File

@ -17,18 +17,10 @@ public:
bool changed;
};
struct TempHumCorrection {
TempHumCorrectionAlgorithm algorithm;
float intercept;
float scalingFactor;
bool changed;
};
private:
bool co2CalibrationRequested;
bool ledBarTestRequested;
bool updated;
bool commandRequested = false;
String failedMessage;
bool _noxLearnOffsetChanged;
bool _tvocLearningOffsetChanged;
@ -38,17 +30,12 @@ private:
bool _offlineMode = false;
bool _ledBarModeChanged = false;
PMCorrection pmCorrection;
TempHumCorrection tempCorrection;
TempHumCorrection rhumCorrection;
AirGradient* ag;
String getLedBarModeName(LedBarMode mode);
PMCorrectionAlgorithm matchPmAlgorithm(String algorithm);
TempHumCorrectionAlgorithm matchTempHumAlgorithm(String algorithm);
bool updatePmCorrection(JSONVar &json);
bool updateTempHumCorrection(JSONVar &json, TempHumCorrection &target,
const char *correctionName);
void saveConfig(void);
void loadConfig(void);
void defaultConfig(void);
@ -59,7 +46,7 @@ private:
void configLogInfo(String name, String fromValue, String toValue);
String getPMStandardString(bool usaqi);
String getAbcDayString(int value);
void toConfig(const char *buf);
void toConfig(const char* buf);
public:
Configuration(Stream &debugLog);
@ -71,9 +58,6 @@ public:
bool hasSensorSGP = true;
bool hasSensorSHT = true;
typedef void (*ConfigurationUpdatedCallback_t)();
void setConfigurationUpdatedCallback(ConfigurationUpdatedCallback_t callback);
bool begin(void);
bool parse(String data, bool isLocal);
String toString(void);
@ -86,7 +70,6 @@ public:
String getLedBarModeName(void);
bool getDisplayMode(void);
String getMqttBrokerUri(void);
String getHttpDomain(void);
bool isPostDataToAirGradient(void);
ConfigurationControl getConfigurationControl(void);
bool isCo2CalibrationRequested(void);
@ -94,7 +77,6 @@ public:
void reset(void);
String getModel(void);
bool isUpdated(void);
bool isCommandRequested(void);
String getFailedMesage(void);
void setPostToAirGradient(bool enable);
bool noxLearnOffsetChanged(void);
@ -112,17 +94,11 @@ public:
bool isOfflineMode(void);
void setOfflineMode(bool offline);
void setOfflineModeWithoutSave(bool offline);
bool isCloudConnectionDisabled(void);
void setDisableCloudConnection(bool disable);
bool isLedBarModeChanged(void);
bool isMonitorDisplayCompensatedValues(void);
bool isPMCorrectionChanged(void);
bool isPMCorrectionEnabled(void);
PMCorrection getPMCorrection(void);
TempHumCorrection getTempCorrection(void);
TempHumCorrection getHumCorrection(void);
private:
ConfigurationUpdatedCallback_t _callback;
};
#endif /** _AG_CONFIG_H_ */

View File

@ -5,33 +5,14 @@
/** Cast U8G2 */
#define DISP() ((U8G2_SH1106_128X64_NONAME_F_HW_I2C *)(this->u8g2))
static const unsigned char WIFI_ISSUE_BITS[] = {
0xd8, 0xc6, 0xde, 0xde, 0xc7, 0xf8, 0xd1, 0xe2, 0xdc, 0xce, 0xcc,
0xcc, 0xc0, 0xc0, 0xd0, 0xc2, 0x00, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0};
static const unsigned char CLOUD_ISSUE_BITS[] = {
0x70, 0xc0, 0x88, 0xc0, 0x04, 0xc1, 0x04, 0xcf, 0x02, 0xd0, 0x01,
0xe0, 0x01, 0xe0, 0x01, 0xe0, 0xa2, 0xd0, 0x4c, 0xce, 0xa0, 0xc0};
// Offline mode icon
static unsigned char OFFLINE_BITS[] = {
0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x30, 0x00, 0x62, 0x00,
0xE6, 0x00, 0xFE, 0x1F, 0xFE, 0x1F, 0xE6, 0x00, 0x62, 0x00,
0x30, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
};
// {
// 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x62, 0x00, 0xE2, 0x00,
// 0xFE, 0x1F, 0xFE, 0x1F, 0xE2, 0x00, 0x62, 0x00, 0x60, 0x00, 0x30, 0x00,
// 0x00, 0x00, 0x00, 0x00, };
/**
* @brief Show dashboard temperature and humdity
*
* @param hasStatus
*/
void OledDisplay::showTempHum(bool hasStatus) {
char buf[10];
void OledDisplay::showTempHum(bool hasStatus, char *buf, int buf_size) {
/** Temperature */
float temp = value.getCorrectedTempHum(Measurements::Temperature, 1);
float temp = value.getAverage(Measurements::Temperature);
if (utils::isValidTemperature(temp)) {
float t = 0.0f;
if (config.isTemperatureUnitInF()) {
@ -42,32 +23,32 @@ void OledDisplay::showTempHum(bool hasStatus) {
if (config.isTemperatureUnitInF()) {
if (hasStatus) {
snprintf(buf, sizeof(buf), "%0.1f", t);
snprintf(buf, buf_size, "%0.1f", t);
} else {
snprintf(buf, sizeof(buf), "%0.1f°F", t);
snprintf(buf, buf_size, "%0.1f°F", t);
}
} else {
if (hasStatus) {
snprintf(buf, sizeof(buf), "%.1f", t);
snprintf(buf, buf_size, "%.1f", t);
} else {
snprintf(buf, sizeof(buf), "%.1f°C", t);
snprintf(buf, buf_size, "%.1f°C", t);
}
}
} else { /** Show invalid value */
if (config.isTemperatureUnitInF()) {
snprintf(buf, sizeof(buf), "-°F");
snprintf(buf, buf_size, "-°F");
} else {
snprintf(buf, sizeof(buf), "-°C");
snprintf(buf, buf_size, "-°C");
}
}
DISP()->drawUTF8(1, 10, buf);
/** Show humidity */
int rhum = round(value.getCorrectedTempHum(Measurements::Humidity, 1));
int rhum = round(value.getAverage(Measurements::Humidity));
if (utils::isValidHumidity(rhum)) {
snprintf(buf, sizeof(buf), "%d%%", rhum);
snprintf(buf, buf_size, "%d%%", rhum);
} else {
snprintf(buf, sizeof(buf), "-%%");
snprintf(buf, buf_size, "-%%");
}
if (rhum > 99.0) {
@ -86,9 +67,6 @@ void OledDisplay::setCentralText(int y, const char *text) {
DISP()->drawStr(x, y, text);
}
void OledDisplay::showIcon(int x, int y, xbm_icon_t *icon) {
DISP()->drawXBM(x, y, icon->width, icon->height, icon->icon);
}
/**
* @brief Construct a new Ag Oled Display:: Ag Oled Display object
*
@ -274,60 +252,36 @@ void OledDisplay::setText(const char *line1, const char *line2,
* @brief Update dashboard content
*
*/
void OledDisplay::showDashboard(void) { showDashboard(DashBoardStatusNone); }
void OledDisplay::showDashboard(void) { showDashboard(NULL); }
/**
* @brief Update dashboard content and error status
*
*/
void OledDisplay::showDashboard(DashboardStatus status) {
void OledDisplay::showDashboard(const char *status) {
if (isDisplayOff) {
return;
}
char strBuf[16];
const int icon_pos_x = 64;
xbm_icon_t xbm_icon = {
.width = 0,
.height = 0,
.icon = nullptr,
};
if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) {
DISP()->firstPage();
do {
DISP()->setFont(u8g2_font_t0_16_tf);
switch (status) {
case DashBoardStatusNone: {
// Maybe show signal strength?
showTempHum(false);
break;
}
case DashBoardStatusWiFiIssue: {
DISP()->drawXBM(icon_pos_x, 0, 14, 11, WIFI_ISSUE_BITS);
showTempHum(false);
break;
}
case DashBoardStatusServerIssue: {
DISP()->drawXBM(icon_pos_x, 0, 14, 11, CLOUD_ISSUE_BITS);
showTempHum(false);
break;
}
case DashBoardStatusAddToDashboard: {
setCentralText(10, "Add To Dashboard");
break;
}
case DashBoardStatusDeviceId: {
setCentralText(10, ag->deviceId().c_str());
break;
}
case DashBoardStatusOfflineMode: {
DISP()->drawXBM(icon_pos_x, 0, 14, 14, OFFLINE_BITS);
showTempHum(false); // First true
break;
}
default:
break;
if ((status == NULL) || (strlen(status) == 0)) {
showTempHum(false, strBuf, sizeof(strBuf));
} else {
String strStatus = "Show status: " + String(status);
logInfo(strStatus);
int strWidth = DISP()->getStrWidth(status);
DISP()->drawStr((DISP()->getWidth() - strWidth) / 2, 10, status);
/** Show WiFi NA*/
if (strcmp(status, "WiFi N/A") == 0) {
DISP()->setFont(u8g2_font_t0_12_tf);
showTempHum(true, strBuf, sizeof(strBuf));
}
}
/** Draw horizonal line */
@ -362,7 +316,7 @@ void OledDisplay::showDashboard(DashboardStatus status) {
int pm25 = round(value.getAverage(Measurements::PM25));
if (utils::isValidPm(pm25)) {
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
pm25 = round(value.getCorrectedPM25(true));
pm25 = round(value.getCorrectedPM25(*ag, config, true));
}
if (config.isPmStandardInUSAQI()) {
sprintf(strBuf, "%d", ag->pms5003.convertPm25ToUsAqi(pm25));
@ -423,7 +377,7 @@ void OledDisplay::showDashboard(DashboardStatus status) {
/** Set PM */
int pm25 = round(value.getAverage(Measurements::PM25));
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
pm25 = round(value.getCorrectedPM25(true));
pm25 = round(value.getCorrectedPM25(*ag, config, true));
}
ag->display.setCursor(0, 12);
@ -435,11 +389,10 @@ void OledDisplay::showDashboard(DashboardStatus status) {
ag->display.setText(strBuf);
/** Set temperature and humidity */
float temp = value.getCorrectedTempHum(Measurements::Temperature, 1);
float temp = value.getAverage(Measurements::Temperature);
if (utils::isValidTemperature(temp)) {
if (config.isTemperatureUnitInF()) {
snprintf(strBuf, sizeof(strBuf), "T:%0.1f F",
utils::degreeC_To_F(temp));
snprintf(strBuf, sizeof(strBuf), "T:%0.1f F", utils::degreeC_To_F(temp));
} else {
snprintf(strBuf, sizeof(strBuf), "T:%0.1f C", temp);
}
@ -454,7 +407,7 @@ void OledDisplay::showDashboard(DashboardStatus status) {
ag->display.setCursor(0, 24);
ag->display.setText(strBuf);
int rhum = round(value.getCorrectedTempHum(Measurements::Humidity, 1));
int rhum = round(value.getAverage(Measurements::Humidity));
if (utils::isValidHumidity(rhum)) {
snprintf(strBuf, sizeof(strBuf), "H:%d %%", rhum);
} else {
@ -489,7 +442,8 @@ void OledDisplay::setBrightness(int percent) {
// Clear display.
ag->display.clear();
ag->display.show();
} else {
}
else {
isDisplayOff = false;
ag->display.setContrast((255 * percent) / 100);
}

View File

@ -16,32 +16,17 @@ private:
Measurements &value;
bool isDisplayOff = false;
typedef struct {
int width;
int height;
unsigned char *icon;
} xbm_icon_t;
void showTempHum(bool hasStatus);
void showTempHum(bool hasStatus, char* buf, int buf_size);
void setCentralText(int y, String text);
void setCentralText(int y, const char *text);
void showIcon(int x, int y, xbm_icon_t *icon);
public:
OledDisplay(Configuration &config, Measurements &value, Stream &log);
OledDisplay(Configuration &config, Measurements &value,
Stream &log);
~OledDisplay();
enum DashboardStatus {
DashBoardStatusNone,
DashBoardStatusWiFiIssue,
DashBoardStatusServerIssue,
DashBoardStatusAddToDashboard,
DashBoardStatusDeviceId,
DashBoardStatusOfflineMode,
};
void setAirGradient(AirGradient *ag);
bool begin(void);
bool begin(void);
void end(void);
void setText(String &line1, String &line2, String &line3);
void setText(const char *line1, const char *line2, const char *line3);
@ -49,7 +34,7 @@ public:
void setText(const char *line1, const char *line2, const char *line3,
const char *line4);
void showDashboard(void);
void showDashboard(DashboardStatus status);
void showDashboard(const char *status);
void setBrightness(int percent);
#ifdef ESP32
void showFirmwareUpdateVersion(String version);

View File

@ -1,7 +1,6 @@
#include "AgStateMachine.h"
#include "AgOledDisplay.h"
#define LED_TEST_BLINK_DELAY 50 /** ms */
#define LED_TEST_BLINK_DELAY 50 /** ms */
#define LED_FAST_BLINK_DELAY 250 /** ms */
#define LED_SLOW_BLINK_DELAY 1000 /** ms */
#define LED_SHORT_BLINK_DELAY 500 /** ms */
@ -9,9 +8,9 @@
#define SENSOR_CO2_CALIB_COUNTDOWN_MAX 5 /** sec */
#define RGB_COLOR_R 255, 0, 0 /** Red */
#define RGB_COLOR_G 0, 255, 0 /** Green */
#define RGB_COLOR_Y 255, 150, 0 /** Yellow */
#define RGB_COLOR_R 255, 0, 0 /** Red */
#define RGB_COLOR_G 0, 255, 0 /** Green */
#define RGB_COLOR_Y 255, 150, 0 /** Yellow */
#define RGB_COLOR_O 255, 40, 0 /** Orange */
#define RGB_COLOR_P 180, 0, 255 /** Purple */
#define RGB_COLOR_CLEAR 0, 0, 0 /** No color */
@ -51,7 +50,7 @@ void StateMachine::ledStatusBlinkDelay(uint32_t ms) {
/**
* @brief Led bar show PM or CO2 led color status
*
* @return true if all led bar are used, false othwerwise
* @return true if all led bar are used, false othwerwise
*/
bool StateMachine::sensorhandleLeds(void) {
int totalLedUsed = 0;
@ -83,7 +82,7 @@ bool StateMachine::sensorhandleLeds(void) {
/**
* @brief Show CO2 LED status
*
* @return return total number of led that are used on the monitor
* @return return total number of led that are used on the monitor
*/
int StateMachine::co2handleLeds(void) {
int totalUsed = ag->ledBar.getNumberOfLeds();
@ -167,15 +166,15 @@ int StateMachine::co2handleLeds(void) {
/**
* @brief Show PM2.5 LED status
*
* @return return total number of led that are used on the monitor
*
* @return return total number of led that are used on the monitor
*/
int StateMachine::pm25handleLeds(void) {
int totalUsed = ag->ledBar.getNumberOfLeds();
int pm25Value = round(value.getAverage(Measurements::PM25));
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
pm25Value = round(value.getCorrectedPM25(true));
pm25Value = round(value.getCorrectedPM25(*ag, config, true));
}
if (pm25Value <= 5) {
@ -370,17 +369,18 @@ void StateMachine::ledBarTest(void) {
} else {
ledBarRunTest();
}
} else if (ag->isOpenAir()) {
}
else if(ag->isOpenAir()) {
ledBarRunTest();
}
}
}
void StateMachine::ledBarPowerUpTest(void) {
void StateMachine::ledBarPowerUpTest(void) {
if (ag->isOne()) {
ag->ledBar.clear();
}
ledBarRunTest();
ledBarRunTest();
}
void StateMachine::ledBarRunTest(void) {
@ -544,11 +544,11 @@ void StateMachine::displayHandle(AgStateMachineState state) {
break;
}
case AgStateMachineWiFiLost: {
disp.showDashboard(OledDisplay::DashBoardStatusWiFiIssue);
disp.showDashboard("WiFi N/A");
break;
}
case AgStateMachineServerLost: {
disp.showDashboard(OledDisplay::DashBoardStatusServerIssue);
disp.showDashboard("AG Server N/A");
break;
}
case AgStateMachineSensorConfigFailed: {
@ -557,24 +557,19 @@ void StateMachine::displayHandle(AgStateMachineState state) {
if (ms >= 5000) {
addToDashboardTime = millis();
if (addToDashBoardToggle) {
disp.showDashboard(OledDisplay::DashBoardStatusAddToDashboard);
disp.showDashboard("Add to AG Dashb.");
} else {
disp.showDashboard(OledDisplay::DashBoardStatusDeviceId);
disp.showDashboard(ag->deviceId().c_str());
}
addToDashBoardToggle = !addToDashBoardToggle;
}
} else {
disp.showDashboard();
disp.showDashboard("");
}
break;
}
case AgStateMachineNormal: {
if (config.isOfflineMode()) {
disp.showDashboard(
OledDisplay::DashBoardStatusOfflineMode);
} else {
disp.showDashboard();
}
disp.showDashboard();
break;
}
case AgStateMachineCo2Calibration:

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,6 @@
#include "Libraries/Arduino_JSON/src/Arduino_JSON.h"
#include "Main/utils.h"
#include <Arduino.h>
#include <cstdint>
#include <vector>
class Measurements {
@ -35,36 +34,9 @@ private:
};
public:
Measurements(Configuration &config);
Measurements() {}
~Measurements() {}
struct Measures {
float temperature[2];
float humidity[2];
float co2;
float tvoc; // Index value
float tvoc_raw;
float nox; // Index value
float nox_raw;
float pm_01[2]; // pm 1.0 atmospheric environment
float pm_25[2]; // pm 2.5 atmospheric environment
float pm_10[2]; // pm 10 atmospheric environment
float pm_01_sp[2]; // pm 1.0 standard particle
float pm_25_sp[2]; // pm 2.5 standard particle
float pm_10_sp[2]; // pm 10 standard particle
float pm_03_pc[2]; // particle count 0.3
float pm_05_pc[2]; // particle count 0.5
float pm_01_pc[2]; // particle count 1.0
float pm_25_pc[2]; // particle count 2.5
float pm_5_pc[2]; // particle count 5.0
float pm_10_pc[2]; // particle count 10
int bootCount;
int signal;
uint32_t freeHeap;
};
void setAirGradient(AirGradient *ag);
// Enumeration for every AG measurements
enum MeasurementType {
Temperature,
@ -88,8 +60,6 @@ public:
PM10_PC, // Particle 10 count
};
void printCurrentAverage();
/**
* @brief Set each MeasurementType maximum period length for moving average
*
@ -153,55 +123,38 @@ public:
*/
float getAverage(MeasurementType type, int ch = 1);
/**
* @brief Get Temperature or Humidity correction value
* Only if correction is applied from configuration or forceCorrection is True
*
* @param type measurement type either Temperature or Humidity
* @param ch target type value channel
* @param forceCorrection force using correction even though config correction is not applied, but
* not for CUSTOM
* @return correction value
*/
float getCorrectedTempHum(MeasurementType type, int ch = 1, bool forceCorrection = false);
/**
* @brief Get the Corrected PM25 object based on the correction algorithm from configuration
*
*
* If correction is not enabled, then will return the raw value (either average or last value)
*
* @param ag AirGradient instance
* @param config Configuration instance
* @param useAvg Use moving average value if true, otherwise use latest value
* @param ch MeasurementType channel
* @param forceCorrection force using correction even though config correction is not applied, default to EPA
* @return float Corrected PM2.5 value
*/
float getCorrectedPM25(bool useAvg = false, int ch = 1, bool forceCorrection = false);
float getCorrectedPM25(AirGradient &ag, Configuration &config, bool useAvg = false, int ch = 1);
/**
* build json payload for every measurements
*/
String toString(bool localServer, AgFirmwareMode fwMode, int rssi);
String toString(bool localServer, AgFirmwareMode fwMode, int rssi, AirGradient &ag,
Configuration &config);
Measures getMeasures();
std::string buildMeasuresPayload(Measures &measures);
bool resetLocalStorage();
bool saveLocalStorage(AirGradient &ag, Configuration &config);
char *getLocalStorage();
/**
* Set to true if want to debug every update value
*/
void setDebug(bool debug);
int bootCount();
void setBootCount(int bootCount);
#ifndef ESP8266
void setResetReason(esp_reset_reason_t reason);
#endif
// TODO: update this to use setter
int bootCount;
private:
Configuration &config;
AirGradient *ag;
// Some declared as an array (channel), because FW_MODE_O_1PPx has two PMS5003T
FloatValue _temperature[2];
FloatValue _humidity[2];
@ -222,9 +175,9 @@ private:
IntegerValue _pm_25_pc[2]; // particle count 2.5
IntegerValue _pm_5_pc[2]; // particle count 5.0
IntegerValue _pm_10_pc[2]; // particle count 10
int _bootCount;
int _resetReason;
bool _debug = false;
const char *FILE_PATH = "/measurements.csv"; // Local storage file path
/**
* @brief Get PMS5003 firmware version string
@ -260,11 +213,10 @@ private:
*/
void validateChannel(int ch);
void printCurrentPMAverage(int ch);
JSONVar buildOutdoor(bool localServer, AgFirmwareMode fwMode);
JSONVar buildIndoor(bool localServer);
JSONVar buildPMS(int ch, bool allCh, bool withTempHum, bool compensate);
JSONVar buildOutdoor(bool localServer, AgFirmwareMode fwMode, AirGradient &ag,
Configuration &config);
JSONVar buildIndoor(bool localServer, AirGradient &ag, Configuration &config);
JSONVar buildPMS(AirGradient &ag, int ch, bool allCh, bool withTempHum, bool compensate);
};
#endif /** _AG_VALUE_H_ */

View File

@ -81,15 +81,16 @@ bool WifiConnector::connect(void) {
// ssid = "AG-" + String(ESP.getChipId(), HEX);
WIFI()->setConfigPortalTimeout(WIFI_CONNECT_COUNTDOWN_MAX);
WiFiManagerParameter disableCloud("chbPostToAg", "Prevent Connection to AirGradient Server", "T",
2, "type=\"checkbox\" ", WFM_LABEL_AFTER);
WIFI()->addParameter(&disableCloud);
WiFiManagerParameter disableCloudInfo(
WiFiManagerParameter postToAg("chbPostToAg",
"Prevent Connection to AirGradient Server", "T",
2, "type=\"checkbox\" ", WFM_LABEL_AFTER);
WIFI()->addParameter(&postToAg);
WiFiManagerParameter postToAgInfo(
"<p>Prevent connection to the AirGradient Server. Important: Only enable "
"it if you are sure you don't want to use any AirGradient cloud "
"features. As a result you will not receive automatic firmware updates, "
"configuration settings from cloud and the measure data will not reach the AirGradient dashboard.</p>");
WIFI()->addParameter(&disableCloudInfo);
"features. As a result you will not receive automatic firmware updates "
"and your data will not reach the AirGradient dashboard.</p>");
WIFI()->addParameter(&postToAgInfo);
WIFI()->autoConnect(ssid.c_str(), WIFI_HOTSPOT_PASSWORD_DEFAULT);
@ -173,11 +174,12 @@ bool WifiConnector::connect(void) {
logInfo("WiFi Connected: " + WiFi.SSID() + " IP: " + localIpStr());
if (hasPortalConfig) {
String result = String(disableCloud.getValue());
logInfo("Setting disableCloudConnection set from " +
String(config.isCloudConnectionDisabled() ? "True" : "False") + String(" to ") +
String(result == "T" ? "True" : "False") + String(" successful"));
config.setDisableCloudConnection(result == "T");
String result = String(postToAg.getValue());
logInfo("Setting postToAirGradient set from " +
String(config.isPostDataToAirGradient() ? "True" : "False") +
String(" to ") + String(result != "T" ? "True" : "False") +
String(" successful"));
config.setPostToAirGradient(result != "T");
}
hasPortalConfig = false;
}

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

@ -15,47 +15,7 @@
#include "Main/utils.h"
#ifndef GIT_VERSION
#define GIT_VERSION "3.3.9-snap"
#endif
#ifndef ESP8266
// Airgradient server root ca certificate
const char *const AG_SERVER_ROOT_CA =
"-----BEGIN CERTIFICATE-----\n"
"MIIF4jCCA8oCCQD7MgvcaVWxkTANBgkqhkiG9w0BAQsFADCBsjELMAkGA1UEBhMC\n"
"VEgxEzARBgNVBAgMCkNoaWFuZyBNYWkxEDAOBgNVBAcMB01hZSBSaW0xGTAXBgNV\n"
"BAoMEEFpckdyYWRpZW50IEx0ZC4xFDASBgNVBAsMC1NlbnNvciBMYWJzMSgwJgYD\n"
"VQQDDB9BaXJHcmFkaWVudCBTZW5zb3IgTGFicyBSb290IENBMSEwHwYJKoZIhvcN\n"
"AQkBFhJjYUBhaXJncmFkaWVudC5jb20wHhcNMjEwOTE3MTE0NDE3WhcNNDEwOTEy\n"
"MTE0NDE3WjCBsjELMAkGA1UEBhMCVEgxEzARBgNVBAgMCkNoaWFuZyBNYWkxEDAO\n"
"BgNVBAcMB01hZSBSaW0xGTAXBgNVBAoMEEFpckdyYWRpZW50IEx0ZC4xFDASBgNV\n"
"BAsMC1NlbnNvciBMYWJzMSgwJgYDVQQDDB9BaXJHcmFkaWVudCBTZW5zb3IgTGFi\n"
"cyBSb290IENBMSEwHwYJKoZIhvcNAQkBFhJjYUBhaXJncmFkaWVudC5jb20wggIi\n"
"MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC6XkVQ4O9d5GcUjPYRgF/uaY6O\n"
"5ry1xCGvotxkEeKkBk99lB1oNUUfNsP5bwuDci4XKfY9Ro6/jmkfHSVcPAwUnjAt\n"
"BcHqZtA/cMXykaynf9yXPxPQN7XLu/Rk32RIfb90sIGS318xgNziCYvzWZmlxpxc\n"
"3gUcAgGtamlgZ6wD3yOHVo8B9aFNvmP16QwkUm8fKDHunJG+iX2Bxa4ka5FJovhG\n"
"TnUwtso6Vrn0JaWF9qWcPZE0JZMjFW8PYRriyJmHwr/nAXfPPKphD1oRO+oA7/jq\n"
"dYkrJw6+OHfFXnPB1xkeh4OPBzcCZHT5XWNfwBYazYpjcJa9ngGFSmg8lX1ac23C\n"
"zea1XJmSrPwbZbWxoQznnf7Y78mRjruYKgSP8rf74KYvBe/HGPL5NQyXQ3l6kwmu\n"
"CCUqfcC0wCWEtWESxwSdFE2qQii8CZ12kQExzvR2PrOIyKQYSdkGx9/RBZtAVPXP\n"
"hmLuRBQYHrF5Cxf1oIbBK8OMoNVgBm6ftt15t9Sq9dH5Aup2YR6WEJkVaYkYzZzK\n"
"X7M+SQcdbXp+hAO8PFpABJxkaDAO2kiB5Ov7pDYPAcmNFqnJT48AY0TZJeVeCa5W\n"
"sIv3lPvB/XcFjP0+aZxxNSEEwpGPUYgvKUYUUmb0NammlYQwZHKaShPEmZ3UZ0bp\n"
"VNt4p6374nzO376sSwIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQB/LfBPgTx7xKQB\n"
"JNMUhah17AFAn050NiviGJOHdPQely6u3DmJGg+ijEVlPWO1FEW3it+LOuNP5zOu\n"
"bhq8paTYIxPxtALIxw5ksykX9woDuX3H6FF9mPdQIbL7ft+3ZtZ4FWPui9dUtaPe\n"
"ZBmDFDi4U29nhWZK68JSp5QkWjfaYLV/vtag7120eVyGEPFZ0UAuTUNqpw+stOt9\n"
"gJ2ZxNx13xJ8ZnLK7qz1crPe8/8IVAdxbVLoY7JaWPLc//+VF+ceKicy8+4gV7zN\n"
"Gnq2IyM+CHFz8VYMLbW+3eVp4iJjTa72vae116kozboEIUVN9rgLqIKyVqQXiuoN\n"
"g3xY+yfncPB2+H/+lfyy6mepPIfgksd3+KeNxFADSc5EVY2JKEdorRodnAh7a8K6\n"
"WjTYgq+GjWXU2uQW2SyPt6Tu33OT8nBnu3NB80eT8WXgdVCkgsuyCuLvNRf1Xmze\n"
"igvurpU6JmQ1GlLgLJo8omJHTh1zIbkR9injPYne2v9ciHCoP6+LDEqe+rOsvPCB\n"
"C/o/iZ4svmYX4fWGuU7GgqZE8hhrC3+GdOTf2ADC752cYCZxBidXGtkrGNoHQKmQ\n"
"KCOMFBxZIvWteB3tUo3BKYz1D2CvKWz1wV4moc5JHkOgS+jqxhvOkQ/vfQBQ1pUY\n"
"TMui9BSwU7B1G2XjdLbfF3Dc67zaSg==\n"
"-----END CERTIFICATE-----\n";
#define GIT_VERSION "3.1.13-snap"
#endif
/**
@ -213,6 +173,9 @@ public:
*/
String deviceId(void);
void setCurrentTime(long epochTime);
String getCurrentTime();
private:
BoardType boardType;
};

View File

@ -95,29 +95,26 @@ enum ConfigurationControl {
};
enum PMCorrectionAlgorithm {
COR_ALGO_PM_UNKNOWN, // Unknown algorithm
COR_ALGO_PM_NONE, // No PM correction
COR_ALGO_PM_EPA_2021,
COR_ALGO_PM_SLR_CUSTOM,
};
// Don't change the order of the enum
enum TempHumCorrectionAlgorithm {
COR_ALGO_TEMP_HUM_UNKNOWN, // Unknown algorithm
COR_ALGO_TEMP_HUM_NONE, // No PM correction
COR_ALGO_TEMP_HUM_AG_PMS5003T_2024,
COR_ALGO_TEMP_HUM_SLR_CUSTOM
Unknown, // Unknown algorithm
None, // No PM correction
EPA_2021,
SLR_PMS5003_20220802,
SLR_PMS5003_20220803,
SLR_PMS5003_20220824,
SLR_PMS5003_20231030,
SLR_PMS5003_20231218,
SLR_PMS5003_20240104,
};
enum AgFirmwareMode {
FW_MODE_I_9PSL, /** ONE_INDOOR */
FW_MODE_O_1PST, /** PMS5003T, S8 and SGP41 */
FW_MODE_O_1PPT, /** PMS5003T_1, PMS5003T_2, SGP41 */
FW_MODE_O_1PP, /** PMS5003T_1, PMS5003T_2 */
FW_MODE_O_1PS, /** PMS5003T, S8 */
FW_MODE_O_1P, /** PMS5003T */
FW_MODE_I_42PS, /** DIY_PRO 4.2 */
FW_MODE_I_33PS, /** DIY_PRO 3.3 */
FW_MODE_I_9PSL, /** ONE_INDOOR */
FW_MODE_O_1PST, /** PMS5003T, S8 and SGP41 */
FW_MODE_O_1PPT, /** PMS5003T_1, PMS5003T_2, SGP41 */
FW_MODE_O_1PP, /** PMS5003T_1, PMS5003T_2 */
FW_MODE_O_1PS, /** PMS5003T, S8 */
FW_MODE_O_1P, /** PMS5003T */
FW_MODE_I_42PS, /** DIY_PRO 4.2 */
FW_MODE_I_33PS, /** DIY_PRO 3.3 */
FW_MODE_I_BASIC_40PS, /** DIY_BASIC 4.0 */
};
const char *AgFirmwareModeName(AgFirmwareMode mode);

View File

@ -131,22 +131,6 @@ void Sgp41::handle(void) {
}
#else
void Sgp41::pause() {
onPause = true;
Serial.println("Pausing SGP41 handler task");
// Set latest value to invalid
tvocRaw = utils::getInvalidVOC();
tvoc = utils::getInvalidVOC();
noxRaw = utils::getInvalidNOx();
nox = utils::getInvalidNOx();
}
void Sgp41::resume() {
onPause = false;
Serial.println("Resuming SGP41 handler task");
}
/**
* @brief Handle the sensor conditioning and run time udpate value, This method
* must not call, it's called on private task
@ -168,11 +152,6 @@ void Sgp41::_handle(void) {
uint16_t srawVoc, srawNox;
for (;;) {
vTaskDelay(pdMS_TO_TICKS(1000));
if (onPause) {
continue;
}
if (getRawSignal(srawVoc, srawNox)) {
tvocRaw = srawVoc;
noxRaw = srawNox;

View File

@ -18,10 +18,6 @@ public:
bool begin(TwoWire &wire, Stream &stream);
void handle(void);
#else
/* pause _handle task to read sensor */
void pause();
/* resume _handle task to read sensor */
void resume();
void _handle(void);
#endif
void end(void);
@ -36,7 +32,6 @@ public:
int getTvocLearningOffset(void);
private:
bool onPause = false;
bool onConditioning = true;
bool ready = false;
bool _isBegin = false;