Compare commits

...

106 Commits

Author SHA1 Message Date
02849a1938 Fix pass agclient to openmetrics
previously, agclient initialized after setAirgradient
2025-05-01 13:38:42 +08:00
074337a96d Merge pull request #304 from airgradienthq/fix/api-root
FIX: HTTP domain configuration changes applied for OTA too
2025-04-21 13:42:26 +07:00
4daa817a0b Change airgradient-ota commit to main branch 2025-04-21 13:41:15 +07:00
81a4502952 Fix: http domain applied for OTA 2025-04-21 13:27:05 +07:00
764e2eae38 Prepare release 3.3.6 2025-04-16 12:34:17 +07:00
79bf9811be Merge pull request #303 from airgradienthq/fix/ce-tvoc
Fix incorrect TVOC / NOx values when when network option is cellular
2025-04-15 12:25:39 +07:00
9475724d0c Remove comment 2025-04-15 12:20:26 +07:00
e7603a7659 Update feedback
Change airgradient-ota submodule to latest main instead of branch
2025-04-14 15:24:53 +07:00
9bba89722e Fix sgp unreliable value by only pause task when performing ota 2025-04-12 02:25:04 +07:00
81945a358e SGP41 add method to pause and resume task handle 2025-04-12 02:22:55 +07:00
3d26a54d69 Prepare release 3.3.5 2025-04-11 15:56:05 +07:00
b70ee75d50 Merge pull request #302 from airgradienthq/improve-ce-reconnection
Improve cellular client reconnection
2025-04-11 15:49:25 +07:00
c6846c818a Rename MICROS_TO_MINUTES() to follow convention 2025-04-11 15:46:21 +07:00
0b1c901a76 Rename cellularModule object name to cellularCard
Rename checkCellularClientNotReady to restartIfCeClientIssueOverTwoHours
2025-04-11 13:41:07 +07:00
83504c8628 Bump libs to latest 2025-04-10 19:05:28 +07:00
4487992748 Remove unnecessary code 2025-04-10 14:58:51 +07:00
3c8a65a329 Use esp_timer_get_time for timer of ce client not ready 2025-04-10 14:58:11 +07:00
673d564ddb Fix based on feedback 2025-04-10 12:45:18 +07:00
423eb4808f Change airgradient-client to latest main 2025-04-10 02:14:34 +07:00
18a710ffc2 Make sure transmit cycle not too long to wait divisible by 3 2025-04-10 02:06:11 +07:00
040cb79a4d Transmit measures only if queue size is 1 or divisible by 3 2025-04-10 00:27:44 +07:00
52d3dc03f1 Redundant check if cellular client not ready for 2 hours
Check calls happen in both task
2025-04-09 23:46:03 +07:00
1c6bc3ec55 Bump airgradient-client fix esp8266 compile 2025-04-09 22:48:21 +07:00
34d7c93e14 Improve reconnection of CE network option
Restart system if it already too long
2025-04-09 15:51:54 +07:00
fee1dc25d6 Improve reconnection of CE network option
Restart system if it already too long
Bump airgradient-client: Improve ensureClientConnection
2025-04-09 15:49:34 +07:00
9fb01d42f4 Prepare release 3.3.4 2025-04-07 16:56:54 +07:00
7bb013939c Merge pull request #301 from airgradienthq/feat/signal
Include cellular signal in RSSI (dbm) when post measures
2025-04-07 16:55:42 +07:00
0da21155e7 bump submodule to post measures with new endpoint
that include signal in rssi
2025-04-07 16:29:54 +07:00
7a153cc0ea add cellular signal quality to post measures payload
If value invalid 0, then do include it to payload
2025-04-07 16:29:15 +07:00
b079c35e6b Include cellular signal in rssi to measurement cycle 2025-04-07 16:28:37 +07:00
6051e183b8 Merge pull request #300 from airgradienthq/fix/pms-error
Remove CORE_DEBUG_LEVEL that affected PM sensor reading
2025-04-07 15:33:37 +07:00
c95379b957 Update submodule to the latest main branch 2025-04-07 15:30:52 +07:00
0cae8bc185 Change ag log level to info 2025-04-05 23:56:14 +07:00
5902a4c8e4 Remove arduino-esp32 core debug level from build_flags
And change it to airgradient log level that take effect to airgradient submodules
Temporary bump submodule to WIP branch
2025-04-05 23:45:46 +07:00
66818cd075 prepare 3.3.3 release 2025-04-04 11:31:19 +07:00
c1a6ddc68f Merge pull request #299 from airgradienthq/tmp/avg-max-period
Calculate measurement average max period use the same constant
2025-04-04 11:04:15 +07:00
20a32dd22c Measures average max period use the same constant
Cellular network options using wifi measurement interval as the constant reference to calculate max period
2025-04-04 10:54:03 +07:00
263dc9934e Merge pull request #298 from airgradienthq/fix/recover-cellular-connection
Restarting cellular module when cellular client is not ready
2025-04-04 10:43:59 +07:00
61b863b7f1 Fix esp_log logs not come out on O-PP 2025-04-04 10:21:50 +07:00
e01c1029fe Bump ag client
ensure client connection properly
2025-04-04 10:09:34 +07:00
ba5d817739 Merge pull request #297 from airgradienthq/feat/api-root
New local configuration to set HTTP domain name for monitor to post measures and fetch configuration from server
2025-04-03 16:51:04 +07:00
a91747e379 Update config sample 2025-04-02 16:30:13 +07:00
029457c3fa Add accepted value to http domain 2025-04-02 16:26:07 +07:00
55710dd4d9 Update docs for new configuration http domain 2025-04-02 16:18:55 +07:00
4886163cda Show on oled when httpDomain is set 2025-04-02 02:33:24 +07:00
7c57477238 Add local configuration to set http domain
change http domain by PUT from local server request
2025-04-02 02:12:13 +07:00
9ed58d1853 Prepare release 3.3.2 2025-03-31 17:12:15 +07:00
6c52b038e9 Merge pull request #295 from airgradienthq/feat/enable-at-debug
Enable cellular AT command debug when in network registration
2025-03-31 17:09:43 +07:00
2f69932ef7 add depth submodule update 2025-03-31 17:04:37 +07:00
1d96a274a6 Merge branch 'develop' into feat/enable-at-debug 2025-03-31 16:55:12 +07:00
df9f6dfc95 Fix bugs from 3.3.1 release 2025-03-31 16:52:09 +07:00
3fc02b3f54 Check signal when initialize cellular client 2025-03-31 16:51:29 +07:00
958ed0bd80 Fix TVOC and NOx payload position 2025-03-31 15:26:34 +07:00
e9be9dcc83 Fix mqtt host still exist on local when on server is disabled 2025-03-31 14:51:53 +07:00
7fbab82088 Change log level when correction not found 2025-03-31 14:07:30 +07:00
decdecdf22 Don't start mqtt when network option is cellular
Even when mqtt host is set
2025-03-31 14:01:49 +07:00
145c612867 Enable cellular at debug when registering network
On boot, airgradient-client change cellular init timeout to 5 mins
2025-03-31 13:53:56 +07:00
37de127887 prepare 3.3.1 release 2025-03-28 14:37:08 +07:00
baf80ce250 untrack compile_commands.json 2025-03-28 14:24:13 +07:00
80100e2475 prepare 3.3.0 release 2025-03-28 14:13:40 +07:00
d9c3fc6ec4 Merge pull request #292 from airgradienthq/feat/cellular
Add cellular connection as network options for AirGradient ONE and Open Air
2025-03-28 13:55:17 +07:00
67d377a514 Rename measurementCycle on agVlaue to Measures
capitalize static const for image bit
2025-03-28 13:45:07 +07:00
fff982f35f Apply stop main task for wifi too
Improve flow OTA success display
2025-03-27 17:46:08 +07:00
86cd90b94a Handling cellular client not ready better 2025-03-27 16:10:56 +07:00
656509c74d resize measurement cycle queue if already more than reserved 2025-03-27 14:34:26 +07:00
01f83cb02e update how to contribute 2025-03-27 14:11:10 +07:00
5c9c25c6b5 update how to compile with submodule 2025-03-27 14:04:24 +07:00
9291598209 Fix compile error esp8266 boards 2025-03-26 21:38:21 +07:00
429adb5e5e Remove otahandler from source file 2025-03-26 17:57:13 +07:00
4e651afc8c Remove oneopenair deps from AgApiClient 2025-03-26 17:43:39 +07:00
859abbe177 Update github action to recursive submodules when checkout 2025-03-26 17:41:49 +07:00
f079bb30d2 Update submodule
Printout http urls
fix progress more than 100%
2025-03-26 16:23:32 +07:00
070a103234 build CE payload include tvoc and nox index
If measures value invalid, set it to empty
New schedule to print network signal
2025-03-26 16:18:48 +07:00
ef87cde9d6 Change error status on display to icon 2025-03-24 03:52:46 +07:00
ea5e23b307 Fix cellular payload
Interval value should be in seconds
2025-03-24 03:49:33 +07:00
c2a26e78a0 agclient keep serial number on initialization 2025-03-23 21:48:46 +07:00
0297059e91 Fix check pm0003 count is valid
AgLog set to debug level
2025-03-21 08:46:14 +07:00
30622fca99 MeasurementCycle queue only applied for cellular
Cellular post measures payload different with wifi
Update submodule to support different cellular post endpoint
2025-03-21 04:40:27 +07:00
7c2aa35e4f Fix wifi connection error when using cellular 2025-03-18 01:02:10 +07:00
e93009f31c Decrease delay otaInProgress check
Do not run NetworkingTask when in offline mode
2025-03-18 00:01:04 +07:00
26db6372cd Tested ota cellular integration 2025-03-17 22:17:59 +07:00
d94ebbc570 Integrate ota 2025-03-17 15:12:11 +07:00
299234ac40 Update OneOpenAir.ino 2025-03-17 02:20:43 +07:00
76b2b3f940 Adjust interval based on network options 2025-03-16 23:15:01 +07:00
bf09b746c7 Handle reconnection when network option is cellular 2025-03-16 22:47:21 +07:00
b5c67cb0b1 Better network mode representation
Handle wifi network reconnection
if measurementCycleQueue empty, skip transmission
Move agclient implementation on initializeNetwork function
2025-03-16 16:13:14 +07:00
5f40a327b3 Run networking related on seperate task
A couple of todos still needs to address
2025-03-16 02:22:38 +07:00
66b0c63de5 New function for measurement cycle
getMeasurementCycle to capture current measurement that will be added to queue
buildMeasurementPayload using measurementCycle to build json string as transmission payload
2025-03-16 02:19:22 +07:00
cc3228f49a Fix: submodule changes 2025-03-14 11:48:12 +07:00
8728589ca1 Make sure CE load switch disable on boot 2025-03-14 11:04:32 +07:00
4b356920c2 First working integration using airgradientClient 2025-03-14 01:41:23 +07:00
c94b886360 Add airgradient-client as submodule 2025-03-14 01:18:36 +07:00
e056e44917 Add how to compile document 2025-02-28 16:10:56 +07:00
3b00fa69b8 Remove forgottern to delete text 2025-02-28 16:01:37 +07:00
e0720ac580 Add notes about arduino-esp32 version compatibility
Adding how to contribute section
2025-02-28 15:59:46 +07:00
0861c2dcaa Missing install library step for diy model 2025-02-26 14:43:14 +07:00
59fc0c409b Update to more comprehensive steps 2025-02-26 14:37:31 +07:00
6d63fdf643 Remove step to plug the monitor 2025-02-24 21:42:14 +07:00
033358e2c2 Update example sketch headers 2025-02-24 14:59:12 +07:00
47034f62b4 Add howto compile docs 2025-02-24 14:53:47 +07:00
71a21ce7e6 Merge branch 'master' into develop 2025-02-24 14:15:13 +07:00
3f5e5eebbb Merge pull request #279 from MallocArray/patch-2
Update workflow to use compile-sketches action
2025-02-16 04:32:28 +07:00
f9be400a5d Address PR comments on versions 2025-02-15 15:28:17 -06:00
54808ac076 Merge remote-tracking branch 'origin/develop' 2025-02-10 01:37:41 +07:00
063bb2a227 Prepare release 3.2.0 2025-02-10 01:36:41 +07:00
a50e1e2472 Update workflow to use compile-sketches action
Instead of straight CLI, uses the arduino/compile-sketches action.

One immediate benefit is enabling deltas reports
https://github.com/arduino/compile-sketches/tree/v1.1.2/?tab=readme-ov-file#enable-deltas-report

This can show the change in flash and ram compared to the previous run
https://github.com/MallocArray/arduino/actions/runs/12954136400/job/36135424360
```
Compiling sketch: examples/OneOpenAir
Compilation time elapsed: 6s
Change in flash: 147290 (7.49%)
Change in RAM for global variables: 2280 (0.7%)
```

A future improvement could be to also add https://github.com/arduino/report-size-deltas that can generate a comment on a PR that also shows the delta information.
2025-01-25 13:39:25 -06:00
36 changed files with 1150 additions and 519 deletions

View File

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

5
.gitignore vendored
View File

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

6
.gitmodules vendored Normal file
View File

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

105
docs/howto-compile.md Normal file
View File

@ -0,0 +1,105 @@
# 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.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
docs/images/ag-lib.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

BIN
docs/images/compiled.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

BIN
docs/images/esp32-board.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

BIN
docs/images/settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

View File

@ -93,6 +93,7 @@ Compensated values apply correction algorithms to make the sensor values more ac
"tvocLearningOffset": 12, "tvocLearningOffset": 12,
"noxLearningOffset": 12, "noxLearningOffset": 12,
"mqttBrokerUrl": "", "mqttBrokerUrl": "",
"httpDomain": "",
"temperatureUnit": "c", "temperatureUnit": "c",
"configurationControl": "local", "configurationControl": "local",
"postDataToAirGradient": true, "postDataToAirGradient": true,
@ -146,7 +147,8 @@ 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}` | | `displayBrightness` | Brightness of the Display. | Number | 0-100 | `{"displayBrightness": 50}` |
| `ledBarBrightness` | Brightness of the LEDBar. | Number | 0-100 | `{"ledBarBrightness": 40}` | | `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}` | | `abcDays` | Number of days for CO2 automatic baseline calibration. | Number | Maximum 200 days. Default 8 days. | `{"abcDays": 8}` |
| `mqttBrokerUrl` | MQTT broker URL. | String | | `{"mqttBrokerUrl": "mqtt://192.168.0.18:1883"}` | | `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"}` |
| `temperatureUnit` | Temperature unit shown on the display. | String | `c` or `C`: Degree Celsius °C <br>`f` or `F`: Degree Fahrenheit °F | `{"temperatureUnit": "c"}` | | `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"}` | | `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}` | | `postDataToAirGradient` | Send data to AirGradient cloud. | Boolean | `true`: Enabled <br>`false`: Disabled | `{"postDataToAirGradient": true}` |
@ -154,8 +156,8 @@ 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}` | | `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}` | | `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}` | | `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 }` | | `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_ | | `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** **Notes**

View File

@ -12,10 +12,8 @@ Outdoor Monitor: https://www.airgradient.com/outdoor/
Build Instructions: Build Instructions:
https://www.airgradient.com/documentation/diy-v4/ https://www.airgradient.com/documentation/diy-v4/
Please make sure you have esp8266 board manager installed. Tested with Compile Instructions:
version 3.1.2. https://github.com/airgradienthq/arduino/blob/master/docs/howto-compile.md
Set board to "LOLIN(WEMOS) D1 R2 & mini"
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3) Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
can be set through the AirGradient dashboard. can be set through the AirGradient dashboard.

View File

@ -12,10 +12,8 @@ Outdoor Monitor: https://www.airgradient.com/outdoor/
Build Instructions: Build Instructions:
https://www.airgradient.com/documentation/diy-v4/ https://www.airgradient.com/documentation/diy-v4/
Please make sure you have esp8266 board manager installed. Tested with Compile Instructions:
version 3.1.2. https://github.com/airgradienthq/arduino/blob/master/docs/howto-compile.md
Set board to "LOLIN(WEMOS) D1 R2 & mini"
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3) Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
can be set through the AirGradient dashboard. can be set through the AirGradient dashboard.

View File

@ -12,10 +12,8 @@ Outdoor Monitor: https://www.airgradient.com/outdoor/
Build Instructions: Build Instructions:
https://www.airgradient.com/documentation/diy-v4/ https://www.airgradient.com/documentation/diy-v4/
Please make sure you have esp8266 board manager installed. Tested with Compile Instructions:
version 3.1.2. https://github.com/airgradienthq/arduino/blob/master/docs/howto-compile.md
Set board to "LOLIN(WEMOS) D1 R2 & mini"
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3) Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
can be set through the AirGradient dashboard. can be set through the AirGradient dashboard.

View File

@ -14,17 +14,8 @@ https://www.airgradient.com/documentation/one-v9/ Build Instructions:
AirGradient Open Air: AirGradient Open Air:
https://www.airgradient.com/documentation/open-air-pst-kit-1-3/ https://www.airgradient.com/documentation/open-air-pst-kit-1-3/
Please make sure you have esp32 board manager installed. Tested with Compile Instructions:
version 2.0.11. https://github.com/airgradienthq/arduino/blob/master/docs/howto-compile.md
Important flashing settings:
- Set board to "ESP32C3 Dev Module"
- Enable "USB CDC On Boot"
- Flash frequency "80Mhz"
- Flash mode "QIO"
- Flash size "4MB"
- Partition scheme "Minimal SPIFFS (1.9MB APP with OTA/190KB SPIFFS)"
- JTAG adapter "Disabled"
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3) Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
can be set through the AirGradient dashboard. can be set through the AirGradient dashboard.
@ -35,29 +26,47 @@ https://forum.airgradient.com/
CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
*/ */
#include "AgApiClient.h"
#include "AgConfigure.h" #include "AgConfigure.h"
#include "AgSchedule.h" #include "AgSchedule.h"
#include "AgStateMachine.h" #include "AgStateMachine.h"
#include "AgValue.h"
#include "AgWiFiConnector.h" #include "AgWiFiConnector.h"
#include "AirGradient.h" #include "AirGradient.h"
#include "App/AppDef.h"
#include "Arduino.h"
#include "EEPROM.h" #include "EEPROM.h"
#include "ESPmDNS.h" #include "ESPmDNS.h"
#include "Libraries/airgradient-client/src/common.h"
#include "LocalServer.h" #include "LocalServer.h"
#include "MqttClient.h" #include "MqttClient.h"
#include "OpenMetrics.h" #include "OpenMetrics.h"
#include "OtaHandler.h"
#include "WebServer.h" #include "WebServer.h"
#include "esp32c3/rom/rtc.h" #include "esp32c3/rom/rtc.h"
#include <HardwareSerial.h> #include <HardwareSerial.h>
#include <WebServer.h> #include <WebServer.h>
#include <WiFi.h> #include <WiFi.h>
#include <cstdint>
#include <string>
#include "Libraries/airgradient-client/src/agSerial.h"
#include "Libraries/airgradient-client/src/cellularModule.h"
#include "Libraries/airgradient-client/src/cellularModuleA7672xx.h"
#include "Libraries/airgradient-client/src/airgradientCellularClient.h"
#include "Libraries/airgradient-client/src/airgradientWifiClient.h"
#include "Libraries/airgradient-ota/src/airgradientOta.h"
#include "Libraries/airgradient-ota/src/airgradientOtaWifi.h"
#include "Libraries/airgradient-ota/src/airgradientOtaCellular.h"
#include "esp_system.h"
#include "freertos/projdefs.h"
#define LED_BAR_ANIMATION_PERIOD 100 /** ms */ #define LED_BAR_ANIMATION_PERIOD 100 /** ms */
#define DISP_UPDATE_INTERVAL 2500 /** ms */ #define DISP_UPDATE_INTERVAL 2500 /** ms */
#define SERVER_CONFIG_SYNC_INTERVAL 60000 /** ms */ #define WIFI_SERVER_CONFIG_SYNC_INTERVAL 1 * 60000 /** ms */
#define SERVER_SYNC_INTERVAL 60000 /** ms */ #define WIFI_MEASUREMENT_INTERVAL 1 * 60000 /** ms */
#define WIFI_TRANSMISSION_INTERVAL 1 * 60000 /** ms */
#define CELLULAR_SERVER_CONFIG_SYNC_INTERVAL 30 * 60000 /** ms */
#define CELLULAR_MEASUREMENT_INTERVAL 3 * 60000 /** ms */
#define CELLULAR_TRANSMISSION_INTERVAL 3 * 60000 /** ms */
#define MQTT_SYNC_INTERVAL 60000 /** ms */ #define MQTT_SYNC_INTERVAL 60000 /** ms */
#define SENSOR_CO2_CALIB_COUNTDOWN_MAX 5 /** sec */ #define SENSOR_CO2_CALIB_COUNTDOWN_MAX 5 /** sec */
#define SENSOR_TVOC_UPDATE_INTERVAL 1000 /** ms */ #define SENSOR_TVOC_UPDATE_INTERVAL 1000 /** ms */
@ -66,16 +75,28 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
#define SENSOR_TEMP_HUM_UPDATE_INTERVAL 6000 /** ms */ #define SENSOR_TEMP_HUM_UPDATE_INTERVAL 6000 /** ms */
#define DISPLAY_DELAY_SHOW_CONTENT_MS 2000 /** ms */ #define DISPLAY_DELAY_SHOW_CONTENT_MS 2000 /** ms */
#define FIRMWARE_CHECK_FOR_UPDATE_MS (60 * 60 * 1000) /** ms */ #define FIRMWARE_CHECK_FOR_UPDATE_MS (60 * 60 * 1000) /** ms */
#define TIME_TO_START_POWER_CYCLE_CELLULAR_MODULE (1 * 60) /** minutes */
#define TIMEOUT_WAIT_FOR_CELLULAR_MODULE_READY (2 * 60) /** minutes */
#define MEASUREMENT_TRANSMIT_CYCLE 3
#define MAXIMUM_MEASUREMENT_CYCLE_QUEUE 80
#define RESERVED_MEASUREMENT_CYCLE_CAPACITY 10
/** I2C define */ /** I2C define */
#define I2C_SDA_PIN 7 #define I2C_SDA_PIN 7
#define I2C_SCL_PIN 6 #define I2C_SCL_PIN 6
#define OLED_I2C_ADDR 0x3C #define OLED_I2C_ADDR 0x3C
/** Power pin */
#define GPIO_POWER_MODULE_PIN 5
#define GPIO_EXPANSION_CARD_POWER 4
#define GPIO_IIC_RESET 3
#define MINUTES() ((uint32_t)(esp_timer_get_time() / 1000 / 1000 / 60))
static MqttClient mqttClient(Serial); static MqttClient mqttClient(Serial);
static TaskHandle_t mqttTask = NULL; static TaskHandle_t mqttTask = NULL;
static Configuration configuration(Serial); static Configuration configuration(Serial);
static AgApiClient apiClient(Serial, configuration);
static Measurements measurements(configuration); static Measurements measurements(configuration);
static AirGradient *ag; static AirGradient *ag;
static OledDisplay oledDisplay(configuration, measurements, Serial); static OledDisplay oledDisplay(configuration, measurements, Serial);
@ -83,22 +104,39 @@ static StateMachine stateMachine(oledDisplay, Serial, measurements,
configuration); configuration);
static WifiConnector wifiConnector(oledDisplay, Serial, stateMachine, static WifiConnector wifiConnector(oledDisplay, Serial, stateMachine,
configuration); configuration);
static OpenMetrics openMetrics(measurements, configuration, wifiConnector, static OpenMetrics openMetrics(measurements, configuration, wifiConnector);
apiClient);
static OtaHandler otaHandler;
static LocalServer localServer(Serial, openMetrics, measurements, configuration, static LocalServer localServer(Serial, openMetrics, measurements, configuration,
wifiConnector); wifiConnector);
static AgSerial *agSerial;
static CellularModule *cellularCard;
static AirgradientClient *agClient;
enum NetworkOption {
UseWifi,
UseCellular
};
NetworkOption networkOption;
TaskHandle_t handleNetworkTask = NULL;
static bool firmwareUpdateInProgress = false;
static uint32_t factoryBtnPressTime = 0; static uint32_t factoryBtnPressTime = 0;
static AgFirmwareMode fwMode = FW_MODE_I_9PSL; static AgFirmwareMode fwMode = FW_MODE_I_9PSL;
static bool ledBarButtonTest = false; static bool ledBarButtonTest = false;
static String fwNewVersion; static String fwNewVersion;
static int lastCellSignalQuality = 99; // CSQ
// Default value is 0, indicate its not started yet
// In minutes
uint32_t agCeClientProblemDetectedTime = 0;
SemaphoreHandle_t mutexMeasurementCycleQueue;
static std::vector<Measurements::Measures> measurementCycleQueue;
static void boardInit(void); static void boardInit(void);
static void initializeNetwork(void); static void initializeNetwork();
static void failedHandler(String msg); static void failedHandler(String msg);
static void configurationUpdateSchedule(void); static void configurationUpdateSchedule(void);
static void configUpdateHandle(void);
static void updateDisplayAndLedBar(void); static void updateDisplayAndLedBar(void);
static void updateTvoc(void); static void updateTvoc(void);
static void updatePm(void); static void updatePm(void);
@ -113,27 +151,37 @@ static void wdgFeedUpdate(void);
static void ledBarEnabledUpdate(void); static void ledBarEnabledUpdate(void);
static bool sgp41Init(void); static bool sgp41Init(void);
static void checkForFirmwareUpdate(void); static void checkForFirmwareUpdate(void);
static void otaHandlerCallback(OtaHandler::OtaState state, String mesasge); static void otaHandlerCallback(AirgradientOTA::OtaResult result, const char *msg);
static void displayExecuteOta(OtaHandler::OtaState state, String msg, int processing); static void displayExecuteOta(AirgradientOTA::OtaResult result, String msg, int processing);
static int calculateMaxPeriod(int updateInterval); static int calculateMaxPeriod(int updateInterval);
static void setMeasurementMaxPeriod(); static void setMeasurementMaxPeriod();
static void newMeasurementCycle();
static void restartIfCeClientIssueOverTwoHours();
static void networkSignalCheck();
static void networkingTask(void *args);
AgSchedule dispLedSchedule(DISP_UPDATE_INTERVAL, updateDisplayAndLedBar); AgSchedule dispLedSchedule(DISP_UPDATE_INTERVAL, updateDisplayAndLedBar);
AgSchedule configSchedule(SERVER_CONFIG_SYNC_INTERVAL, AgSchedule configSchedule(WIFI_SERVER_CONFIG_SYNC_INTERVAL,
configurationUpdateSchedule); configurationUpdateSchedule);
AgSchedule agApiPostSchedule(SERVER_SYNC_INTERVAL, sendDataToServer); AgSchedule transmissionSchedule(WIFI_TRANSMISSION_INTERVAL, sendDataToServer);
AgSchedule measurementSchedule(WIFI_MEASUREMENT_INTERVAL, newMeasurementCycle);
AgSchedule co2Schedule(SENSOR_CO2_UPDATE_INTERVAL, co2Update); AgSchedule co2Schedule(SENSOR_CO2_UPDATE_INTERVAL, co2Update);
AgSchedule pmsSchedule(SENSOR_PM_UPDATE_INTERVAL, updatePm); AgSchedule pmsSchedule(SENSOR_PM_UPDATE_INTERVAL, updatePm);
AgSchedule tempHumSchedule(SENSOR_TEMP_HUM_UPDATE_INTERVAL, tempHumUpdate); AgSchedule tempHumSchedule(SENSOR_TEMP_HUM_UPDATE_INTERVAL, tempHumUpdate);
AgSchedule tvocSchedule(SENSOR_TVOC_UPDATE_INTERVAL, updateTvoc); AgSchedule tvocSchedule(SENSOR_TVOC_UPDATE_INTERVAL, updateTvoc);
AgSchedule watchdogFeedSchedule(60000, wdgFeedUpdate); AgSchedule watchdogFeedSchedule(60000, wdgFeedUpdate);
AgSchedule checkForUpdateSchedule(FIRMWARE_CHECK_FOR_UPDATE_MS, checkForFirmwareUpdate); AgSchedule checkForUpdateSchedule(FIRMWARE_CHECK_FOR_UPDATE_MS, checkForFirmwareUpdate);
AgSchedule networkSignalCheckSchedule(10000, networkSignalCheck);
void setup() { void setup() {
/** Serial for print debug message */ /** Serial for print debug message */
Serial.begin(115200); Serial.begin(115200);
delay(100); /** For bester show log */ delay(100); /** For bester show log */
// Enable cullular module power board
pinMode(GPIO_EXPANSION_CARD_POWER, OUTPUT);
digitalWrite(GPIO_EXPANSION_CARD_POWER, HIGH);
/** Print device ID into log */ /** Print device ID into log */
Serial.println("Serial nr: " + ag->deviceId()); Serial.println("Serial nr: " + ag->deviceId());
@ -162,14 +210,10 @@ void setup() {
oledDisplay.setAirGradient(ag); oledDisplay.setAirGradient(ag);
stateMachine.setAirGradient(ag); stateMachine.setAirGradient(ag);
wifiConnector.setAirGradient(ag); wifiConnector.setAirGradient(ag);
apiClient.setAirGradient(ag);
openMetrics.setAirGradient(ag); openMetrics.setAirGradient(ag);
localServer.setAirGraident(ag); localServer.setAirGraident(ag);
measurements.setAirGradient(ag); measurements.setAirGradient(ag);
/** Example set custom API root URL */
// apiClient.setApiRoot("https://example.custom.api");
/** Init sensor */ /** Init sensor */
boardInit(); boardInit();
setMeasurementMaxPeriod(); setMeasurementMaxPeriod();
@ -177,9 +221,8 @@ void setup() {
// Comment below line to disable debug measurement readings // Comment below line to disable debug measurement readings
measurements.setDebug(true); measurements.setDebug(true);
/** Connecting wifi */ bool connectToNetwork = true;
bool connectToWifi = false; if (ag->isOne()) { // Offline mode only available for indoor monitor
if (ag->isOne()) {
/** Show message confirm offline mode, should me perform if LED bar button /** Show message confirm offline mode, should me perform if LED bar button
* test pressed */ * test pressed */
if (ledBarButtonTest == false) { if (ledBarButtonTest == false) {
@ -202,21 +245,21 @@ void setup() {
break; break;
} }
} }
connectToWifi = !configuration.isOfflineMode(); connectToNetwork = !configuration.isOfflineMode();
} else { } else {
configuration.setOfflineModeWithoutSave(true); configuration.setOfflineModeWithoutSave(true);
connectToNetwork = false;
} }
} else {
connectToWifi = true;
} }
// Initialize networking configuration // Initialize networking configuration
if (connectToWifi) { if (connectToNetwork) {
oledDisplay.setText("Initialize", "network...", "");
initializeNetwork(); initializeNetwork();
} }
/** Set offline mode without saving, cause wifi is not configured */ /** Set offline mode without saving, cause wifi is not configured */
if (wifiConnector.hasConfigurated() == false) { if (wifiConnector.hasConfigurated() == false && networkOption == UseWifi) {
Serial.println("Set offline mode cause wifi is not configurated"); Serial.println("Set offline mode cause wifi is not configurated");
configuration.setOfflineModeWithoutSave(true); configuration.setOfflineModeWithoutSave(true);
} }
@ -230,17 +273,65 @@ void setup() {
oledDisplay.setBrightness(configuration.getDisplayBrightness()); oledDisplay.setBrightness(configuration.getDisplayBrightness());
} }
// Reset post schedulers to make sure measurements value already available
agApiPostSchedule.update(); if (networkOption == UseCellular) {
// If using cellular re-set scheduler interval
configSchedule.setPeriod(CELLULAR_SERVER_CONFIG_SYNC_INTERVAL);
transmissionSchedule.setPeriod(CELLULAR_TRANSMISSION_INTERVAL);
measurementSchedule.setPeriod(CELLULAR_MEASUREMENT_INTERVAL);
measurementSchedule.update();
// Queue now only applied for cellular
// Allocate queue memory to avoid always reallocation
measurementCycleQueue.reserve(RESERVED_MEASUREMENT_CYCLE_CAPACITY);
// Initialize mutex to access mesurementCycleQueue
mutexMeasurementCycleQueue = xSemaphoreCreateMutex();
}
// Only run network task if monitor is not in offline mode
if (configuration.isOfflineMode() == false) {
BaseType_t xReturned =
xTaskCreate(networkingTask, "NetworkingTask", 4096, null, 5, &handleNetworkTask);
if (xReturned == pdPASS) {
Serial.println("Success create networking task");
} else {
assert("Failed to create networking task");
}
}
// Log monitor mode for debugging purpose
if (configuration.isOfflineMode()) {
Serial.println("Running monitor in offline mode");
}
else if (configuration.isCloudConnectionDisabled()) {
Serial.println("Running monitor without connection to AirGradient server");
}
} }
void loop() { void loop() {
/** Run schedulers */ if (networkOption == UseCellular) {
dispLedSchedule.run(); // Check if cellular client not ready until certain time
configSchedule.run(); // Redundant check in both task to make sure its executed
agApiPostSchedule.run(); restartIfCeClientIssueOverTwoHours();
}
// Schedule to feed external watchdog
watchdogFeedSchedule.run(); watchdogFeedSchedule.run();
if (firmwareUpdateInProgress) {
// Firmare update currently in progress, temporarily disable running sensor schedules
delay(10000);
return;
}
// Schedule to update display and led
dispLedSchedule.run();
if (networkOption == UseCellular) {
// Queue now only applied for cellular
measurementSchedule.run();
}
if (configuration.hasSensorS8) { if (configuration.hasSensorS8) {
co2Schedule.run(); co2Schedule.run();
} }
@ -261,7 +352,7 @@ void loop() {
static bool pmsConnected = false; static bool pmsConnected = false;
if (pmsConnected != ag->pms5003.connected()) { if (pmsConnected != ag->pms5003.connected()) {
pmsConnected = ag->pms5003.connected(); pmsConnected = ag->pms5003.connected();
Serial.printf("PMS sensor %s ", pmsConnected?"connected":"removed"); Serial.printf("PMS sensor %s \n", pmsConnected?"connected":"removed");
} }
} }
} else { } else {
@ -273,17 +364,11 @@ void loop() {
} }
} }
/** Check for handle WiFi reconnect */
wifiConnector.handle();
/** factory reset handle */ /** factory reset handle */
factoryConfigReset(); factoryConfigReset();
/** check that local configuration changed then do some action */ /** check that local configuration changed then do some action */
configUpdateHandle(); configUpdateHandle();
/** Firmware check for update handle */
checkForUpdateSchedule.run();
} }
static void co2Update(void) { static void co2Update(void) {
@ -356,6 +441,11 @@ static void initMqtt(void) {
return; return;
} }
if (networkOption == UseCellular) {
Serial.println("MQTT not available for cellular options");
return;
}
if (mqttClient.begin(mqttUri)) { if (mqttClient.begin(mqttUri)) {
Serial.println("Successfully connected to MQTT broker"); Serial.println("Successfully connected to MQTT broker");
createMqttTask(); createMqttTask();
@ -464,47 +554,75 @@ static bool sgp41Init(void) {
return false; return false;
} }
static void checkForFirmwareUpdate(void) { void checkForFirmwareUpdate(void) {
Serial.println(); AirgradientOTA *agOta;
Serial.print("checkForFirmwareUpdate: "); if (networkOption == UseWifi) {
agOta = new AirgradientOTAWifi;
if (configuration.isOfflineMode() || configuration.isCloudConnectionDisabled()) { } else {
Serial.println("mode is offline or cloud connection disabled, ignored"); agOta = new AirgradientOTACellular(cellularCard);
return;
} }
if (!wifiConnector.isConnected()) { // Indicate main task that firmware update is in progress
Serial.println("wifi not connected, ignored"); firmwareUpdateInProgress = true;
return;
agOta->setHandlerCallback(otaHandlerCallback);
String httpDomain = configuration.getHttpDomain();
if (httpDomain != "") {
Serial.printf("httpDomain configuration available, start OTA with custom domain\n",
httpDomain.c_str());
agOta->updateIfAvailable(ag->deviceId().c_str(), GIT_VERSION, httpDomain.c_str());
} else {
agOta->updateIfAvailable(ag->deviceId().c_str(), GIT_VERSION);
} }
Serial.println("perform"); // Only goes to this line if firmware update is not success
otaHandler.setHandlerCallback(otaHandlerCallback); // Handled by otaHandlerCallback
otaHandler.updateFirmwareIfOutdated(ag->deviceId());
// Indicate main task that firmware update finish
firmwareUpdateInProgress = false;
delete agOta;
Serial.println(); Serial.println();
} }
static void otaHandlerCallback(OtaHandler::OtaState state, String message) { void otaHandlerCallback(AirgradientOTA::OtaResult result, const char *msg) {
Serial.println("OTA message: " + message); switch (result) {
switch (state) { case AirgradientOTA::Starting: {
case OtaHandler::OTA_STATE_BEGIN: Serial.println("Firmware update starting...");
displayExecuteOta(state, fwNewVersion, 0); if (configuration.hasSensorSGP && networkOption == UseCellular) {
// Temporary pause SGP41 task while cellular firmware update is in progress
ag->sgp41.pause();
}
displayExecuteOta(result, fwNewVersion, 0);
break; break;
case OtaHandler::OTA_STATE_FAIL: }
displayExecuteOta(state, "", 0); case AirgradientOTA::InProgress:
Serial.printf("OTA progress: %s\n", msg);
displayExecuteOta(result, "", std::stoi(msg));
break; break;
case OtaHandler::OTA_STATE_PROCESSING: case AirgradientOTA::Failed:
case OtaHandler::OTA_STATE_SUCCESS: displayExecuteOta(result, "", 0);
displayExecuteOta(state, "", message.toInt()); if (configuration.hasSensorSGP && networkOption == UseCellular) {
ag->sgp41.resume();
}
break;
case AirgradientOTA::Skipped:
case AirgradientOTA::AlreadyUpToDate:
displayExecuteOta(result, "", 0);
break;
case AirgradientOTA::Success:
displayExecuteOta(result, "", 0);
esp_restart();
break; break;
default: default:
break; break;
} }
} }
static void displayExecuteOta(OtaHandler::OtaState state, String msg, int processing) { static void displayExecuteOta(AirgradientOTA::OtaResult result, String msg, int processing) {
switch (state) { switch (result) {
case OtaHandler::OTA_STATE_BEGIN: { case AirgradientOTA::Starting:
if (ag->isOne()) { if (ag->isOne()) {
oledDisplay.showFirmwareUpdateVersion(msg); oledDisplay.showFirmwareUpdateVersion(msg);
} else { } else {
@ -512,52 +630,40 @@ static void displayExecuteOta(OtaHandler::OtaState state, String msg, int proces
} }
delay(2500); delay(2500);
break; break;
} case AirgradientOTA::Failed:
case OtaHandler::OTA_STATE_FAIL: {
if (ag->isOne()) { if (ag->isOne()) {
oledDisplay.showFirmwareUpdateFailed(); oledDisplay.showFirmwareUpdateFailed();
} else { } else {
Serial.println("Error: Firmware update: failed"); Serial.println("Error: Firmware update: failed");
} }
delay(2500); delay(2500);
break; break;
} case AirgradientOTA::Skipped:
case OtaHandler::OTA_STATE_SKIP: {
if (ag->isOne()) { if (ag->isOne()) {
oledDisplay.showFirmwareUpdateSkipped(); oledDisplay.showFirmwareUpdateSkipped();
} else { } else {
Serial.println("Firmware update: Skipped"); Serial.println("Firmware update: Skipped");
} }
delay(2500); delay(2500);
break; break;
} case AirgradientOTA::AlreadyUpToDate:
case OtaHandler::OTA_STATE_UP_TO_DATE: {
if (ag->isOne()) { if (ag->isOne()) {
oledDisplay.showFirmwareUpdateUpToDate(); oledDisplay.showFirmwareUpdateUpToDate();
} else { } else {
Serial.println("Firmware update: up to date"); Serial.println("Firmware update: up to date");
} }
delay(2500); delay(2500);
break; break;
} case AirgradientOTA::InProgress:
case OtaHandler::OTA_STATE_PROCESSING: {
if (ag->isOne()) { if (ag->isOne()) {
oledDisplay.showFirmwareUpdateProgress(processing); oledDisplay.showFirmwareUpdateProgress(processing);
} else { } else {
Serial.println("Firmware update: " + String(processing) + String("%")); Serial.println("Firmware update: " + String(processing) + String("%"));
} }
break; break;
} case AirgradientOTA::Success: {
case OtaHandler::OTA_STATE_SUCCESS: {
int i = 6;
while(i != 0) {
i = i - 1;
Serial.println("OTA update performed, restarting ..."); Serial.println("OTA update performed, restarting ...");
int i = 6; int i = 3;
while (i != 0) { while (i != 0) {
i = i - 1; i = i - 1;
if (ag->isOne()) { if (ag->isOne()) {
@ -565,11 +671,12 @@ static void displayExecuteOta(OtaHandler::OtaState state, String msg, int proces
} else { } else {
Serial.println("Rebooting... " + String(i)); Serial.println("Rebooting... " + String(i));
} }
delay(1000); delay(1000);
} }
if (ag->isOne()) {
oledDisplay.setAirGradient(0);
oledDisplay.setBrightness(0); oledDisplay.setBrightness(0);
esp_restart();
} }
break; break;
} }
@ -602,7 +709,13 @@ static void sendDataToAg() {
"task_led", 2048, NULL, 5, NULL); "task_led", 2048, NULL, 5, NULL);
delay(1500); delay(1500);
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount())) {
// Build payload to check connection to airgradient server
JSONVar root;
root["wifi"] = wifiConnector.RSSI();
root["boot"] = measurements.bootCount();
std::string payload = JSON.stringify(root).c_str();
if (agClient->httpPostMeasures(payload)) {
if (ag->isOne()) { if (ag->isOne()) {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected); stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected);
} }
@ -730,8 +843,6 @@ static void openAirInit(void) {
Serial.println("CO2 S8 sensor not found"); Serial.println("CO2 S8 sensor not found");
Serial.println("Can not detect S8 run mode 'PPT'"); Serial.println("Can not detect S8 run mode 'PPT'");
fwMode = FW_MODE_O_1PPT; fwMode = FW_MODE_O_1PPT;
Serial0.end();
delay(200); delay(200);
} else { } else {
Serial.println("Found S8 on Serial0"); Serial.println("Found S8 on Serial0");
@ -838,6 +949,55 @@ static void failedHandler(String msg) {
} }
void initializeNetwork() { void initializeNetwork() {
// Check if cellular module available
agSerial = new AgSerial(Wire);
agSerial->init(GPIO_IIC_RESET);
if (agSerial->open()) {
Serial.println("Cellular module found");
// Initialize cellular module and use cellular as agClient
cellularCard = new CellularModuleA7672XX(agSerial, GPIO_POWER_MODULE_PIN);
agClient = new AirgradientCellularClient(cellularCard);
networkOption = UseCellular;
} else {
Serial.println("Cellular module not available, using wifi");
delete agSerial;
agSerial = nullptr;
// Use wifi as agClient
agClient = new AirgradientWifiClient;
networkOption = UseWifi;
}
if (networkOption == UseCellular) {
// Enable serial stream debugging to check the AT command when doing registration
agSerial->setDebug(true);
}
String httpDomain = configuration.getHttpDomain();
if (httpDomain != "") {
agClient->setHttpDomain(httpDomain.c_str());
Serial.printf("HTTP domain name is set to: %s\n", httpDomain.c_str());
oledDisplay.setText("HTTP domain name", "using local", "configuration");
delay(2500);
}
if (!agClient->begin(ag->deviceId().c_str())) {
oledDisplay.setText("Client", "initialization", "failed");
delay(5000);
oledDisplay.showRebooting();
delay(2500);
oledDisplay.setText("", "", "");
ESP.restart();
}
// Provide openmetrics to have access to last transmission result
openMetrics.setAirgradientClient(agClient);
if (networkOption == UseCellular) {
// Disabling it again
agSerial->setDebug(false);
}
if (networkOption == UseWifi) {
if (!wifiConnector.connect()) { if (!wifiConnector.connect()) {
Serial.println("Cannot initiate wifi connection"); Serial.println("Cannot initiate wifi connection");
return; return;
@ -867,25 +1027,18 @@ void initializeNetwork() {
return; return;
} }
// Initialize api client
apiClient.begin();
// Send data for the first time to AG server at boot // Send data for the first time to AG server at boot
sendDataToAg(); sendDataToAg();
}
// OTA check
#ifdef ESP8266
// ota not supported
#else
checkForFirmwareUpdate();
checkForUpdateSchedule.update();
#endif
apiClient.fetchServerConfiguration(); std::string config = agClient->httpFetchConfig();
configSchedule.update(); configSchedule.update();
if (apiClient.isFetchConfigurationFailed()) { // Check if fetch configuration failed or fetch succes but parsing failed
if (agClient->isLastFetchConfigSucceed() == false ||
configuration.parse(config.c_str(), false) == false) {
if (ag->isOne()) { if (ag->isOne()) {
if (apiClient.isNotAvailableOnDashboard()) { if (agClient->isRegisteredOnAgServer() == false) {
stateMachine.displaySetAddToDashBoard(); stateMachine.displaySetAddToDashBoard();
stateMachine.displayHandle(AgStateMachineWiFiOkServerOkSensorConfigFailed); stateMachine.displayHandle(AgStateMachineWiFiOkServerOkSensorConfigFailed);
} else { } else {
@ -894,26 +1047,22 @@ void initializeNetwork() {
} }
stateMachine.handleLeds(AgStateMachineWiFiOkServerOkSensorConfigFailed); stateMachine.handleLeds(AgStateMachineWiFiOkServerOkSensorConfigFailed);
delay(DISPLAY_DELAY_SHOW_CONTENT_MS); delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
} else { }
else {
ledBarEnabledUpdate(); ledBarEnabledUpdate();
} }
} }
static void configurationUpdateSchedule(void) { static void configurationUpdateSchedule(void) {
if (configuration.isOfflineMode() || configuration.isCloudConnectionDisabled() || if (configuration.getConfigurationControl() ==
configuration.getConfigurationControl() == ConfigurationControl::ConfigurationControlLocal) { ConfigurationControl::ConfigurationControlLocal) {
Serial.println("Ignore fetch server configuration. Either mode is offline or cloud connection " Serial.println("Ignore fetch server configuration, configurationControl set to local");
"disabled or configurationControl set to local"); agClient->resetFetchConfigurationStatus();
apiClient.resetFetchConfigurationStatus();
return; return;
} }
if (wifiConnector.isConnected() == false) { std::string config = agClient->httpFetchConfig();
Serial.println(" WiFi not connected, skipping fetch configuration from AG server"); if (agClient->isLastFetchConfigSucceed() && configuration.parse(config.c_str(), false)) {
return;
}
if (apiClient.fetchServerConfiguration()) {
configUpdateHandle(); configUpdateHandle();
} }
} }
@ -931,6 +1080,16 @@ static void configUpdateHandle() {
initMqtt(); initMqtt();
} }
String httpDomain = configuration.getHttpDomain();
if (httpDomain != "") {
Serial.printf("HTTP domain name set to: %s\n", httpDomain.c_str());
agClient->setHttpDomain(httpDomain.c_str());
} else {
// Its empty, set to default
Serial.println("HTTP domain name from configuration empty, set to default");
agClient->setHttpDomainDefault();
}
if (configuration.hasSensorSGP) { if (configuration.hasSensorSGP) {
if (configuration.noxLearnOffsetChanged() || if (configuration.noxLearnOffsetChanged() ||
configuration.tvocLearnOffsetChanged()) { configuration.tvocLearnOffsetChanged()) {
@ -1010,11 +1169,21 @@ static void updateDisplayAndLedBar(void) {
return; return;
} }
if (networkOption == UseWifi) {
if (wifiConnector.isConnected() == false) { if (wifiConnector.isConnected() == false) {
stateMachine.displayHandle(AgStateMachineWiFiLost); stateMachine.displayHandle(AgStateMachineWiFiLost);
stateMachine.handleLeds(AgStateMachineWiFiLost); stateMachine.handleLeds(AgStateMachineWiFiLost);
return; return;
} }
}
else if (networkOption == UseCellular) {
if (agClient->isClientReady() == false) {
// Same action as wifi
stateMachine.displayHandle(AgStateMachineWiFiLost);
stateMachine.handleLeds(AgStateMachineWiFiLost);
return;
}
}
if (configuration.isCloudConnectionDisabled()) { if (configuration.isCloudConnectionDisabled()) {
// Ignore API related check since cloud is disabled // Ignore API related check since cloud is disabled
@ -1024,14 +1193,15 @@ static void updateDisplayAndLedBar(void) {
} }
AgStateMachineState state = AgStateMachineNormal; AgStateMachineState state = AgStateMachineNormal;
if (apiClient.isFetchConfigurationFailed()) { if (agClient->isLastFetchConfigSucceed() == false) {
state = AgStateMachineSensorConfigFailed; state = AgStateMachineSensorConfigFailed;
if (apiClient.isNotAvailableOnDashboard()) { if (agClient->isRegisteredOnAgServer() == false) {
stateMachine.displaySetAddToDashBoard(); stateMachine.displaySetAddToDashBoard();
} else { } else {
stateMachine.displayClearAddToDashBoard(); stateMachine.displayClearAddToDashBoard();
} }
} else if (apiClient.isPostToServerFailed() && configuration.isPostDataToAirGradient()) { } else if (agClient->isLastPostMeasureSucceed() == false &&
configuration.isPostDataToAirGradient()) {
state = AgStateMachineServerLost; state = AgStateMachineServerLost;
} }
@ -1187,34 +1357,90 @@ static void updatePm(void) {
} }
} }
static void sendDataToServer(void) { void postUsingWifi() {
/** Increment bootcount when send measurements data is scheduled */ // Increment bootcount when send measurements data is scheduled
int bootCount = measurements.bootCount() + 1; int bootCount = measurements.bootCount() + 1;
measurements.setBootCount(bootCount); measurements.setBootCount(bootCount);
if (configuration.isOfflineMode() || configuration.isCloudConnectionDisabled() || String payload = measurements.toString(false, fwMode, wifiConnector.RSSI());
!configuration.isPostDataToAirGradient()) { if (agClient->httpPostMeasures(payload.c_str()) == false) {
Serial.println("Skipping transmission of data to AG server. Either mode is offline or cloud connection is "
"disabled or post data to server disabled");
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());
if (apiClient.postToServer(syncData)) {
Serial.println(); Serial.println();
Serial.println("Online mode and isPostToAirGradient = true"); Serial.println("Online mode and isPostToAirGradient = true");
Serial.println(); Serial.println();
} }
/** Log current free heap size */ // Log current free heap size
Serial.printf("Free heap: %u\n", ESP.getFreeHeap()); Serial.printf("Free heap: %u\n", ESP.getFreeHeap());
} }
/**
* forcePost to force post without checking transmit cycle
*/
void postUsingCellular(bool forcePost) {
// Aquire queue mutex to get queue size
xSemaphoreTake(mutexMeasurementCycleQueue, portMAX_DELAY);
// Make sure measurement cycle available
int queueSize = measurementCycleQueue.size();
if (queueSize == 0) {
Serial.println("Skipping transmission, measurementCycle empty");
xSemaphoreGive(mutexMeasurementCycleQueue);
return;
}
// Check queue size if its ready to transmit
// It is ready if size is divisible by 3
if (!forcePost && (queueSize % MEASUREMENT_TRANSMIT_CYCLE) > 0) {
Serial.printf("Not ready to transmit, queue size are %d\n", queueSize);
xSemaphoreGive(mutexMeasurementCycleQueue);
return;
}
// Build payload include all measurements from queue
std::string payload;
payload += std::to_string(CELLULAR_MEASUREMENT_INTERVAL / 1000); // Convert to seconds
for (int i = 0; i < queueSize; i++) {
auto mc = measurementCycleQueue.at(i);
payload += ",";
payload += measurements.buildMeasuresPayload(mc);
}
// Release before actually post measures that might takes too long
xSemaphoreGive(mutexMeasurementCycleQueue);
// Attempt to send
if (agClient->httpPostMeasures(payload) == false) {
// Consider network has a problem, retry in next schedule
Serial.println("Post measures failed, retry in next schedule");
return;
}
// Post success, remove the data that previously sent from queue
xSemaphoreTake(mutexMeasurementCycleQueue, portMAX_DELAY);
measurementCycleQueue.erase(measurementCycleQueue.begin(),
measurementCycleQueue.begin() + queueSize);
if (measurementCycleQueue.capacity() > RESERVED_MEASUREMENT_CYCLE_CAPACITY) {
Serial.println("measurementCycleQueue capacity more than reserved space, resizing..");
measurementCycleQueue.resize(RESERVED_MEASUREMENT_CYCLE_CAPACITY);
}
xSemaphoreGive(mutexMeasurementCycleQueue);
}
void sendDataToServer(void) {
if (configuration.isPostDataToAirGradient() == false) {
Serial.println("Skipping transmission of data to AG server, post data to server disabled");
agClient->resetPostMeasuresStatus();
return;
}
if (networkOption == UseWifi) {
postUsingWifi();
} else if (networkOption == UseCellular) {
postUsingCellular(false);
}
}
static void tempHumUpdate(void) { static void tempHumUpdate(void) {
delay(100); delay(100);
if (ag->sht.measure()) { if (ag->sht.measure()) {
@ -1281,5 +1507,169 @@ void setMeasurementMaxPeriod() {
int calculateMaxPeriod(int updateInterval) { int calculateMaxPeriod(int updateInterval) {
// 0.8 is 80% reduced interval for max period // 0.8 is 80% reduced interval for max period
return (SERVER_SYNC_INTERVAL - (SERVER_SYNC_INTERVAL * 0.8)) / updateInterval; // NOTE: Both network option use the same measurement interval
return (WIFI_MEASUREMENT_INTERVAL - (WIFI_MEASUREMENT_INTERVAL * 0.8)) / updateInterval;
} }
void networkSignalCheck() {
if (networkOption == UseWifi) {
Serial.printf("WiFi RSSI %d\n", wifiConnector.RSSI());
} else if (networkOption == UseCellular) {
auto result = cellularCard->retrieveSignal();
if (result.status != CellReturnStatus::Ok) {
agClient->setClientReady(false);
lastCellSignalQuality = 99;
return;
}
// Save last signal quality
lastCellSignalQuality = result.data;
if (result.data == 99) {
// 99 indicate cellular not attached to network
agClient->setClientReady(false);
return;
}
Serial.printf("Cellular signal quality %d\n", result.data);
}
}
/**
* If in 2 hours cellular client still not ready, then restart system
*/
void restartIfCeClientIssueOverTwoHours() {
if (agCeClientProblemDetectedTime > 0 &&
(MINUTES() - agCeClientProblemDetectedTime) >
TIMEOUT_WAIT_FOR_CELLULAR_MODULE_READY) {
// Give up wait
Serial.println("Rebooting because CE client issues for 2 hours detected");
int i = 3;
while (i != 0) {
if (ag->isOne()) {
String tmp = "Rebooting in " + String(i);
oledDisplay.setText("CE error", "since 2h", tmp.c_str());
} else {
Serial.println("Rebooting... " + String(i));
}
i = i - 1;
delay(1000);
}
oledDisplay.setBrightness(0);
esp_restart();
}
}
void networkingTask(void *args) {
// OTA check on boot
#ifndef ESP8266
checkForFirmwareUpdate();
checkForUpdateSchedule.update();
#endif
// Because cellular interval is longer, needs to send first measures cycle on
// boot to indicate that its online
if (networkOption == UseCellular) {
Serial.println("Prepare first measures cycle to send on boot for 20s");
delay(20000);
networkSignalCheck();
newMeasurementCycle();
postUsingCellular(true);
measurementSchedule.update();
}
// Reset scheduler
configSchedule.update();
transmissionSchedule.update();
while (1) {
// Handle reconnection based on mode
if (networkOption == UseWifi) {
wifiConnector.handle();
if (wifiConnector.isConnected() == false) {
delay(1000);
continue;
}
}
else if (networkOption == UseCellular) {
if (agClient->isClientReady() == false) {
// Start time if value still default
if (agCeClientProblemDetectedTime == 0) {
agCeClientProblemDetectedTime = MINUTES();
}
// Enable at command debug
agSerial->setDebug(true);
// Check if cellular client not ready until certain time
// Redundant check in both task to make sure its executed
restartIfCeClientIssueOverTwoHours();
// Power cycling cellular module due to network issues for more than 1 hour
bool resetModule = true;
if ((MINUTES() - agCeClientProblemDetectedTime) >
TIME_TO_START_POWER_CYCLE_CELLULAR_MODULE) {
Serial.println("The CE client hasn't recovered in more than 1 hour, "
"performing a power cycle");
cellularCard->powerOff();
delay(2000);
cellularCard->powerOn();
delay(10000);
// no need to reset module when calling ensureClientConnection()
resetModule = false;
}
// Attempt to reconnect
Serial.println("Cellular client not ready, ensuring connection...");
if (agClient->ensureClientConnection(resetModule) == false) {
Serial.println("Cellular client connection not ready, retry in 30s...");
delay(30000); // before retry, wait for 30s
continue;
}
// Client is ready
agCeClientProblemDetectedTime = 0; // reset to default
agSerial->setDebug(false); // disable at command debug
}
}
// If connection to AirGradient server disable don't run config and transmission schedule
if (configuration.isCloudConnectionDisabled()) {
delay(1000);
return;
}
// Run scheduler
networkSignalCheckSchedule.run();
configSchedule.run();
transmissionSchedule.run();
checkForUpdateSchedule.run();
delay(1000);
}
vTaskDelete(handleNetworkTask);
}
void newMeasurementCycle() {
if (xSemaphoreTake(mutexMeasurementCycleQueue, portMAX_DELAY) == pdTRUE) {
// Make sure queue not overflow
if (measurementCycleQueue.size() >= MAXIMUM_MEASUREMENT_CYCLE_QUEUE) {
// Remove the oldest data from queue if queue reach max
measurementCycleQueue.erase(measurementCycleQueue.begin());
}
// Get current measures
auto mc = measurements.getMeasures();
mc.signal = cellularCard->csqToDbm(lastCellSignalQuality); // convert to RSSI
measurementCycleQueue.push_back(mc);
Serial.println("New measurement cycle added to queue");
// Release mutex
xSemaphoreGive(mutexMeasurementCycleQueue);
// Log current free heap size
Serial.printf("Free heap: %u\n", ESP.getFreeHeap());
}
}

View File

@ -1,13 +1,18 @@
#include "OpenMetrics.h" #include "OpenMetrics.h"
OpenMetrics::OpenMetrics(Measurements &measure, Configuration &config, OpenMetrics::OpenMetrics(Measurements &measure, Configuration &config,
WifiConnector &wifiConnector, AgApiClient &apiClient) WifiConnector &wifiConnector)
: measure(measure), config(config), wifiConnector(wifiConnector), : measure(measure), config(config), wifiConnector(wifiConnector) {}
apiClient(apiClient) {}
OpenMetrics::~OpenMetrics() {} OpenMetrics::~OpenMetrics() {}
void OpenMetrics::setAirGradient(AirGradient *ag) { this->ag = ag; } void OpenMetrics::setAirGradient(AirGradient *ag) {
this->ag = ag;
}
void OpenMetrics::setAirgradientClient(AirgradientClient *client) {
this->agClient = client;
}
const char *OpenMetrics::getApiContentType(void) { const char *OpenMetrics::getApiContentType(void) {
return "application/openmetrics-text; version=1.0.0; charset=utf-8"; return "application/openmetrics-text; version=1.0.0; charset=utf-8";
@ -43,13 +48,13 @@ String OpenMetrics::getPayload(void) {
"1 if the AirGradient device was able to successfully fetch its " "1 if the AirGradient device was able to successfully fetch its "
"configuration from the server", "configuration from the server",
"gauge"); "gauge");
add_metric_point("", apiClient.isFetchConfigurationFailed() ? "0" : "1"); add_metric_point("", agClient->isLastFetchConfigSucceed() ? "1" : "0");
add_metric( add_metric(
"post_ok", "post_ok",
"1 if the AirGradient device was able to successfully send to the server", "1 if the AirGradient device was able to successfully send to the server",
"gauge"); "gauge");
add_metric_point("", apiClient.isPostToServerFailed() ? "0" : "1"); add_metric_point("", agClient->isLastPostMeasureSucceed() ? "1" : "0");
add_metric( add_metric(
"wifi_rssi", "wifi_rssi",

View File

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

View File

@ -1,5 +1,5 @@
name=AirGradient Air Quality Sensor name=AirGradient Air Quality Sensor
version=3.2.0-alpha version=3.3.6
author=AirGradient <support@airgradient.com> author=AirGradient <support@airgradient.com>
maintainer=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. 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 platform = espressif32
board = esp32-c3-devkitm-1 board = esp32-c3-devkitm-1
framework = arduino framework = arduino
build_flags = !echo '-D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 -D GIT_VERSION=\\"'$(git describe --tags --always --dirty)'\\"' 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)'\\"'
board_build.partitions = partitions.csv board_build.partitions = partitions.csv
monitor_speed = 115200 monitor_speed = 115200
lib_deps = lib_deps =

View File

@ -46,6 +46,7 @@ JSON_PROP_DEF(abcDays);
JSON_PROP_DEF(tvocLearningOffset); JSON_PROP_DEF(tvocLearningOffset);
JSON_PROP_DEF(noxLearningOffset); JSON_PROP_DEF(noxLearningOffset);
JSON_PROP_DEF(mqttBrokerUrl); JSON_PROP_DEF(mqttBrokerUrl);
JSON_PROP_DEF(httpDomain);
JSON_PROP_DEF(temperatureUnit); JSON_PROP_DEF(temperatureUnit);
JSON_PROP_DEF(configurationControl); JSON_PROP_DEF(configurationControl);
JSON_PROP_DEF(postDataToAirGradient); JSON_PROP_DEF(postDataToAirGradient);
@ -68,6 +69,7 @@ JSON_PROP_DEF(rhum);
#define jprop_tvocLearningOffset_default 12 #define jprop_tvocLearningOffset_default 12
#define jprop_noxLearningOffset_default 12 #define jprop_noxLearningOffset_default 12
#define jprop_mqttBrokerUrl_default "" #define jprop_mqttBrokerUrl_default ""
#define jprop_httpDomain_default ""
#define jprop_temperatureUnit_default "c" #define jprop_temperatureUnit_default "c"
#define jprop_configurationControl_default String(CONFIGURATION_CONTROL_NAME[ConfigurationControl::ConfigurationControlBoth]) #define jprop_configurationControl_default String(CONFIGURATION_CONTROL_NAME[ConfigurationControl::ConfigurationControlBoth])
#define jprop_postDataToAirGradient_default true #define jprop_postDataToAirGradient_default true
@ -240,7 +242,7 @@ bool Configuration::updateTempHumCorrection(JSONVar &json, TempHumCorrection &ta
JSONVar corrections = json[jprop_corrections]; JSONVar corrections = json[jprop_corrections];
if (!corrections.hasOwnProperty(correctionName)) { if (!corrections.hasOwnProperty(correctionName)) {
logWarning(String(correctionName) + " correction field not found on configuration"); logInfo(String(correctionName) + " correction field not found on configuration");
return false; return false;
} }
@ -377,6 +379,7 @@ void Configuration::defaultConfig(void) {
jconfig[jprop_country] = jprop_country_default; jconfig[jprop_country] = jprop_country_default;
jconfig[jprop_mqttBrokerUrl] = jprop_mqttBrokerUrl_default; jconfig[jprop_mqttBrokerUrl] = jprop_mqttBrokerUrl_default;
jconfig[jprop_httpDomain] = jprop_httpDomain_default;
jconfig[jprop_configurationControl] = jprop_configurationControl_default; jconfig[jprop_configurationControl] = jprop_configurationControl_default;
jconfig[jprop_pmStandard] = jprop_pmStandard_default; jconfig[jprop_pmStandard] = jprop_pmStandard_default;
jconfig[jprop_temperatureUnit] = jprop_temperatureUnit_default; jconfig[jprop_temperatureUnit] = jprop_temperatureUnit_default;
@ -735,11 +738,17 @@ bool Configuration::parse(String data, bool isLocal) {
jconfig[jprop_mqttBrokerUrl] = broker; jconfig[jprop_mqttBrokerUrl] = broker;
} }
} else { } else {
failedMessage = "\"mqttBrokerUrl\" length should <= 255"; failedMessage = "\"mqttBrokerUrl\" length should less than 255 character";
jsonInvalid(); jsonInvalid();
return false; return false;
} }
} else { }
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 {
if (jsonTypeInvalid(root[jprop_mqttBrokerUrl], "string")) { if (jsonTypeInvalid(root[jprop_mqttBrokerUrl], "string")) {
failedMessage = failedMessage =
jsonTypeInvalidMessage(String(jprop_mqttBrokerUrl), "string"); jsonTypeInvalidMessage(String(jprop_mqttBrokerUrl), "string");
@ -748,6 +757,32 @@ 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") { if (JSON.typeof_(root[jprop_temperatureUnit]) == "string") {
String unit = root[jprop_temperatureUnit]; String unit = root[jprop_temperatureUnit];
String oldUnit = jconfig[jprop_temperatureUnit]; String oldUnit = jconfig[jprop_temperatureUnit];
@ -1030,6 +1065,16 @@ String Configuration::getMqttBrokerUri(void) {
return broker; 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 * @brief Get configuratoin post data to AirGradient cloud
* *
@ -1115,7 +1160,7 @@ bool Configuration::isUpdated(void) {
} }
String Configuration::jsonTypeInvalidMessage(String name, String type) { String Configuration::jsonTypeInvalidMessage(String name, String type) {
return "'" + name + "' type invalid, it's should '" + type + "'"; return "'" + name + "' type is invalid, expecting '" + type + "'";
} }
String Configuration::jsonValueInvalidMessage(String name, String value) { String Configuration::jsonValueInvalidMessage(String name, String value) {
@ -1269,6 +1314,18 @@ void Configuration::toConfig(const char *buf) {
logInfo("toConfig: mqttBroker changed"); 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 */ /** Validate temperature unit */
if (JSON.typeof_(jconfig[jprop_temperatureUnit]) != "string") { if (JSON.typeof_(jconfig[jprop_temperatureUnit]) != "string") {
isConfigFieldInvalid = true; isConfigFieldInvalid = true;

View File

@ -82,6 +82,7 @@ public:
String getLedBarModeName(void); String getLedBarModeName(void);
bool getDisplayMode(void); bool getDisplayMode(void);
String getMqttBrokerUri(void); String getMqttBrokerUri(void);
String getHttpDomain(void);
bool isPostDataToAirGradient(void); bool isPostDataToAirGradient(void);
ConfigurationControl getConfigurationControl(void); ConfigurationControl getConfigurationControl(void);
bool isCo2CalibrationRequested(void); bool isCo2CalibrationRequested(void);

View File

@ -5,12 +5,31 @@
/** Cast U8G2 */ /** Cast U8G2 */
#define DISP() ((U8G2_SH1106_128X64_NONAME_F_HW_I2C *)(this->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 * @brief Show dashboard temperature and humdity
* *
* @param hasStatus * @param hasStatus
*/ */
void OledDisplay::showTempHum(bool hasStatus, char *buf, int buf_size) { void OledDisplay::showTempHum(bool hasStatus) {
char buf[10];
/** Temperature */ /** Temperature */
float temp = value.getCorrectedTempHum(Measurements::Temperature, 1); float temp = value.getCorrectedTempHum(Measurements::Temperature, 1);
if (utils::isValidTemperature(temp)) { if (utils::isValidTemperature(temp)) {
@ -23,22 +42,22 @@ void OledDisplay::showTempHum(bool hasStatus, char *buf, int buf_size) {
if (config.isTemperatureUnitInF()) { if (config.isTemperatureUnitInF()) {
if (hasStatus) { if (hasStatus) {
snprintf(buf, buf_size, "%0.1f", t); snprintf(buf, sizeof(buf), "%0.1f", t);
} else { } else {
snprintf(buf, buf_size, "%0.1f°F", t); snprintf(buf, sizeof(buf), "%0.1f°F", t);
} }
} else { } else {
if (hasStatus) { if (hasStatus) {
snprintf(buf, buf_size, "%.1f", t); snprintf(buf, sizeof(buf), "%.1f", t);
} else { } else {
snprintf(buf, buf_size, "%.1f°C", t); snprintf(buf, sizeof(buf), "%.1f°C", t);
} }
} }
} else { /** Show invalid value */ } else { /** Show invalid value */
if (config.isTemperatureUnitInF()) { if (config.isTemperatureUnitInF()) {
snprintf(buf, buf_size, "-°F"); snprintf(buf, sizeof(buf), "-°F");
} else { } else {
snprintf(buf, buf_size, "-°C"); snprintf(buf, sizeof(buf), "-°C");
} }
} }
DISP()->drawUTF8(1, 10, buf); DISP()->drawUTF8(1, 10, buf);
@ -46,9 +65,9 @@ void OledDisplay::showTempHum(bool hasStatus, char *buf, int buf_size) {
/** Show humidity */ /** Show humidity */
int rhum = round(value.getCorrectedTempHum(Measurements::Humidity, 1)); int rhum = round(value.getCorrectedTempHum(Measurements::Humidity, 1));
if (utils::isValidHumidity(rhum)) { if (utils::isValidHumidity(rhum)) {
snprintf(buf, buf_size, "%d%%", rhum); snprintf(buf, sizeof(buf), "%d%%", rhum);
} else { } else {
snprintf(buf, buf_size, "-%%"); snprintf(buf, sizeof(buf), "-%%");
} }
if (rhum > 99.0) { if (rhum > 99.0) {
@ -67,6 +86,9 @@ void OledDisplay::setCentralText(int y, const char *text) {
DISP()->drawStr(x, y, 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 * @brief Construct a new Ag Oled Display:: Ag Oled Display object
* *
@ -252,36 +274,60 @@ void OledDisplay::setText(const char *line1, const char *line2,
* @brief Update dashboard content * @brief Update dashboard content
* *
*/ */
void OledDisplay::showDashboard(void) { showDashboard(NULL); } void OledDisplay::showDashboard(void) { showDashboard(DashBoardStatusNone); }
/** /**
* @brief Update dashboard content and error status * @brief Update dashboard content and error status
* *
*/ */
void OledDisplay::showDashboard(const char *status) { void OledDisplay::showDashboard(DashboardStatus status) {
if (isDisplayOff) { if (isDisplayOff) {
return; return;
} }
char strBuf[16]; 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()) { if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) {
DISP()->firstPage(); DISP()->firstPage();
do { do {
DISP()->setFont(u8g2_font_t0_16_tf); DISP()->setFont(u8g2_font_t0_16_tf);
if ((status == NULL) || (strlen(status) == 0)) { switch (status) {
showTempHum(false, strBuf, sizeof(strBuf)); case DashBoardStatusNone: {
} else { // Maybe show signal strength?
String strStatus = "Show status: " + String(status); showTempHum(false);
logInfo(strStatus); break;
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));
} }
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;
} }
/** Draw horizonal line */ /** Draw horizonal line */
@ -392,7 +438,8 @@ void OledDisplay::showDashboard(const char *status) {
float temp = value.getCorrectedTempHum(Measurements::Temperature, 1); float temp = value.getCorrectedTempHum(Measurements::Temperature, 1);
if (utils::isValidTemperature(temp)) { if (utils::isValidTemperature(temp)) {
if (config.isTemperatureUnitInF()) { 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 { } else {
snprintf(strBuf, sizeof(strBuf), "T:%0.1f C", temp); snprintf(strBuf, sizeof(strBuf), "T:%0.1f C", temp);
} }
@ -442,8 +489,7 @@ void OledDisplay::setBrightness(int percent) {
// Clear display. // Clear display.
ag->display.clear(); ag->display.clear();
ag->display.show(); ag->display.show();
} } else {
else {
isDisplayOff = false; isDisplayOff = false;
ag->display.setContrast((255 * percent) / 100); ag->display.setContrast((255 * percent) / 100);
} }

View File

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

View File

@ -1,4 +1,5 @@
#include "AgStateMachine.h" #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_FAST_BLINK_DELAY 250 /** ms */
@ -369,8 +370,7 @@ void StateMachine::ledBarTest(void) {
} else { } else {
ledBarRunTest(); ledBarRunTest();
} }
} } else if (ag->isOpenAir()) {
else if(ag->isOpenAir()) {
ledBarRunTest(); ledBarRunTest();
} }
} }
@ -544,11 +544,11 @@ void StateMachine::displayHandle(AgStateMachineState state) {
break; break;
} }
case AgStateMachineWiFiLost: { case AgStateMachineWiFiLost: {
disp.showDashboard("WiFi N/A"); disp.showDashboard(OledDisplay::DashBoardStatusWiFiIssue);
break; break;
} }
case AgStateMachineServerLost: { case AgStateMachineServerLost: {
disp.showDashboard("AG Server N/A"); disp.showDashboard(OledDisplay::DashBoardStatusServerIssue);
break; break;
} }
case AgStateMachineSensorConfigFailed: { case AgStateMachineSensorConfigFailed: {
@ -557,19 +557,24 @@ void StateMachine::displayHandle(AgStateMachineState state) {
if (ms >= 5000) { if (ms >= 5000) {
addToDashboardTime = millis(); addToDashboardTime = millis();
if (addToDashBoardToggle) { if (addToDashBoardToggle) {
disp.showDashboard("Add to AG Dashb."); disp.showDashboard(OledDisplay::DashBoardStatusAddToDashboard);
} else { } else {
disp.showDashboard(ag->deviceId().c_str()); disp.showDashboard(OledDisplay::DashBoardStatusDeviceId);
} }
addToDashBoardToggle = !addToDashBoardToggle; addToDashBoardToggle = !addToDashBoardToggle;
} }
} else { } else {
disp.showDashboard(""); disp.showDashboard();
} }
break; break;
} }
case AgStateMachineNormal: { case AgStateMachineNormal: {
if (config.isOfflineMode()) {
disp.showDashboard(
OledDisplay::DashBoardStatusOfflineMode);
} else {
disp.showDashboard(); disp.showDashboard();
}
break; break;
} }
case AgStateMachineCo2Calibration: case AgStateMachineCo2Calibration:

View File

@ -2,6 +2,8 @@
#include "AgConfigure.h" #include "AgConfigure.h"
#include "AirGradient.h" #include "AirGradient.h"
#include "App/AppDef.h" #include "App/AppDef.h"
#include <cmath>
#include <sstream>
#define json_prop_pmFirmware "firmware" #define json_prop_pmFirmware "firmware"
#define json_prop_pm01Ae "pm01" #define json_prop_pm01Ae "pm01"
@ -686,6 +688,155 @@ float Measurements::getCorrectedPM25(bool useAvg, int ch, bool forceCorrection)
return corrected; return corrected;
} }
Measurements::Measures Measurements::getMeasures() {
Measures mc;
mc.bootCount = _bootCount;
mc.freeHeap = ESP.getFreeHeap();
// co2, tvoc, nox
mc.co2 = _co2.update.avg;
mc.tvoc = _tvoc.update.avg;
mc.tvoc_raw = _tvoc_raw.update.avg;
mc.nox = _nox.update.avg;
mc.nox_raw = _nox_raw.update.avg;
// Temperature & Humidity
mc.temperature[0] = _temperature[0].update.avg;
mc.humidity[0] = _humidity[0].update.avg;
mc.temperature[1] = _temperature[1].update.avg;
mc.humidity[1] = _humidity[1].update.avg;
// PM atmospheric
mc.pm_01[0] = _pm_01[0].update.avg;
mc.pm_25[0] = _pm_25[0].update.avg;
mc.pm_10[0] = _pm_10[0].update.avg;
mc.pm_01[1] = _pm_01[1].update.avg;
mc.pm_25[1] = _pm_25[1].update.avg;
mc.pm_10[1] = _pm_10[1].update.avg;
// PM standard particle
mc.pm_01_sp[0] = _pm_01_sp[0].update.avg;
mc.pm_25_sp[0] = _pm_25_sp[0].update.avg;
mc.pm_10_sp[0] = _pm_10_sp[0].update.avg;
mc.pm_01_sp[1] = _pm_01_sp[1].update.avg;
mc.pm_25_sp[1] = _pm_25_sp[1].update.avg;
mc.pm_10_sp[1] = _pm_10_sp[1].update.avg;
// Particle Count
mc.pm_03_pc[0] = _pm_03_pc[0].update.avg;
mc.pm_05_pc[0] = _pm_05_pc[0].update.avg;
mc.pm_01_pc[0] = _pm_01_pc[0].update.avg;
mc.pm_25_pc[0] = _pm_25_pc[0].update.avg;
mc.pm_5_pc[0] = _pm_5_pc[0].update.avg;
mc.pm_10_pc[0] = _pm_10_pc[0].update.avg;
mc.pm_03_pc[1] = _pm_03_pc[1].update.avg;
mc.pm_05_pc[1] = _pm_05_pc[1].update.avg;
mc.pm_01_pc[1] = _pm_01_pc[1].update.avg;
mc.pm_25_pc[1] = _pm_25_pc[1].update.avg;
mc.pm_5_pc[1] = _pm_5_pc[1].update.avg;
mc.pm_10_pc[1] = _pm_10_pc[1].update.avg;
return mc;
}
std::string Measurements::buildMeasuresPayload(Measures &mc) {
std::ostringstream oss;
// CO2
if (utils::isValidCO2(mc.co2)) {
oss << std::round(mc.co2);
}
oss << ",";
// Temperature
if (utils::isValidTemperature(mc.temperature[0]) && utils::isValidTemperature(mc.temperature[1])) {
float temp = (mc.temperature[0] + mc.temperature[1]) / 2.0f;
oss << std::round(temp * 10);
} else if (utils::isValidTemperature(mc.temperature[0])) {
oss << std::round(mc.temperature[0] * 10);
} else if (utils::isValidTemperature(mc.temperature[1])) {
oss << std::round(mc.temperature[1] * 10);
}
oss << ",";
// Humidity
if (utils::isValidHumidity(mc.humidity[0]) && utils::isValidHumidity(mc.humidity[1])) {
float hum = (mc.humidity[0] + mc.humidity[1]) / 2.0f;
oss << std::round(hum * 10);
} else if (utils::isValidHumidity(mc.humidity[0])) {
oss << std::round(mc.humidity[0] * 10);
} else if (utils::isValidHumidity(mc.humidity[1])) {
oss << std::round(mc.humidity[1] * 10);
}
oss << ",";
/// PM1.0 atmospheric environment
if (utils::isValidPm(mc.pm_01[0]) && utils::isValidPm(mc.pm_01[1])) {
float pm01 = (mc.pm_01[0] + mc.pm_01[1]) / 2.0f;
oss << std::round(pm01 * 10);
} else if (utils::isValidPm(mc.pm_01[0])) {
oss << std::round(mc.pm_01[0] * 10);
} else if (utils::isValidPm(mc.pm_01[1])) {
oss << std::round(mc.pm_01[1] * 10);
}
oss << ",";
/// PM2.5 atmospheric environment
if (utils::isValidPm(mc.pm_25[0]) && utils::isValidPm(mc.pm_25[1])) {
float pm25 = (mc.pm_25[0] + mc.pm_25[1]) / 2.0f;
oss << std::round(pm25 * 10);
} else if (utils::isValidPm(mc.pm_25[0])) {
oss << std::round(mc.pm_25[0] * 10);
} else if (utils::isValidPm(mc.pm_25[1])) {
oss << std::round(mc.pm_25[1] * 10);
}
oss << ",";
/// PM10 atmospheric environment
if (utils::isValidPm(mc.pm_10[0]) && utils::isValidPm(mc.pm_10[1])) {
float pm10 = (mc.pm_10[0] + mc.pm_10[1]) / 2.0f;
oss << std::round(pm10 * 10);
} else if (utils::isValidPm(mc.pm_10[0])) {
oss << std::round(mc.pm_10[0] * 10);
} else if (utils::isValidPm(mc.pm_10[1])) {
oss << std::round(mc.pm_10[1] * 10);
}
oss << ",";
// TVOC
if (utils::isValidVOC(mc.tvoc)) {
oss << std::round(mc.tvoc);
}
oss << ",";
// NOx
if (utils::isValidNOx(mc.nox)) {
oss << std::round(mc.nox);
}
oss << ",";
/// PM 0.3 particle count
if (utils::isValidPm03Count(mc.pm_03_pc[0]) && utils::isValidPm03Count(mc.pm_03_pc[1])) {
oss << std::round((mc.pm_03_pc[0] + mc.pm_03_pc[1]) / 2.0f);
} else if (utils::isValidPm03Count(mc.pm_03_pc[0])) {
oss << std::round(mc.pm_03_pc[0]);
} else if (utils::isValidPm03Count(mc.pm_03_pc[1])) {
oss << std::round(mc.pm_03_pc[1]);
}
oss << ",";
if (mc.signal < 0) {
oss << mc.signal;
}
return oss.str();
}
String Measurements::toString(bool localServer, AgFirmwareMode fwMode, int rssi) { String Measurements::toString(bool localServer, AgFirmwareMode fwMode, int rssi) {
JSONVar root; JSONVar root;

View File

@ -7,6 +7,7 @@
#include "Libraries/Arduino_JSON/src/Arduino_JSON.h" #include "Libraries/Arduino_JSON/src/Arduino_JSON.h"
#include "Main/utils.h" #include "Main/utils.h"
#include <Arduino.h> #include <Arduino.h>
#include <cstdint>
#include <vector> #include <vector>
class Measurements { class Measurements {
@ -37,6 +38,31 @@ public:
Measurements(Configuration &config); 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); void setAirGradient(AirGradient *ag);
// Enumeration for every AG measurements // Enumeration for every AG measurements
@ -154,6 +180,10 @@ public:
*/ */
String toString(bool localServer, AgFirmwareMode fwMode, int rssi); String toString(bool localServer, AgFirmwareMode fwMode, int rssi);
Measures getMeasures();
std::string buildMeasuresPayload(Measures &measures);
/** /**
* Set to true if want to debug every update value * Set to true if want to debug every update value
*/ */

View File

@ -15,9 +15,10 @@
#include "Main/utils.h" #include "Main/utils.h"
#ifndef GIT_VERSION #ifndef GIT_VERSION
#define GIT_VERSION "3.2.0-snap" #define GIT_VERSION "3.3.6-snap"
#endif #endif
#ifndef ESP8266 #ifndef ESP8266
// Airgradient server root ca certificate // Airgradient server root ca certificate
const char *const AG_SERVER_ROOT_CA = const char *const AG_SERVER_ROOT_CA =

View File

@ -1,171 +0,0 @@
#include "OtaHandler.h"
#ifndef ESP8266 // Only for esp32 based mcu
#include "AirGradient.h"
void OtaHandler::setHandlerCallback(OtaHandlerCallback_t callback) { _callback = callback; }
void OtaHandler::updateFirmwareIfOutdated(String deviceId) {
String url =
"https://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;
config.cert_pem = AG_SERVER_ROOT_CA;
OtaUpdateOutcome ret = attemptToPerformOta(&config);
Serial.println(ret);
if (_callback) {
switch (ret) {
case OtaUpdateOutcome::UPDATE_PERFORMED:
_callback(OtaState::OTA_STATE_SUCCESS, "");
break;
case OtaUpdateOutcome::UPDATE_SKIPPED:
_callback(OtaState::OTA_STATE_SKIP, "");
break;
case OtaUpdateOutcome::ALREADY_UP_TO_DATE:
_callback(OtaState::OTA_STATE_UP_TO_DATE, "");
break;
case OtaUpdateOutcome::UPDATE_FAILED:
_callback(OtaState::OTA_STATE_FAIL, "");
break;
default:
break;
}
}
}
OtaHandler::OtaUpdateOutcome
OtaHandler::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::UPDATE_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 (_callback) {
_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 (_callback) {
_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 (_callback) {
_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 (_callback) {
_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 (_callback) {
_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 OtaHandler::cleanupHttp(esp_http_client_handle_t client) {
esp_http_client_close(client);
esp_http_client_cleanup(client);
}
#endif

View File

@ -1,43 +0,0 @@
#ifndef OTA_HANDLER_H
#define OTA_HANDLER_H
#ifndef ESP8266 // Only for esp32 based mcu
#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
class OtaHandler {
public:
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);
void setHandlerCallback(OtaHandlerCallback_t callback);
void updateFirmwareIfOutdated(String deviceId);
private:
OtaHandlerCallback_t _callback;
enum OtaUpdateOutcome {
UPDATE_PERFORMED = 0,
ALREADY_UP_TO_DATE,
UPDATE_FAILED,
UPDATE_SKIPPED
}; // Internal use
OtaUpdateOutcome attemptToPerformOta(const esp_http_client_config_t *config);
void cleanupHttp(esp_http_client_handle_t client);
};
#endif // ESP8266
#endif // OTA_HANDLER_H

View File

@ -131,6 +131,22 @@ void Sgp41::handle(void) {
} }
#else #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 * @brief Handle the sensor conditioning and run time udpate value, This method
* must not call, it's called on private task * must not call, it's called on private task
@ -152,6 +168,11 @@ void Sgp41::_handle(void) {
uint16_t srawVoc, srawNox; uint16_t srawVoc, srawNox;
for (;;) { for (;;) {
vTaskDelay(pdMS_TO_TICKS(1000)); vTaskDelay(pdMS_TO_TICKS(1000));
if (onPause) {
continue;
}
if (getRawSignal(srawVoc, srawNox)) { if (getRawSignal(srawVoc, srawNox)) {
tvocRaw = srawVoc; tvocRaw = srawVoc;
noxRaw = srawNox; noxRaw = srawNox;

View File

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