Compare commits

...

115 Commits

Author SHA1 Message Date
samuelbles07
ba250787e6 WIP - trial energy saving 2025-10-13 02:58:11 +07:00
Samuel Siburian
23f8c383fd Merge pull request #345 from airgradienthq/feat/print-s8-info
Print S8 sensor information
2025-10-02 19:22:22 +07:00
samuelbles07
c0ad1dbfad Print S8 sensor information 2025-10-02 19:18:59 +07:00
Samuel Siburian
75be7d9fc5 Merge pull request #342 from mtlynch/no-trailing-spaces
Eliminate trailing whitespace
2025-09-25 13:45:11 +07:00
Samuel Siburian
309d322100 Merge pull request #343 from sysboy/typo-fix-failuire
Simple typo fix
2025-09-25 11:37:54 +07:00
Steve
89ebe6c39f Simple typo fix 2025-09-23 15:40:36 +01:00
Michael Lynch
29db5469f5 Eliminate trailing whitespace 2025-09-17 20:06:50 -04:00
Samuel Siburian
5802cf17f6 Merge pull request #328 from MallocArray/patch-3
Correct Yellow and Orange LED colors
2025-09-15 19:58:19 +07:00
Samuel Siburian
4a4ce89f00 Merge pull request #340 from airgradienthq/release-information
Add release information section on README
2025-09-15 13:15:42 +07:00
samuelbles07
8985e08a00 fix link typo 2025-09-15 13:05:19 +07:00
samuelbles07
e984aced18 Add release process 2025-09-15 13:03:28 +07:00
samuelbles07
73edf56c97 Fix points to how to compile documents 2025-09-15 13:03:16 +07:00
samuelbles07
e54c62a2ef Merge remote-tracking branch 'origin/develop' into develop 2025-09-12 14:34:50 +07:00
samuelbles07
5c95f011e4 Fix typo in openmetrics 2025-09-12 14:33:36 +07:00
Samuel Siburian
bed448e7d6 Merge pull request #289 from hestiahacker/contributing_improvements
docs: added some details about how to compile and contribute changes upstream
2025-09-03 15:53:17 +07:00
Samuel Siburian
eb8378adfa Merge pull request #338 from Adrien-P/fix/mqtt-with-auth-connected-flag
fix: ensure connected flag set for both authenticated connections.
2025-08-19 10:03:47 +07:00
Adrien-P
f94ed5c5f5 fix: ensure connected flag set for both authenticated connections. 2025-08-18 19:33:30 +02:00
Samuel Siburian
9471f3e323 Merge pull request #337 from mtlynch/fix-js-example
Fix JSON example in /config docs
2025-08-14 01:45:50 +07:00
Samuel Siburian
e6d90372c2 Merge pull request #336 from mtlynch/link-docs
Link to inner docs pages from main README
2025-08-14 01:45:05 +07:00
Michael Lynch
fabf0550fc Fix JSON example in /config docs
The JSON example has an extra closing brace, causing the JSON to be invalid. The fix is to remove the stray closing brace.
2025-08-12 20:45:41 -04:00
Michael Lynch
ce0af5bf60 Link to inner docs pages from main README
Fixes #335

There's useful documentation in the docs/ directory, but it's hard for users to discover it, as there are no links to it.

This change updates the README to add links to the inner docs pages for users interested in customizing and re-flashing firmware onto their devices as well as understanding the OTA update system.
2025-08-12 20:08:29 -04:00
Samuel Siburian
93bdbd85d5 Merge pull request #333 from airgradienthq/feat/support-2g
Support 2G fallback for CE network options
2025-08-01 12:03:45 +07:00
samuelbles07
29f583f20d Fix compile error for esp8266 env 2025-08-01 11:59:41 +07:00
samuelbles07
0f6a2fe908 Bump airgradient-client 2025-08-01 11:54:11 +07:00
Samuel Siburian
e3ce2c41be Merge pull request #332 from airgradienthq/fix/post-time-resolution
Fix post measures time resolutions closer to 1 second
2025-07-28 11:47:53 +07:00
samuelbles07
92b3c69b98 Decrease network task iteration delay
Position post measure first before get configuration
2025-07-26 18:23:04 +07:00
samuelbles07
f4d518aa87 Fix agschedule anon function call order 2025-07-26 18:22:30 +07:00
Samuel Siburian
831c844c24 Merge pull request #330 from airgradienthq/feat/post-ccid
Provide SIM card ICCID when performing ota update
2025-06-20 17:38:22 +07:00
samuelbles07
060a7f6815 Provide iccid when checking if firmware update available 2025-06-20 01:08:46 +07:00
samuelbles07
d8eb6b3c1a Prepare release 3.3.9 2025-06-18 14:25:14 +07:00
MallocArray
77859bea22 Correct Yellow and Orange LED colors 2025-06-16 20:38:19 -05:00
Samuel Siburian
969858b5cb Merge pull request #324 from airgradienthq/fix/comms-ag-server
Post measures and fetch configuration on boot only if respective configuration is set
2025-06-10 01:29:58 +07:00
samuelbles07
09b5805686 Apply brightness 2025-06-10 01:26:29 +07:00
samuelbles07
b09b753339 Only send first measures on boot if postDataToAirgradient is enabled 2025-06-10 01:10:14 +07:00
samuelbles07
ddb3dba131 Skip fetch configuration on boot when configuration control is local 2025-06-10 01:09:53 +07:00
Samuel Siburian
e780b0ace6 Merge pull request #323 from airgradienthq/fix/local-config-update
Update configuration changes by callback
2025-06-09 02:21:07 +07:00
samuelbles07
e82da5401e Add new flag for command request
Such as led bar test and co2 calibration test
2025-06-09 02:13:32 +07:00
samuelbles07
50a98acde4 Update configuration changes to main by callback 2025-06-06 04:10:53 +07:00
samuelbles07
7049d21a41 Prepare release 3.3.8 2025-05-14 13:09:12 +07:00
samuelbles07
d5cdeaa9f3 Fix print average function schedule
if pms value invalid show the channel
2025-05-14 13:01:23 +07:00
Samuel Siburian
09207c6923 Merge pull request #319 from airgradienthq/fix/resizing-queue
Fix resizing measurement queue after post by cellular post
2025-05-14 12:53:07 +07:00
samuelbles07
0a64424196 add show content delay for display brightness 2025-05-14 12:39:42 +07:00
samuelbles07
5b38ca222b Prepare release 3.3.7 2025-05-12 10:54:38 +07:00
Samuel Siburian
9ee35341a5 Merge pull request #318 from airgradienthq/feat/improve-measure-logs
Improve measurements logging
2025-05-11 14:22:08 +07:00
samuelbles07
cec0514444 print measurements on schedule 2025-05-11 14:10:51 +07:00
hestiahacker
21b9ddb2ed Improved contribution instructions
Improved wording suggested by @Iroxxar
2025-05-11 01:10:25 -05:00
hestia
1ee05da5d1 docs: added some details about how to compile and contribute changes upstream 2025-05-11 01:10:22 -05:00
samuelbles07
626a2240fa Fix resizing queue after success post
This fix should be make it more consistent
2025-05-09 15:45:53 +07:00
Samuel Siburian
174ec6568f Merge pull request #313 from airgradienthq/fix/mode-cloud-disable
Fix bootloop when cloud connection is disabled
2025-05-08 01:28:07 +07:00
samuelbles07
6b55719399 Fix cloud connection mode
Use continue instead of return to ignore the rest of transmission code
2025-05-05 17:37:28 +07:00
samuelbles07
e2084f0738 Fix OTA request on boot when cloud Connection disabled 2025-05-05 17:36:14 +07:00
Samuel Siburian
5e07923690 Merge pull request #311 from ccoley/fix/submodule-url
Fix failing to clone submodules recursively
2025-05-04 14:13:47 +07:00
Samuel Siburian
04049439b1 Merge pull request #310 from airgradienthq/fix/openmetrics
Fix calling airgradient client before initialization on open metrics
2025-05-04 13:29:24 +07:00
Chris Coley
c148d256d7 Use a relative path for submodule URL
We keep the username in the path so that forks don't need to also fork
the submodules
2025-04-30 23:25:30 -07:00
samuelbles07
02849a1938 Fix pass agclient to openmetrics
previously, agclient initialized after setAirgradient
2025-05-01 13:38:42 +08:00
Samuel Siburian
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
samuelbles07
4daa817a0b Change airgradient-ota commit to main branch 2025-04-21 13:41:15 +07:00
samuelbles07
81a4502952 Fix: http domain applied for OTA 2025-04-21 13:27:05 +07:00
samuelbles07
764e2eae38 Prepare release 3.3.6 2025-04-16 12:34:17 +07:00
Samuel Siburian
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
samuelbles07
9475724d0c Remove comment 2025-04-15 12:20:26 +07:00
samuelbles07
e7603a7659 Update feedback
Change airgradient-ota submodule to latest main instead of branch
2025-04-14 15:24:53 +07:00
samuelbles07
9bba89722e Fix sgp unreliable value by only pause task when performing ota 2025-04-12 02:25:04 +07:00
samuelbles07
81945a358e SGP41 add method to pause and resume task handle 2025-04-12 02:22:55 +07:00
samuelbles07
3d26a54d69 Prepare release 3.3.5 2025-04-11 15:56:05 +07:00
Samuel Siburian
b70ee75d50 Merge pull request #302 from airgradienthq/improve-ce-reconnection
Improve cellular client reconnection
2025-04-11 15:49:25 +07:00
samuelbles07
c6846c818a Rename MICROS_TO_MINUTES() to follow convention 2025-04-11 15:46:21 +07:00
samuelbles07
0b1c901a76 Rename cellularModule object name to cellularCard
Rename checkCellularClientNotReady to restartIfCeClientIssueOverTwoHours
2025-04-11 13:41:07 +07:00
samuelbles07
83504c8628 Bump libs to latest 2025-04-10 19:05:28 +07:00
samuelbles07
4487992748 Remove unnecessary code 2025-04-10 14:58:51 +07:00
samuelbles07
3c8a65a329 Use esp_timer_get_time for timer of ce client not ready 2025-04-10 14:58:11 +07:00
samuelbles07
673d564ddb Fix based on feedback 2025-04-10 12:45:18 +07:00
samuelbles07
423eb4808f Change airgradient-client to latest main 2025-04-10 02:14:34 +07:00
samuelbles07
18a710ffc2 Make sure transmit cycle not too long to wait divisible by 3 2025-04-10 02:06:11 +07:00
samuelbles07
040cb79a4d Transmit measures only if queue size is 1 or divisible by 3 2025-04-10 00:27:44 +07:00
samuelbles07
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
samuelbles07
1c6bc3ec55 Bump airgradient-client fix esp8266 compile 2025-04-09 22:48:21 +07:00
samuelbles07
34d7c93e14 Improve reconnection of CE network option
Restart system if it already too long
2025-04-09 15:51:54 +07:00
samuelbles07
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
samuelbles07
9fb01d42f4 Prepare release 3.3.4 2025-04-07 16:56:54 +07:00
Samuel Siburian
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
samuelbles07
0da21155e7 bump submodule to post measures with new endpoint
that include signal in rssi
2025-04-07 16:29:54 +07:00
samuelbles07
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
samuelbles07
b079c35e6b Include cellular signal in rssi to measurement cycle 2025-04-07 16:28:37 +07:00
Samuel Siburian
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
samuelbles07
c95379b957 Update submodule to the latest main branch 2025-04-07 15:30:52 +07:00
samuelbles07
0cae8bc185 Change ag log level to info 2025-04-05 23:56:14 +07:00
samuelbles07
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
samuelbles07
66818cd075 prepare 3.3.3 release 2025-04-04 11:31:19 +07:00
Samuel Siburian
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
samuelbles07
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
Samuel Siburian
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
samuelbles07
61b863b7f1 Fix esp_log logs not come out on O-PP 2025-04-04 10:21:50 +07:00
samuelbles07
e01c1029fe Bump ag client
ensure client connection properly
2025-04-04 10:09:34 +07:00
Samuel Siburian
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
samuelbles07
a91747e379 Update config sample 2025-04-02 16:30:13 +07:00
samuelbles07
029457c3fa Add accepted value to http domain 2025-04-02 16:26:07 +07:00
samuelbles07
55710dd4d9 Update docs for new configuration http domain 2025-04-02 16:18:55 +07:00
samuelbles07
4886163cda Show on oled when httpDomain is set 2025-04-02 02:33:24 +07:00
samuelbles07
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
samuelbles07
9ed58d1853 Prepare release 3.3.2 2025-03-31 17:12:15 +07:00
Samuel Siburian
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
samuelbles07
2f69932ef7 add depth submodule update 2025-03-31 17:04:37 +07:00
samuelbles07
1d96a274a6 Merge branch 'develop' into feat/enable-at-debug 2025-03-31 16:55:12 +07:00
Samuel Siburian
df9f6dfc95 Fix bugs from 3.3.1 release 2025-03-31 16:52:09 +07:00
samuelbles07
3fc02b3f54 Check signal when initialize cellular client 2025-03-31 16:51:29 +07:00
samuelbles07
958ed0bd80 Fix TVOC and NOx payload position 2025-03-31 15:26:34 +07:00
samuelbles07
e9be9dcc83 Fix mqtt host still exist on local when on server is disabled 2025-03-31 14:51:53 +07:00
samuelbles07
7fbab82088 Change log level when correction not found 2025-03-31 14:07:30 +07:00
samuelbles07
decdecdf22 Don't start mqtt when network option is cellular
Even when mqtt host is set
2025-03-31 14:01:49 +07:00
samuelbles07
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
samuelbles07
37de127887 prepare 3.3.1 release 2025-03-28 14:37:08 +07:00
samuelbles07
baf80ce250 untrack compile_commands.json 2025-03-28 14:24:13 +07:00
samuelbles07
80100e2475 prepare 3.3.0 release 2025-03-28 14:13:40 +07:00
Samuel Siburian
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
42 changed files with 824 additions and 1853 deletions

View File

@@ -1,5 +1,36 @@
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
trailing-whitespace:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4.2.2
with:
fetch-depth: 0
- name: Check for trailing whitespace
run: |
set -u
# Don't enforce checks on vendored libraries.
readonly EXCLUDED_DIR='src/Libraries'
has_trailing_whitespace=false
while read -r line; do
if grep \
"\s$" \
--line-number \
--with-filename \
--binary-files=without-match \
"${line}"; then
has_trailing_whitespace=true
fi
done < <(git ls-files | grep --invert-match "^${EXCLUDED_DIR}/")
if [ "$has_trailing_whitespace" = true ]; then
echo "ERROR: Found trailing whitespace"
exit 1
fi
compile: compile:
strategy: strategy:
fail-fast: false fail-fast: false
@@ -38,6 +69,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v4.2.2
with: with:
fetch-depth: 0
submodules: 'true' submodules: 'true'
- uses: arduino/compile-sketches@v1.1.2 - uses: arduino/compile-sketches@v1.1.2
with: with:

4
.gitignore vendored
View File

@@ -4,5 +4,7 @@ build
/.idea/ /.idea/
.pio .pio
.cache .cache
.clangd
logs logs
gen_compile_commands.py
compile_commands.json

4
.gitmodules vendored
View File

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

View File

@@ -20,13 +20,32 @@ Make sure you have exactly the versions of libraries and boards installed as des
If you have an older version of the AirGradient PCB not mentioned in the example files, please downgrade this library to version 2.4.15 to support these legacy boards. If you have an older version of the AirGradient PCB not mentioned in the example files, please downgrade this library to version 2.4.15 to support these legacy boards.
### Release Process
Releases published on GitHub are **not immediately deployed to all devices in the market**. Each release first goes through internal testing, including limited deployments in select locations to verify stability and functionality.
If the tests pass, the firmware is then made available for:
- **FOTA (Firmware Over-The-Air) updates** from AirGradient dashboard
- **Manual flashing** via [Airgradient](https://www.airgradient.com/documentation/firmwares/) website
Each GitHub release note will also include the planned rollout date for wider availability.
## Help & Support ## Help & Support
If you have any questions or problems, check out [our forum](https://forum.airgradient.com/). If you have any questions or problems, check out [our forum](https://forum.airgradient.com/).
## Documentation ## Development
Local server API documentation is available in [/docs/local-server.md](/docs/local-server.md) and AirGradient server API on [https://api.airgradient.com/public/docs/api/v1/](https://api.airgradient.com/public/docs/api/v1/). * See [compilation instructions](/docs/howto-compile.md) for details about how to customize AirGradient's firmware and flash it to your device.
## Over the air (OTA) updates
* See the [OTA Updates documentation](/docs/ota-updates.md) for details about how AirGradient monitors receive over the air updates.
## API documentation
* [Local server API documentation](/docs/local-server.md)
* [AirGradient Cloud server API documentation](https://api.airgradient.com/public/docs/api/v1/).
## The following libraries have been integrated into this library for ease of use ## The following libraries have been integrated into this library for ease of use

File diff suppressed because one or more lines are too long

View File

@@ -16,15 +16,15 @@ Arduino IDE version 2.x ([download](https://www.arduino.cc/en/software))
#### Version < 3.2.0 #### Version < 3.2.0
Using library manager install the latest version (Tools ➝ Manage Libraries... ➝ search for `"airgradient"`) Using library manager install the latest version (Tools ➝ Manage Libraries... ➝ search for `"airgradient"`)
![Aigradient Library](images/ag-lib.png) ![Aigradient Library](images/ag-lib.png)
#### Version >= 3.3.0 #### Version >= 3.3.0
- From your terminal, go to Arduino libraries folder (windows and mac: `Documents/Arduino/libraries` or linux: `~/Arduino/Libraries`). - 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` - With **git** cli, execute this command `git clone --recursive https://github.com/airgradienthq/arduino.git AirGradient_Air_Quality_Sensor`
- Restart Arduino IDE - Restart Arduino IDE
3. On tools tab, follow settings below 3. On tools tab, follow settings below
@@ -57,7 +57,7 @@ Upload Speed ➝ 921600
![board manager](images/esp8266-board.png) ![board manager](images/esp8266-board.png)
3. Install AirGradient library on library manager using the latest version (Tools ➝ Manage Libraries... ➝ search for `"airgradient"`) 3. Install AirGradient library on library manager using the latest version (Tools ➝ Manage Libraries... ➝ search for `"airgradient"`)
![Aigradient Library](images/ag-lib.png) ![Aigradient Library](images/ag-lib.png)
@@ -65,7 +65,7 @@ Upload Speed ➝ 921600
![settings esp8266](images/settings-esp8266.png) ![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` 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 6. Compile
![compiled esp8266](images/compiled-esp8266.png) ![compiled esp8266](images/compiled-esp8266.png)
@@ -78,19 +78,19 @@ ModuleNotFoundError: No module named serial
![Linux Failed](images/linux-failed.png) ![Linux Failed](images/linux-failed.png)
Make sure python pyserial module installed globally in the environment by executing: Make sure python pyserial module installed globally in the environment by executing:
`$ sudo apt install -y python3-pyserial` `$ sudo apt install -y python3-pyserial`
or or
`$ pip install pyserial` `$ pip install pyserial`
Choose based on how python installed on your machine. But most user, using `apt` is better. Choose based on how python installed on your machine. But most user, using `apt` is better.
## How to contribute ## 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. 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 which you plan to contribute back to the main project, instead of installing the AirGradient library, check out the repository at Documents/Arduino/libraries (for Windows and Mac) or ~/Arduino/libraries (for 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` **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`
@@ -100,6 +100,3 @@ There are 2 environment options to compile this project, PlatformIO and ArduinoI
- For PlatformIO, it should work out of the box - 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 - 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

View File

@@ -1,6 +1,6 @@
## Local Server API ## Local Server API
From [firmware version 3.0.10](firmwares) onwards, the AirGradient ONE and Open Air monitors have below API available. From [firmware version 3.0.10](firmwares) onwards, the AirGradient ONE and Open Air monitors have below API available.
### Discovery ### Discovery
@@ -20,7 +20,7 @@ http://airgradient_ecda3b1eaaaf.local/measures/current
“ecda3b1eaaaf” being the serial number of your monitor. “ecda3b1eaaaf” being the serial number of your monitor.
You get the following response: You get the following response:
```json ```json
{ {
"wifi": -46, "wifi": -46,
"serialno": "ecda3b1eaaaf", "serialno": "ecda3b1eaaaf",
@@ -84,7 +84,7 @@ Compensated values apply correction algorithms to make the sensor values more ac
"/config" path returns the current configuration of the monitor. "/config" path returns the current configuration of the monitor.
```json ```json
{ {
"country": "TH", "country": "TH",
"pmStandard": "ugm3", "pmStandard": "ugm3",
@@ -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,
@@ -105,7 +106,6 @@ Compensated values apply correction algorithms to make the sensor values more ac
"pm02": { "pm02": {
"correctionAlgorithm": "epa_2021", "correctionAlgorithm": "epa_2021",
"slr": {} "slr": {}
}
} }
} }
} }
@@ -118,22 +118,22 @@ Configuration parameters can be changed with a PUT request to the monitor, e.g.
Example to force CO2 calibration Example to force CO2 calibration
```bash ```bash
curl -X PUT -H "Content-Type: application/json" -d '{"co2CalibrationRequested":true}' http://airgradient_84fce612eff4.local/config curl -X PUT -H "Content-Type: application/json" -d '{"co2CalibrationRequested":true}' http://airgradient_84fce612eff4.local/config
``` ```
Example to set monitor to Celsius Example to set monitor to Celsius
```bash ```bash
curl -X PUT -H "Content-Type: application/json" -d '{"temperatureUnit":"c"}' http://airgradient_84fce612eff4.local/config curl -X PUT -H "Content-Type: application/json" -d '{"temperatureUnit":"c"}' http://airgradient_84fce612eff4.local/config
``` ```
If you use command prompt on Windows, you need to escape the quotes: If you use command prompt on Windows, you need to escape the quotes:
``` -d "{\"param\":\"value\"}" ``` ``` -d "{\"param\":\"value\"}" ```
### Avoiding Conflicts with Configuration on AirGradient Server ### Avoiding Conflicts with Configuration on AirGradient Server
If the monitor is set up on the AirGradient dashboard, it will also receive the configuration parameters from there. In case you do not want this, please set `configurationControl` to `local`. In case you set it to `cloud` and want to change it to `local`, you need to make a factory reset. If the monitor is set up on the AirGradient dashboard, it will also receive the configuration parameters from there. In case you do not want this, please set `configurationControl` to `local`. In case you set it to `cloud` and want to change it to `local`, you need to make a factory reset.
### Configuration Parameters (GET/PUT) ### Configuration Parameters (GET/PUT)
@@ -146,7 +146,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 +155,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**
@@ -203,34 +204,34 @@ Example correction configuration:
Field Name: `pm02` Field Name: `pm02`
| Algorithm | Value | Description | SLR required | | Algorithm | Value | Description | SLR required |
|------------|-------------|------|---------| |------------|-------------|------|---------|
| Raw | `"none"` | No correction (default) | No | | Raw | `"none"` | No correction (default) | No |
| EPA 2021 | `"epa_2021"` | Use EPA 2021 correction factors on top of raw value | No | | EPA 2021 | `"epa_2021"` | Use EPA 2021 correction factors on top of raw value | No |
| PMS5003_20240104 | `"slr_PMS5003_20240104"` | Correction for PMS5003 sensor batch 20240104| Yes | | PMS5003_20240104 | `"slr_PMS5003_20240104"` | Correction for PMS5003 sensor batch 20240104| Yes |
| PMS5003_20231218 | `"slr_PMS5003_20231218"` | Correction for PMS5003 sensor batch 20231218| Yes | | PMS5003_20231218 | `"slr_PMS5003_20231218"` | Correction for PMS5003 sensor batch 20231218| Yes |
| PMS5003_20231030 | `"slr_PMS5003_20231030"` | Correction for PMS5003 sensor batch 20231030| Yes | | PMS5003_20231030 | `"slr_PMS5003_20231030"` | Correction for PMS5003 sensor batch 20231030| Yes |
**NOTES**: **NOTES**:
- Set `useEpa2021` to `true` if want to apply EPA 2021 correction factors on top of SLR correction value, otherwise `false` - Set `useEpa2021` to `true` if want to apply EPA 2021 correction factors on top of SLR correction value, otherwise `false`
- `intercept` and `scalingFactor` values can be obtained from [this article](https://www.airgradient.com/blog/low-readings-from-pms5003/) - `intercept` and `scalingFactor` values can be obtained from [this article](https://www.airgradient.com/blog/low-readings-from-pms5003/)
- If `configurationControl` is set to `local` (eg. when using Home Assistant), correction need to be set manually, see examples below - If `configurationControl` is set to `local` (eg. when using Home Assistant), correction need to be set manually, see examples below
**Examples**: **Examples**:
- PMS5003_20231030 - PMS5003_20231030
```bash ```bash
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20231030","slr":{"intercept":0,"scalingFactor":0.02838,"useEpa2021":true}}}}' curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20231030","slr":{"intercept":0,"scalingFactor":0.02838,"useEpa2021":true}}}}'
``` ```
- PMS5003_20231218 - PMS5003_20231218
```bash ```bash
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20231218","slr":{"intercept":0,"scalingFactor":0.03525,"useEpa2021":true}}}}' curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20231218","slr":{"intercept":0,"scalingFactor":0.03525,"useEpa2021":true}}}}'
``` ```
- PMS5003_20240104 - PMS5003_20240104
```bash ```bash
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20240104","slr":{"intercept":0,"scalingFactor":0.02896,"useEpa2021":true}}}}' curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20240104","slr":{"intercept":0,"scalingFactor":0.02896,"useEpa2021":true}}}}'
@@ -243,10 +244,10 @@ Field Name:
- Humidity: `rhum` - Humidity: `rhum`
| Algorithm | Value | Description | SLR required | | Algorithm | Value | Description | SLR required |
|------------|-------------|------|---------| |------------|-------------|------|---------|
| Raw | `"none"` | No correction (default) | No | | Raw | `"none"` | No correction (default) | No |
| AirGradient Standard Correction | `"ag_pms5003t_2024"` | Using standard airgradient correction (for outdoor monitor)| No | | AirGradient Standard Correction | `"ag_pms5003t_2024"` | Using standard airgradient correction (for outdoor monitor)| No |
| Custom | `"custom"` | custom corrections constant, set `intercept` and `scalingFactor` manually | Yes | | Custom | `"custom"` | custom corrections constant, set `intercept` and `scalingFactor` manually | Yes |
*Table above apply for both Temperature and Humidity* *Table above apply for both Temperature and Humidity*

View File

@@ -1,6 +1,6 @@
## OTA Updates ## OTA Updates
From [firmware version 3.1.1](https://github.com/airgradienthq/arduino/tree/3.1.1) onwards, the AirGradient ONE and Open Air monitors support over the air (OTA) updates. From [firmware version 3.1.1](https://github.com/airgradienthq/arduino/tree/3.1.1) onwards, the AirGradient ONE and Open Air monitors support over the air (OTA) updates.
#### Mechanism #### Mechanism
@@ -10,7 +10,7 @@ The device attempts to update to the latest version on startup and in regular in
http://hw.airgradient.com/sensors/{deviceId}/generic/os/firmware.bin?current_firmware={GIT_VERSION} http://hw.airgradient.com/sensors/{deviceId}/generic/os/firmware.bin?current_firmware={GIT_VERSION}
If does pass the version it is currently running on along to the server through URL parameter 'current_firmware'. If does pass the version it is currently running on along to the server through URL parameter 'current_firmware'.
This allows the server to identify if the device is already running on the latest version or should update. This allows the server to identify if the device is already running on the latest version or should update.
The following scenarios are possible The following scenarios are possible

View File

@@ -294,7 +294,7 @@ static bool sgp41Init(void) {
configuration.hasSensorSGP = true; configuration.hasSensorSGP = true;
return true; return true;
} else { } else {
Serial.println("Init SGP41 failuire"); Serial.println("Init SGP41 failure");
configuration.hasSensorSGP = false; configuration.hasSensorSGP = false;
} }
return false; return false;

View File

@@ -351,7 +351,7 @@ static bool sgp41Init(void) {
configuration.hasSensorSGP = true; configuration.hasSensorSGP = true;
return true; return true;
} else { } else {
Serial.println("Init SGP41 failuire"); Serial.println("Init SGP41 failure");
configuration.hasSensorSGP = false; configuration.hasSensorSGP = false;
} }
return false; return false;

View File

@@ -249,7 +249,7 @@ void loop() {
configUpdateHandle(); configUpdateHandle();
localServer._handle(); localServer._handle();
if (configuration.hasSensorSGP) { if (configuration.hasSensorSGP) {
ag.sgp41.handle(); ag.sgp41.handle();
} }
@@ -374,7 +374,7 @@ static bool sgp41Init(void) {
configuration.hasSensorSGP = true; configuration.hasSensorSGP = true;
return true; return true;
} else { } else {
Serial.println("Init SGP41 failuire"); Serial.println("Init SGP41 failure");
configuration.hasSensorSGP = false; configuration.hasSensorSGP = false;
} }
return false; return false;

View File

@@ -26,7 +26,6 @@ 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 "AgConfigure.h" #include "AgConfigure.h"
#include "AgSchedule.h" #include "AgSchedule.h"
#include "AgStateMachine.h" #include "AgStateMachine.h"
@@ -37,6 +36,7 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
#include "Arduino.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"
@@ -45,6 +45,7 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
#include <HardwareSerial.h> #include <HardwareSerial.h>
#include <WebServer.h> #include <WebServer.h>
#include <WiFi.h> #include <WiFi.h>
#include <cstdint>
#include <string> #include <string>
#include "Libraries/airgradient-client/src/agSerial.h" #include "Libraries/airgradient-client/src/agSerial.h"
@@ -65,7 +66,7 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
#define WIFI_TRANSMISSION_INTERVAL 1 * 60000 /** ms */ #define WIFI_TRANSMISSION_INTERVAL 1 * 60000 /** ms */
#define CELLULAR_SERVER_CONFIG_SYNC_INTERVAL 30 * 60000 /** ms */ #define CELLULAR_SERVER_CONFIG_SYNC_INTERVAL 30 * 60000 /** ms */
#define CELLULAR_MEASUREMENT_INTERVAL 3 * 60000 /** ms */ #define CELLULAR_MEASUREMENT_INTERVAL 3 * 60000 /** ms */
#define CELLULAR_TRANSMISSION_INTERVAL 9 * 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 */
@@ -74,7 +75,10 @@ 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 MAXIMUM_MEASUREMENT_CYCLE_QUEUE 80
#define RESERVED_MEASUREMENT_CYCLE_CAPACITY 10 #define RESERVED_MEASUREMENT_CYCLE_CAPACITY 10
@@ -88,6 +92,8 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
#define GPIO_EXPANSION_CARD_POWER 4 #define GPIO_EXPANSION_CARD_POWER 4
#define GPIO_IIC_RESET 3 #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);
@@ -102,7 +108,7 @@ static OpenMetrics openMetrics(measurements, configuration, wifiConnector);
static LocalServer localServer(Serial, openMetrics, measurements, configuration, static LocalServer localServer(Serial, openMetrics, measurements, configuration,
wifiConnector); wifiConnector);
static AgSerial *agSerial; static AgSerial *agSerial;
static CellularModule *cell; static CellularModule *cellularCard;
static AirgradientClient *agClient; static AirgradientClient *agClient;
enum NetworkOption { enum NetworkOption {
@@ -111,12 +117,17 @@ enum NetworkOption {
}; };
NetworkOption networkOption; NetworkOption networkOption;
TaskHandle_t handleNetworkTask = NULL; TaskHandle_t handleNetworkTask = NULL;
static bool otaInProgress = false; 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; SemaphoreHandle_t mutexMeasurementCycleQueue;
static std::vector<Measurements::Measures> measurementCycleQueue; static std::vector<Measurements::Measures> measurementCycleQueue;
@@ -125,13 +136,14 @@ static void boardInit(void);
static void initializeNetwork(); 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 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);
static void sendDataToServer(void); static void sendDataToServer(void);
static void tempHumUpdate(void); static void tempHumUpdate(void);
static void co2Update(void); static void co2Update(void);
static void printMeasurements();
static void mdnsInit(void); static void mdnsInit(void);
static void createMqttTask(void); static void createMqttTask(void);
static void initMqtt(void); static void initMqtt(void);
@@ -145,6 +157,7 @@ static void displayExecuteOta(AirgradientOTA::OtaResult result, String msg, int
static int calculateMaxPeriod(int updateInterval); static int calculateMaxPeriod(int updateInterval);
static void setMeasurementMaxPeriod(); static void setMeasurementMaxPeriod();
static void newMeasurementCycle(); static void newMeasurementCycle();
static void restartIfCeClientIssueOverTwoHours();
static void networkSignalCheck(); static void networkSignalCheck();
static void networkingTask(void *args); static void networkingTask(void *args);
@@ -160,13 +173,17 @@ 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); AgSchedule networkSignalCheckSchedule(10000, networkSignalCheck);
AgSchedule printMeasurementsSchedule(6000, printMeasurements);
static int pmsValueTaken = 0;
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 // Enable cullular module power board
pinMode(GPIO_EXPANSION_CARD_POWER, OUTPUT); pinMode(GPIO_EXPANSION_CARD_POWER, OUTPUT);
digitalWrite(GPIO_EXPANSION_CARD_POWER, HIGH); digitalWrite(GPIO_EXPANSION_CARD_POWER, HIGH);
@@ -179,6 +196,7 @@ void setup() {
/** Initialize local configure */ /** Initialize local configure */
configuration.begin(); configuration.begin();
configuration.setConfigurationUpdatedCallback(configUpdateHandle);
/** Init I2C */ /** Init I2C */
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN); Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
@@ -198,7 +216,7 @@ void setup() {
oledDisplay.setAirGradient(ag); oledDisplay.setAirGradient(ag);
stateMachine.setAirGradient(ag); stateMachine.setAirGradient(ag);
wifiConnector.setAirGradient(ag); wifiConnector.setAirGradient(ag);
openMetrics.setAirGradient(ag, agClient); openMetrics.setAirGradient(ag);
localServer.setAirGraident(ag); localServer.setAirGraident(ag);
measurements.setAirGradient(ag); measurements.setAirGradient(ag);
@@ -206,9 +224,6 @@ void setup() {
boardInit(); boardInit();
setMeasurementMaxPeriod(); setMeasurementMaxPeriod();
// Comment below line to disable debug measurement readings
measurements.setDebug(true);
bool connectToNetwork = true; bool connectToNetwork = true;
if (ag->isOne()) { // Offline mode only available for indoor monitor if (ag->isOne()) { // Offline mode only available for indoor monitor
/** Show message confirm offline mode, should me perform if LED bar button /** Show message confirm offline mode, should me perform if LED bar button
@@ -259,9 +274,13 @@ void setup() {
Serial.println("Display brightness: " + String(configuration.getDisplayBrightness())); Serial.println("Display brightness: " + String(configuration.getDisplayBrightness()));
oledDisplay.setBrightness(configuration.getDisplayBrightness()); oledDisplay.setBrightness(configuration.getDisplayBrightness());
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
} }
configSchedule.setPeriod(CELLULAR_TRANSMISSION_INTERVAL);
transmissionSchedule.setPeriod(CELLULAR_TRANSMISSION_INTERVAL);
if (networkOption == UseCellular) { if (networkOption == UseCellular) {
// If using cellular re-set scheduler interval // If using cellular re-set scheduler interval
configSchedule.setPeriod(CELLULAR_SERVER_CONFIG_SYNC_INTERVAL); configSchedule.setPeriod(CELLULAR_SERVER_CONFIG_SYNC_INTERVAL);
@@ -271,7 +290,7 @@ void setup() {
// Queue now only applied for cellular // Queue now only applied for cellular
// Allocate queue memory to avoid always reallocation // Allocate queue memory to avoid always reallocation
measurementCycleQueue.reserve(RESERVED_MEASUREMENT_CYCLE_CAPACITY); measurementCycleQueue.reserve(RESERVED_MEASUREMENT_CYCLE_CAPACITY);
// Initialize mutex to access mesurementCycleQueue // Initialize mutex to access mesurementCycleQueue
mutexMeasurementCycleQueue = xSemaphoreCreateMutex(); mutexMeasurementCycleQueue = xSemaphoreCreateMutex();
} }
@@ -297,11 +316,17 @@ void setup() {
} }
void loop() { void loop() {
if (networkOption == UseCellular) {
// Check if cellular client not ready until certain time
// Redundant check in both task to make sure its executed
restartIfCeClientIssueOverTwoHours();
}
// Schedule to feed external watchdog // Schedule to feed external watchdog
watchdogFeedSchedule.run(); watchdogFeedSchedule.run();
if (otaInProgress) { if (firmwareUpdateInProgress) {
// OTA currently in progress, temporarily disable running sensor schedules // Firmare update currently in progress, temporarily disable running sensor schedules
delay(10000); delay(10000);
return; return;
} }
@@ -328,29 +353,38 @@ void loop() {
if (configuration.hasSensorSGP) { if (configuration.hasSensorSGP) {
tvocSchedule.run(); tvocSchedule.run();
} }
if (ag->isOne()) {
if (configuration.hasSensorPMS1) { if (pmsValueTaken < 60) {
ag->pms5003.handle(); if (ag->isOne()) {
static bool pmsConnected = false; if (configuration.hasSensorPMS1) {
if (pmsConnected != ag->pms5003.connected()) { ag->pms5003.handle();
pmsConnected = ag->pms5003.connected(); static bool pmsConnected = false;
Serial.printf("PMS sensor %s ", pmsConnected?"connected":"removed"); if (pmsConnected != ag->pms5003.connected()) {
pmsConnected = ag->pms5003.connected();
Serial.printf("PMS sensor %s \n", pmsConnected?"connected":"removed");
}
}
} else {
if (configuration.hasSensorPMS1) {
ag->pms5003t_1.handle();
}
if (configuration.hasSensorPMS2) {
ag->pms5003t_2.handle();
} }
} }
} else {
if (configuration.hasSensorPMS1) {
ag->pms5003t_1.handle();
}
if (configuration.hasSensorPMS2) {
ag->pms5003t_2.handle();
}
} }
/* Run measurement schedule */
printMeasurementsSchedule.run();
/** factory reset handle */ /** factory reset handle */
factoryConfigReset(); factoryConfigReset();
/** check that local configuration changed then do some action */ if (configuration.isCommandRequested()) {
configUpdateHandle(); // Each state machine already has an independent request command check
stateMachine.executeCo2Calibration();
stateMachine.executeLedBarTest();
}
} }
static void co2Update(void) { static void co2Update(void) {
@@ -367,6 +401,10 @@ static void co2Update(void) {
} }
} }
void printMeasurements() {
measurements.printCurrentAverage();
}
static void mdnsInit(void) { static void mdnsInit(void) {
if (!MDNS.begin(localServer.getHostname().c_str())) { if (!MDNS.begin(localServer.getHostname().c_str())) {
Serial.println("Init mDNS failed"); Serial.println("Init mDNS failed");
@@ -423,6 +461,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();
@@ -525,42 +568,44 @@ static bool sgp41Init(void) {
configuration.hasSensorSGP = true; configuration.hasSensorSGP = true;
return true; return true;
} else { } else {
Serial.println("Init SGP41 failuire"); Serial.println("Init SGP41 failure");
configuration.hasSensorSGP = false; configuration.hasSensorSGP = false;
} }
return false; return false;
} }
void checkForFirmwareUpdate(void) { void checkForFirmwareUpdate(void) {
if (configuration.isCloudConnectionDisabled()) {
Serial.println("Cloud connection is disabled, skip firmware update");
return;
}
AirgradientOTA *agOta; AirgradientOTA *agOta;
if (networkOption == UseWifi) { if (networkOption == UseWifi) {
agOta = new AirgradientOTAWifi; agOta = new AirgradientOTAWifi;
} else { } else {
agOta = new AirgradientOTACellular(cell); agOta = new AirgradientOTACellular(cellularCard, agClient->getICCID());
} }
// Indicate main task that ota is performing // Indicate main task that firmware update is in progress
Serial.println("Check for firmware update, disabling main task"); firmwareUpdateInProgress = true;
otaInProgress = true;
if (configuration.hasSensorSGP && networkOption == UseCellular) {
// Only for cellular because it can disturb i2c line
Serial.println("Disable SGP41 task for cellular OTA");
ag->sgp41.end();
}
agOta->setHandlerCallback(otaHandlerCallback); agOta->setHandlerCallback(otaHandlerCallback);
agOta->updateIfAvailable(ag->deviceId().c_str(), GIT_VERSION);
// Only goes to this line if OTA is not success 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);
}
// Only goes to this line if firmware update is not success
// Handled by otaHandlerCallback // Handled by otaHandlerCallback
otaInProgress = false; // Indicate main task that firmware update finish
if (configuration.hasSensorSGP && networkOption == UseCellular) { firmwareUpdateInProgress = false;
// Re-start SGP41 task
if (!sgp41Init()) {
Serial.println("Failed re-start SGP41 task");
}
}
delete agOta; delete agOta;
Serial.println(); Serial.println();
@@ -568,14 +613,25 @@ void checkForFirmwareUpdate(void) {
void otaHandlerCallback(AirgradientOTA::OtaResult result, const char *msg) { void otaHandlerCallback(AirgradientOTA::OtaResult result, const char *msg) {
switch (result) { switch (result) {
case AirgradientOTA::Starting: case AirgradientOTA::Starting: {
Serial.println("Firmware update starting...");
if (configuration.hasSensorSGP && networkOption == UseCellular) {
// Temporary pause SGP41 task while cellular firmware update is in progress
ag->sgp41.pause();
}
displayExecuteOta(result, fwNewVersion, 0); displayExecuteOta(result, fwNewVersion, 0);
break; break;
}
case AirgradientOTA::InProgress: case AirgradientOTA::InProgress:
Serial.printf("OTA progress: %s\n", msg); Serial.printf("OTA progress: %s\n", msg);
displayExecuteOta(result, "", std::stoi(msg)); displayExecuteOta(result, "", std::stoi(msg));
break; break;
case AirgradientOTA::Failed: case AirgradientOTA::Failed:
displayExecuteOta(result, "", 0);
if (configuration.hasSensorSGP && networkOption == UseCellular) {
ag->sgp41.resume();
}
break;
case AirgradientOTA::Skipped: case AirgradientOTA::Skipped:
case AirgradientOTA::AlreadyUpToDate: case AirgradientOTA::AlreadyUpToDate:
displayExecuteOta(result, "", 0); displayExecuteOta(result, "", 0);
@@ -642,7 +698,11 @@ static void displayExecuteOta(AirgradientOTA::OtaResult result, String msg, int
} }
delay(1000); delay(1000);
} }
oledDisplay.setAirGradient(0);
if (ag->isOne()) {
oledDisplay.setAirGradient(0);
oledDisplay.setBrightness(0);
}
break; break;
} }
default: default:
@@ -674,7 +734,7 @@ static void sendDataToAg() {
"task_led", 2048, NULL, 5, NULL); "task_led", 2048, NULL, 5, NULL);
delay(1500); delay(1500);
// Build payload to check connection to airgradient server // Build payload to check connection to airgradient server
JSONVar root; JSONVar root;
root["wifi"] = wifiConnector.RSSI(); root["wifi"] = wifiConnector.RSSI();
@@ -808,8 +868,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");
@@ -903,6 +961,8 @@ static void boardInit(void) {
} else { } else {
Serial.println("Set S8 AbcDays failure"); Serial.println("Set S8 AbcDays failure");
} }
ag->s8.printInformation();
} }
localServer.setFwMode(fwMode); localServer.setFwMode(fwMode);
@@ -921,9 +981,9 @@ void initializeNetwork() {
agSerial->init(GPIO_IIC_RESET); agSerial->init(GPIO_IIC_RESET);
if (agSerial->open()) { if (agSerial->open()) {
Serial.println("Cellular module found"); Serial.println("Cellular module found");
// Initialize cellular module and use cellular as agClient // Initialize cellular module and use cellular as agClient
cell = new CellularModuleA7672XX(agSerial, GPIO_POWER_MODULE_PIN); cellularCard = new CellularModuleA7672XX(agSerial, GPIO_POWER_MODULE_PIN);
agClient = new AirgradientCellularClient(cell); agClient = new AirgradientCellularClient(cellularCard);
networkOption = UseCellular; networkOption = UseCellular;
} else { } else {
Serial.println("Cellular module not available, using wifi"); Serial.println("Cellular module not available, using wifi");
@@ -934,6 +994,19 @@ void initializeNetwork() {
networkOption = UseWifi; 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())) { if (!agClient->begin(ag->deviceId().c_str())) {
oledDisplay.setText("Client", "initialization", "failed"); oledDisplay.setText("Client", "initialization", "failed");
delay(5000); delay(5000);
@@ -943,12 +1016,20 @@ void initializeNetwork() {
ESP.restart(); 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 (networkOption == UseWifi) {
if (!wifiConnector.connect()) { if (!wifiConnector.connect()) {
Serial.println("Cannot initiate wifi connection"); Serial.println("Cannot initiate wifi connection");
return; return;
} }
if (!wifiConnector.isConnected()) { if (!wifiConnector.isConnected()) {
Serial.println("Failed connect to WiFi"); Serial.println("Failed connect to WiFi");
if (wifiConnector.isConfigurePorttalTimeout()) { if (wifiConnector.isConfigurePorttalTimeout()) {
@@ -957,26 +1038,33 @@ void initializeNetwork() {
oledDisplay.setText("", "", ""); oledDisplay.setText("", "", "");
ESP.restart(); ESP.restart();
} }
// Directly return because the rest of the function applied if wifi is connect only // Directly return because the rest of the function applied if wifi is connect only
return; return;
} }
// Initiate local network configuration // Initiate local network configuration
mdnsInit(); mdnsInit();
localServer.begin(); localServer.begin();
// Apply mqtt connection if configured // Apply mqtt connection if configured
initMqtt(); initMqtt();
// Ignore the rest if cloud connection to AirGradient is disabled // Ignore the rest if cloud connection to AirGradient is disabled
if (configuration.isCloudConnectionDisabled()) { if (configuration.isCloudConnectionDisabled()) {
return; return;
} }
// Send data for the first time to AG server at boot // Send data for the first time to AG server at boot only if postDataToAirgradient is enabled
sendDataToAg(); if (configuration.isPostDataToAirGradient()) {
sendDataToAg();
}
} }
// Skip fetch configuration if configuration control is set to "local" only
if (configuration.getConfigurationControl() == ConfigurationControl::ConfigurationControlLocal) {
ledBarEnabledUpdate();
return;
}
std::string config = agClient->httpFetchConfig(); std::string config = agClient->httpFetchConfig();
configSchedule.update(); configSchedule.update();
@@ -1008,8 +1096,8 @@ static void configurationUpdateSchedule(void) {
} }
std::string config = agClient->httpFetchConfig(); std::string config = agClient->httpFetchConfig();
if (agClient->isLastFetchConfigSucceed() && configuration.parse(config.c_str(), false)) { if (agClient->isLastFetchConfigSucceed()) {
configUpdateHandle(); configuration.parse(config.c_str(), false);
} }
} }
@@ -1018,14 +1106,22 @@ static void configUpdateHandle() {
return; return;
} }
stateMachine.executeCo2Calibration();
String mqttUri = configuration.getMqttBrokerUri(); String mqttUri = configuration.getMqttBrokerUri();
if (mqttClient.isCurrentUri(mqttUri) == false) { if (mqttClient.isCurrentUri(mqttUri) == false) {
mqttClient.end(); mqttClient.end();
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()) {
@@ -1081,11 +1177,6 @@ static void configUpdateHandle() {
if (configuration.isDisplayBrightnessChanged()) { if (configuration.isDisplayBrightnessChanged()) {
oledDisplay.setBrightness(configuration.getDisplayBrightness()); oledDisplay.setBrightness(configuration.getDisplayBrightness());
} }
stateMachine.executeLedBarTest();
}
else if(ag->isOpenAir()) {
stateMachine.executeLedBarTest();
} }
// Update display and led bar notification based on updated configuration // Update display and led bar notification based on updated configuration
@@ -1122,7 +1213,7 @@ static void updateDisplayAndLedBar(void) {
} }
if (configuration.isCloudConnectionDisabled()) { if (configuration.isCloudConnectionDisabled()) {
// Ignore API related check since cloud is disabled // Ignore API related check since cloud is disabled
stateMachine.displayHandle(AgStateMachineNormal); stateMachine.displayHandle(AgStateMachineNormal);
stateMachine.handleLeds(AgStateMachineNormal); stateMachine.handleLeds(AgStateMachineNormal);
return; return;
@@ -1157,6 +1248,16 @@ static void updateTvoc(void) {
} }
static void updatePMS5003() { static void updatePMS5003() {
pmsValueTaken++;
if (pmsValueTaken >= 60) {
if (pmsValueTaken == 60) {
ag->pms5003.sleep();
Serial.println("PMS go sleep");
}
return;
}
if (ag->pms5003.connected()) { if (ag->pms5003.connected()) {
measurements.update(Measurements::PM01, ag->pms5003.getPm01Ae()); measurements.update(Measurements::PM01, ag->pms5003.getPm01Ae());
measurements.update(Measurements::PM25, ag->pms5003.getPm25Ae()); measurements.update(Measurements::PM25, ag->pms5003.getPm25Ae());
@@ -1309,10 +1410,13 @@ void postUsingWifi() {
Serial.printf("Free heap: %u\n", ESP.getFreeHeap()); Serial.printf("Free heap: %u\n", ESP.getFreeHeap());
} }
void postUsingCellular() { /**
* forcePost to force post without checking transmit cycle
*/
void postUsingCellular(bool forcePost) {
// Aquire queue mutex to get queue size // Aquire queue mutex to get queue size
xSemaphoreTake(mutexMeasurementCycleQueue, portMAX_DELAY); xSemaphoreTake(mutexMeasurementCycleQueue, portMAX_DELAY);
// Make sure measurement cycle available // Make sure measurement cycle available
int queueSize = measurementCycleQueue.size(); int queueSize = measurementCycleQueue.size();
if (queueSize == 0) { if (queueSize == 0) {
@@ -1321,6 +1425,14 @@ void postUsingCellular() {
return; 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 // Build payload include all measurements from queue
std::string payload; std::string payload;
payload += std::to_string(CELLULAR_MEASUREMENT_INTERVAL / 1000); // Convert to seconds payload += std::to_string(CELLULAR_MEASUREMENT_INTERVAL / 1000); // Convert to seconds
@@ -1335,20 +1447,24 @@ void postUsingCellular() {
// Attempt to send // Attempt to send
if (agClient->httpPostMeasures(payload) == false) { if (agClient->httpPostMeasures(payload) == false) {
// Consider network has a problem, retry in next schedule // Consider network has a problem, retry in next schedule
Serial.println("Post measures failed, retry in next schedule"); Serial.println("Post measures failed, retry in next schedule");
return; return;
} }
// Post success, remove the data that previously sent from queue // Post success, remove the data that previously sent from queue
xSemaphoreTake(mutexMeasurementCycleQueue, portMAX_DELAY); xSemaphoreTake(mutexMeasurementCycleQueue, portMAX_DELAY);
measurementCycleQueue.erase(measurementCycleQueue.begin(),
measurementCycleQueue.begin() + queueSize);
if (measurementCycleQueue.capacity() > RESERVED_MEASUREMENT_CYCLE_CAPACITY) { if (measurementCycleQueue.capacity() > RESERVED_MEASUREMENT_CYCLE_CAPACITY) {
Serial.println("measurementCycleQueue capacity more than reserved space, resizing.."); Serial.println("measurementCycleQueue capacity more than reserved space, resizing..");
measurementCycleQueue.resize(RESERVED_MEASUREMENT_CYCLE_CAPACITY); std::vector<Measurements::Measures> tmp;
tmp.reserve(RESERVED_MEASUREMENT_CYCLE_CAPACITY);
measurementCycleQueue.swap(tmp);
} else {
// If not more than the capacity, then just clear all the values
measurementCycleQueue.clear();
} }
xSemaphoreGive(mutexMeasurementCycleQueue); xSemaphoreGive(mutexMeasurementCycleQueue);
} }
@@ -1362,8 +1478,13 @@ void sendDataToServer(void) {
if (networkOption == UseWifi) { if (networkOption == UseWifi) {
postUsingWifi(); postUsingWifi();
} else if (networkOption == UseCellular) { } else if (networkOption == UseCellular) {
postUsingCellular(); postUsingCellular(false);
} }
pmsValueTaken = 0;
ag->pms5003.wakeUp();
ag->pms5003.activeMode();
Serial.println("run PMS again");
} }
static void tempHumUpdate(void) { static void tempHumUpdate(void) {
@@ -1432,12 +1553,8 @@ 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
if (networkOption == UseWifi) { // NOTE: Both network option use the same measurement interval
return (WIFI_MEASUREMENT_INTERVAL - (WIFI_MEASUREMENT_INTERVAL * 0.8)) / updateInterval; return (WIFI_MEASUREMENT_INTERVAL - (WIFI_MEASUREMENT_INTERVAL * 0.8)) / updateInterval;
} else {
// Cellular
return (CELLULAR_MEASUREMENT_INTERVAL - (CELLULAR_MEASUREMENT_INTERVAL * 0.8)) / updateInterval;
}
} }
@@ -1445,44 +1562,75 @@ void networkSignalCheck() {
if (networkOption == UseWifi) { if (networkOption == UseWifi) {
Serial.printf("WiFi RSSI %d\n", wifiConnector.RSSI()); Serial.printf("WiFi RSSI %d\n", wifiConnector.RSSI());
} else if (networkOption == UseCellular) { } else if (networkOption == UseCellular) {
auto result = cell->retrieveSignal(); auto result = cellularCard->retrieveSignal();
if (result.status != CellReturnStatus::Ok) { if (result.status != CellReturnStatus::Ok) {
agClient->setClientReady(false); agClient->setClientReady(false);
lastCellSignalQuality = 99;
return; return;
} }
// Save last signal quality
lastCellSignalQuality = result.data;
if (result.data == 99) { if (result.data == 99) {
// 99 indicate cellular not attached to network // 99 indicate cellular not attached to network
agClient->setClientReady(false); agClient->setClientReady(false);
return; return;
} }
Serial.printf("Cellular signal strength %d\n", result.data);
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) { void networkingTask(void *args) {
// OTA check on boot // If cloud connection enabled, run first transmission to server at boot
#ifdef ESP8266 if (configuration.isCloudConnectionDisabled() == false) {
// ota not supported // OTA check on boot
#else #ifndef ESP8266
// because cellular it takes too long, watchdog triggered checkForFirmwareUpdate();
checkForFirmwareUpdate(); checkForUpdateSchedule.update();
checkForUpdateSchedule.update();
#endif #endif
// Because cellular interval is longer, needs to send first measures cycle on // Because cellular interval is longer, needs to send first measures cycle on
// boot to indicate that its online // boot to indicate that its online
if (networkOption == UseCellular) { if (networkOption == UseCellular) {
Serial.println("Prepare first measures cycle to send on boot for 20s"); Serial.println("Prepare first measures cycle to send on boot for 20s");
delay(20000); delay(20000);
newMeasurementCycle(); networkSignalCheck();
sendDataToServer(); newMeasurementCycle();
measurementSchedule.update(); postUsingCellular(true);
measurementSchedule.update();
}
// Reset scheduler
configSchedule.update();
transmissionSchedule.update();
} }
// Reset scheduler
configSchedule.update();
transmissionSchedule.update();
while (1) { while (1) {
// Handle reconnection based on mode // Handle reconnection based on mode
if (networkOption == UseWifi) { if (networkOption == UseWifi) {
@@ -1494,28 +1642,59 @@ void networkingTask(void *args) {
} }
else if (networkOption == UseCellular) { else if (networkOption == UseCellular) {
if (agClient->isClientReady() == false) { 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..."); Serial.println("Cellular client not ready, ensuring connection...");
if (agClient->ensureClientConnection() == false) { if (agClient->ensureClientConnection(resetModule) == false) {
Serial.println("Cellular client connection not ready, retry in 5s..."); Serial.println("Cellular client connection not ready, retry in 30s...");
delay(5000); delay(30000); // before retry, wait for 30s
continue; 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 connection to AirGradient server disable don't run config and transmission schedule
if (configuration.isCloudConnectionDisabled()) { if (configuration.isCloudConnectionDisabled()) {
delay(1000); delay(1000);
return; continue;
} }
// Run scheduler // Run scheduler
networkSignalCheckSchedule.run(); networkSignalCheckSchedule.run();
configSchedule.run();
transmissionSchedule.run(); transmissionSchedule.run();
configSchedule.run();
checkForUpdateSchedule.run(); checkForUpdateSchedule.run();
delay(1000); delay(50);
} }
vTaskDelete(handleNetworkTask); vTaskDelete(handleNetworkTask);
@@ -1523,13 +1702,16 @@ void networkingTask(void *args) {
void newMeasurementCycle() { void newMeasurementCycle() {
if (xSemaphoreTake(mutexMeasurementCycleQueue, portMAX_DELAY) == pdTRUE) { if (xSemaphoreTake(mutexMeasurementCycleQueue, portMAX_DELAY) == pdTRUE) {
// Make sure queue not overflow // Make sure queue not overflow
if (measurementCycleQueue.size() >= MAXIMUM_MEASUREMENT_CYCLE_QUEUE) { if (measurementCycleQueue.size() >= MAXIMUM_MEASUREMENT_CYCLE_QUEUE) {
// Remove the oldest data from queue if queue reach max // Remove the oldest data from queue if queue reach max
measurementCycleQueue.erase(measurementCycleQueue.begin()); measurementCycleQueue.erase(measurementCycleQueue.begin());
} }
auto mc = measurements.getMeasures(); // Get current measures
auto mc = measurements.getMeasures();
mc.signal = cellularCard->csqToDbm(lastCellSignalQuality); // convert to RSSI
measurementCycleQueue.push_back(mc); measurementCycleQueue.push_back(mc);
Serial.println("New measurement cycle added to queue"); Serial.println("New measurement cycle added to queue");
// Release mutex // Release mutex

View File

@@ -6,8 +6,11 @@ OpenMetrics::OpenMetrics(Measurements &measure, Configuration &config,
OpenMetrics::~OpenMetrics() {} OpenMetrics::~OpenMetrics() {}
void OpenMetrics::setAirGradient(AirGradient *ag, AirgradientClient *client) { void OpenMetrics::setAirGradient(AirGradient *ag) {
this->ag = ag; this->ag = ag;
}
void OpenMetrics::setAirgradientClient(AirgradientClient *client) {
this->agClient = client; this->agClient = client;
} }
@@ -199,14 +202,14 @@ String OpenMetrics::getPayload(void) {
} }
if (utils::isValidNOx(nox)) { if (utils::isValidNOx(nox)) {
add_metric("nox_index", add_metric("nox_index",
"The processed Nitrous Oxide (NOx) index as measured by the " "The processed Nitrogen Oxide (NOx) index as measured by the "
"AirGradient SGP sensor", "AirGradient SGP sensor",
"gauge"); "gauge");
add_metric_point("", String(nox)); add_metric_point("", String(nox));
} }
if (utils::isValidNOx(noxRaw)) { if (utils::isValidNOx(noxRaw)) {
add_metric("nox_raw", add_metric("nox_raw",
"The raw input value to the Nitrous Oxide (NOx) index as " "The raw input value to the Nitrogen Oxide (NOx) index as "
"measured by the AirGradient SGP sensor", "measured by the AirGradient SGP sensor",
"gauge"); "gauge");
add_metric_point("", String(noxRaw)); add_metric_point("", String(noxRaw));

View File

@@ -19,7 +19,8 @@ public:
OpenMetrics(Measurements &measure, Configuration &config, OpenMetrics(Measurements &measure, Configuration &config,
WifiConnector &wifiConnector); WifiConnector &wifiConnector);
~OpenMetrics(); ~OpenMetrics();
void setAirGradient(AirGradient *ag, AirgradientClient *client); 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 version=3.3.9
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,10 +12,10 @@
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 CORE_DEBUG_LEVEL=3 -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 =
aglib=symlink://../arduino aglib=symlink://../arduino
EEPROM EEPROM
WebServer WebServer
@@ -32,7 +32,7 @@ platform = espressif8266
board = d1_mini board = d1_mini
framework = arduino framework = arduino
monitor_speed = 115200 monitor_speed = 115200
lib_deps = lib_deps =
aglib=symlink://../arduino aglib=symlink://../arduino
EEPROM EEPROM
ESP8266HTTPClient ESP8266HTTPClient

View File

@@ -47,7 +47,7 @@ bool AgApiClient::fetchServerConfiguration(void) {
} }
#else #else
HTTPClient client; HTTPClient client;
client.setConnectTimeout(timeoutMs); // Set timeout when establishing connection to server client.setConnectTimeout(timeoutMs); // Set timeout when establishing connection to server
client.setTimeout(timeoutMs); // Timeout when waiting for response from AG server client.setTimeout(timeoutMs); // Timeout when waiting for response from AG server
if (apiRootChanged) { if (apiRootChanged) {
// If apiRoot is changed, assume not using https // If apiRoot is changed, assume not using https
@@ -114,7 +114,7 @@ bool AgApiClient::postToServer(String data) {
} }
#else #else
HTTPClient client; HTTPClient client;
client.setConnectTimeout(timeoutMs); // Set timeout when establishing connection to server client.setConnectTimeout(timeoutMs); // Set timeout when establishing connection to server
client.setTimeout(timeoutMs); // Timeout when waiting for response from AG server client.setTimeout(timeoutMs); // Timeout when waiting for response from AG server
if (apiRootChanged) { if (apiRootChanged) {
// If apiRoot is changed, assume not using https // If apiRoot is changed, assume not using https
@@ -185,13 +185,13 @@ void AgApiClient::setAirGradient(AirGradient *ag) { this->ag = ag; }
/** /**
* @brief Send the package to check the connection with cloud * @brief Send the package to check the connection with cloud
* *
* @param rssi WiFi RSSI * @param rssi WiFi RSSI
* @param bootCount Boot count * @param bootCount Boot count
* @return true Success * @return true Success
* @return false Failure * @return false Failure
*/ */
bool AgApiClient::sendPing(int rssi, int bootCount) { bool AgApiClient::sendPing(int rssi, int bootCount) {
JSONVar root; JSONVar root;
root["wifi"] = rssi; root["wifi"] = rssi;
root["boot"] = bootCount; root["boot"] = bootCount;

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
@@ -112,7 +114,7 @@ PMCorrectionAlgorithm Configuration::matchPmAlgorithm(String algorithm) {
const size_t enumSize = COR_ALGO_PM_SLR_CUSTOM + 1; // Get the actual size of the enum const size_t enumSize = COR_ALGO_PM_SLR_CUSTOM + 1; // Get the actual size of the enum
PMCorrectionAlgorithm result = COR_ALGO_PM_UNKNOWN;; PMCorrectionAlgorithm result = COR_ALGO_PM_UNKNOWN;;
// Loop through enum values // Loop through enum values
for (size_t enumVal = 0; enumVal < enumSize; enumVal++) { for (size_t enumVal = 0; enumVal < enumSize; enumVal++) {
if (algorithm == PM_CORRECTION_ALGORITHM_NAMES[enumVal]) { if (algorithm == PM_CORRECTION_ALGORITHM_NAMES[enumVal]) {
@@ -120,7 +122,7 @@ PMCorrectionAlgorithm Configuration::matchPmAlgorithm(String algorithm) {
} }
} }
// If string not match from enum, check if correctionAlgorithm is one of the PM batch corrections // If string not match from enum, check if correctionAlgorithm is one of the PM batch corrections
if (result == COR_ALGO_PM_UNKNOWN) { if (result == COR_ALGO_PM_UNKNOWN) {
// Check the substring "slr_PMS5003_xxxxxxxx" // Check the substring "slr_PMS5003_xxxxxxxx"
if (algorithm.substring(0, 11) == "slr_PMS5003") { if (algorithm.substring(0, 11) == "slr_PMS5003") {
@@ -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;
@@ -453,6 +456,10 @@ bool Configuration::begin(void) {
return true; return true;
} }
void Configuration::setConfigurationUpdatedCallback(ConfigurationUpdatedCallback_t callback) {
_callback = callback;
}
/** /**
* @brief Parse JSON configura string to local configure * @brief Parse JSON configura string to local configure
* *
@@ -735,11 +742,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 +761,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];
@@ -916,15 +955,18 @@ bool Configuration::parse(String data, bool isLocal) {
changed = true; changed = true;
} }
if (ledBarTestRequested || co2CalibrationRequested) {
commandRequested = true;
updated = true;
}
if (changed) { if (changed) {
updated = true; updated = true;
saveConfig(); saveConfig();
printConfig(); printConfig();
} else { _callback();
if (ledBarTestRequested || co2CalibrationRequested) {
updated = true;
}
} }
return true; return true;
} }
@@ -1030,6 +1072,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
* *
@@ -1114,8 +1166,14 @@ bool Configuration::isUpdated(void) {
return updated; return updated;
} }
bool Configuration::isCommandRequested(void) {
bool oldState = this->commandRequested;
this->commandRequested = false;
return oldState;
}
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 +1327,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;
@@ -1492,13 +1562,13 @@ void Configuration::setDisableCloudConnection(bool disable) {
saveConfig(); saveConfig();
} }
bool Configuration::isLedBarModeChanged(void) { bool Configuration::isLedBarModeChanged(void) {
bool changed = _ledBarModeChanged; bool changed = _ledBarModeChanged;
_ledBarModeChanged = false; _ledBarModeChanged = false;
return changed; return changed;
} }
bool Configuration::isMonitorDisplayCompensatedValues(void) { bool Configuration::isMonitorDisplayCompensatedValues(void) {
return jconfig[jprop_monitorDisplayCompensatedValues]; return jconfig[jprop_monitorDisplayCompensatedValues];
} }
@@ -1521,8 +1591,8 @@ bool Configuration::isPMCorrectionChanged(void) {
} }
/** /**
* @brief Check if PM correction is enabled * @brief Check if PM correction is enabled
* *
* @return true if PM correction algorithm is not None, otherwise false * @return true if PM correction algorithm is not None, otherwise false
*/ */
bool Configuration::isPMCorrectionEnabled(void) { bool Configuration::isPMCorrectionEnabled(void) {

View File

@@ -28,6 +28,7 @@ private:
bool co2CalibrationRequested; bool co2CalibrationRequested;
bool ledBarTestRequested; bool ledBarTestRequested;
bool updated; bool updated;
bool commandRequested = false;
String failedMessage; String failedMessage;
bool _noxLearnOffsetChanged; bool _noxLearnOffsetChanged;
bool _tvocLearningOffsetChanged; bool _tvocLearningOffsetChanged;
@@ -70,6 +71,9 @@ public:
bool hasSensorSGP = true; bool hasSensorSGP = true;
bool hasSensorSHT = true; bool hasSensorSHT = true;
typedef void (*ConfigurationUpdatedCallback_t)();
void setConfigurationUpdatedCallback(ConfigurationUpdatedCallback_t callback);
bool begin(void); bool begin(void);
bool parse(String data, bool isLocal); bool parse(String data, bool isLocal);
String toString(void); String toString(void);
@@ -82,6 +86,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);
@@ -89,6 +94,7 @@ public:
void reset(void); void reset(void);
String getModel(void); String getModel(void);
bool isUpdated(void); bool isUpdated(void);
bool isCommandRequested(void);
String getFailedMesage(void); String getFailedMesage(void);
void setPostToAirGradient(bool enable); void setPostToAirGradient(bool enable);
bool noxLearnOffsetChanged(void); bool noxLearnOffsetChanged(void);
@@ -115,6 +121,8 @@ public:
PMCorrection getPMCorrection(void); PMCorrection getPMCorrection(void);
TempHumCorrection getTempCorrection(void); TempHumCorrection getTempCorrection(void);
TempHumCorrection getHumCorrection(void); TempHumCorrection getHumCorrection(void);
private:
ConfigurationUpdatedCallback_t _callback;
}; };
#endif /** _AG_CONFIG_H_ */ #endif /** _AG_CONFIG_H_ */

View File

@@ -8,8 +8,8 @@ AgSchedule::~AgSchedule() {}
void AgSchedule::run(void) { void AgSchedule::run(void) {
uint32_t ms = (uint32_t)(millis() - count); uint32_t ms = (uint32_t)(millis() - count);
if (ms >= period) { if (ms >= period) {
handler();
count = millis(); count = millis();
handler();
} }
} }

View File

@@ -11,8 +11,8 @@
#define RGB_COLOR_R 255, 0, 0 /** Red */ #define RGB_COLOR_R 255, 0, 0 /** Red */
#define RGB_COLOR_G 0, 255, 0 /** Green */ #define RGB_COLOR_G 0, 255, 0 /** Green */
#define RGB_COLOR_Y 255, 150, 0 /** Yellow */ #define RGB_COLOR_Y 255, 255, 0 /** Yellow */
#define RGB_COLOR_O 255, 40, 0 /** Orange */ #define RGB_COLOR_O 255, 128, 0 /** Orange */
#define RGB_COLOR_P 180, 0, 255 /** Purple */ #define RGB_COLOR_P 180, 0, 255 /** Purple */
#define RGB_COLOR_CLEAR 0, 0, 0 /** No color */ #define RGB_COLOR_CLEAR 0, 0, 0 /** No color */

View File

@@ -5,7 +5,7 @@
#include <cmath> #include <cmath>
#include <sstream> #include <sstream>
#define json_prop_pmFirmware "firmware" #define json_prop_pmFirmware "firmware"
#define json_prop_pm01Ae "pm01" #define json_prop_pm01Ae "pm01"
#define json_prop_pm25Ae "pm02" #define json_prop_pm25Ae "pm02"
#define json_prop_pm10Ae "pm10" #define json_prop_pm10Ae "pm10"
@@ -33,7 +33,7 @@ Measurements::Measurements(Configuration &config) : config(config) {
#ifndef ESP8266 #ifndef ESP8266
_resetReason = (int)ESP_RST_UNKNOWN; _resetReason = (int)ESP_RST_UNKNOWN;
#endif #endif
/* Set invalid value for each measurements as default value when initialized*/ /* Set invalid value for each measurements as default value when initialized*/
_temperature[0].update.avg = utils::getInvalidTemperature(); _temperature[0].update.avg = utils::getInvalidTemperature();
_temperature[1].update.avg = utils::getInvalidTemperature(); _temperature[1].update.avg = utils::getInvalidTemperature();
@@ -51,7 +51,7 @@ Measurements::Measurements(Configuration &config) : config(config) {
_pm_05_pc[1].update.avg = utils::getInvalidPmValue(); _pm_05_pc[1].update.avg = utils::getInvalidPmValue();
_pm_5_pc[0].update.avg = utils::getInvalidPmValue(); _pm_5_pc[0].update.avg = utils::getInvalidPmValue();
_pm_5_pc[1].update.avg = utils::getInvalidPmValue(); _pm_5_pc[1].update.avg = utils::getInvalidPmValue();
_pm_01[0].update.avg = utils::getInvalidPmValue(); _pm_01[0].update.avg = utils::getInvalidPmValue();
_pm_01_sp[0].update.avg = utils::getInvalidPmValue(); _pm_01_sp[0].update.avg = utils::getInvalidPmValue();
_pm_01_pc[0].update.avg = utils::getInvalidPmValue(); _pm_01_pc[0].update.avg = utils::getInvalidPmValue();
@@ -76,6 +76,86 @@ Measurements::Measurements(Configuration &config) : config(config) {
void Measurements::setAirGradient(AirGradient *ag) { this->ag = ag; } void Measurements::setAirGradient(AirGradient *ag) { this->ag = ag; }
void Measurements::printCurrentAverage() {
Serial.println();
if (config.hasSensorS8) {
if (utils::isValidCO2(_co2.update.avg)) {
Serial.printf("CO2 = %.2f ppm\n", _co2.update.avg);
} else {
Serial.printf("CO2 = -\n");
}
}
if (config.hasSensorSHT) {
if (utils::isValidTemperature(_temperature[0].update.avg)) {
Serial.printf("Temperature = %.2f C\n", _temperature[0].update.avg);
} else {
Serial.printf("Temperature = -\n");
}
if (utils::isValidHumidity(_humidity[0].update.avg)) {
Serial.printf("Relative Humidity = %.2f\n", _humidity[0].update.avg);
} else {
Serial.printf("Relative Humidity = -\n");
}
}
if (config.hasSensorSGP) {
if (utils::isValidVOC(_tvoc.update.avg)) {
Serial.printf("TVOC Index = %.1f\n", _tvoc.update.avg);
} else {
Serial.printf("TVOC Index = -\n");
}
if (utils::isValidVOC(_tvoc_raw.update.avg)) {
Serial.printf("TVOC Raw = %.1f\n", _tvoc_raw.update.avg);
} else {
Serial.printf("TVOC Raw = -\n");
}
if (utils::isValidNOx(_nox.update.avg)) {
Serial.printf("NOx Index = %.1f\n", _nox.update.avg);
} else {
Serial.printf("NOx Index = -\n");
}
if (utils::isValidNOx(_nox_raw.update.avg)) {
Serial.printf("NOx Raw = %.1f\n", _nox_raw.update.avg);
} else {
Serial.printf("NOx Raw = -\n");
}
}
if (config.hasSensorPMS1) {
printCurrentPMAverage(1);
if (!config.hasSensorSHT) {
if (utils::isValidTemperature(_temperature[0].update.avg)) {
Serial.printf("[1] Temperature = %.2f C\n", _temperature[0].update.avg);
} else {
Serial.printf("[1] Temperature = -\n");
}
if (utils::isValidHumidity(_humidity[0].update.avg)) {
Serial.printf("[1] Relative Humidity = %.2f\n", _humidity[0].update.avg);
} else {
Serial.printf("[1] Relative Humidity = -\n");
}
}
}
if (config.hasSensorPMS2) {
printCurrentPMAverage(2);
if (!config.hasSensorSHT) {
if (utils::isValidTemperature(_temperature[1].update.avg)) {
Serial.printf("[2] Temperature = %.2f C\n", _temperature[1].update.avg);
} else {
Serial.printf("[2] Temperature = -\n");
}
if (utils::isValidHumidity(_humidity[1].update.avg)) {
Serial.printf("[2] Relative Humidity = %.2f\n", _humidity[1].update.avg);
} else {
Serial.printf("[2] Relative Humidity = -\n");
}
}
}
Serial.println();
}
void Measurements::maxPeriod(MeasurementType type, int max) { void Measurements::maxPeriod(MeasurementType type, int max) {
switch (type) { switch (type) {
case Temperature: case Temperature:
@@ -488,7 +568,7 @@ float Measurements::getAverage(MeasurementType type, int ch) {
assert(0); assert(0);
} }
return measurementAverage; return measurementAverage;
} }
String Measurements::pms5003FirmwareVersion(int fwCode) { String Measurements::pms5003FirmwareVersion(int fwCode) {
@@ -570,6 +650,77 @@ String Measurements::measurementTypeStr(MeasurementType type) {
return str; return str;
} }
void Measurements::printCurrentPMAverage(int ch) {
int idx = ch - 1;
if (utils::isValidPm(_pm_01[idx].update.avg)) {
Serial.printf("[%d] Atmospheric PM 1.0 = %.2f ug/m3\n", ch, _pm_01[idx].update.avg);
} else {
Serial.printf("[%d] Atmospheric PM 1.0 = -\n", ch);
}
if (utils::isValidPm(_pm_25[idx].update.avg)) {
Serial.printf("[%d] Atmospheric PM 2.5 = %.2f ug/m3\n", ch, _pm_25[idx].update.avg);
} else {
Serial.printf("[%d] Atmospheric PM 2.5 = -\n", ch);
}
if (utils::isValidPm(_pm_10[idx].update.avg)) {
Serial.printf("[%d] Atmospheric PM 10 = %.2f ug/m3\n", ch, _pm_10[idx].update.avg);
} else {
Serial.printf("[%d] Atmospheric PM 10 = -\n", ch);
}
if (utils::isValidPm(_pm_01_sp[idx].update.avg)) {
Serial.printf("[%d] Standard Particle PM 1.0 = %.2f ug/m3\n", ch, _pm_01_sp[idx].update.avg);
} else {
Serial.printf("[%d] Standard Particle PM 1.0 = -\n", ch);
}
if (utils::isValidPm(_pm_25_sp[idx].update.avg)) {
Serial.printf("[%d] Standard Particle PM 2.5 = %.2f ug/m3\n", ch, _pm_25_sp[idx].update.avg);
} else {
Serial.printf("[%d] Standard Particle PM 2.5 = -\n", ch);
}
if (utils::isValidPm(_pm_10_sp[idx].update.avg)) {
Serial.printf("[%d] Standard Particle PM 10 = %.2f ug/m3\n", ch, _pm_10_sp[idx].update.avg);
} else {
Serial.printf("[%d] Standard Particle PM 10 = -\n", ch);
}
if (utils::isValidPm03Count(_pm_03_pc[idx].update.avg)) {
Serial.printf("[%d] Particle Count 0.3 = %.1f\n", ch, _pm_03_pc[idx].update.avg);
} else {
Serial.printf("[%d] Particle Count 0.3 = -\n", ch);
}
if (utils::isValidPm03Count(_pm_05_pc[idx].update.avg)) {
Serial.printf("[%d] Particle Count 0.5 = %.1f\n", ch, _pm_05_pc[idx].update.avg);
} else {
Serial.printf("[%d] Particle Count 0.5 = -\n", ch);
}
if (utils::isValidPm03Count(_pm_01_pc[idx].update.avg)) {
Serial.printf("[%d] Particle Count 1.0 = %.1f\n", ch, _pm_01_pc[idx].update.avg);
} else {
Serial.printf("[%d] Particle Count 1.0 = -\n", ch);
}
if (utils::isValidPm03Count(_pm_25_pc[idx].update.avg)) {
Serial.printf("[%d] Particle Count 2.5 = %.1f\n", ch, _pm_25_pc[idx].update.avg);
} else {
Serial.printf("[%d] Particle Count 2.5 = -\n", ch);
}
if (_pm_5_pc[idx].listValues.empty() == false) {
if (utils::isValidPm03Count(_pm_5_pc[idx].update.avg)) {
Serial.printf("[%d] Particle Count 5.0 = %.1f\n", ch, _pm_5_pc[idx].update.avg);
} else {
Serial.printf("[%d] Particle Count 5.0 = -\n", ch);
}
}
if (_pm_10_pc[idx].listValues.empty() == false) {
if (utils::isValidPm03Count(_pm_10_pc[idx].update.avg)) {
Serial.printf("[%d] Particle Count 10 = %.1f\n", ch, _pm_10_pc[idx].update.avg);
} else {
Serial.printf("[%d] Particle Count 10 = -\n", ch);
}
}
}
void Measurements::validateChannel(int ch) { void Measurements::validateChannel(int ch) {
if (ch != 1 && ch != 2) { if (ch != 1 && ch != 2) {
Serial.printf("ERROR! Channel %d is undefined. Only channel 1 or 2 is the optional value!", ch); Serial.printf("ERROR! Channel %d is undefined. Only channel 1 or 2 is the optional value!", ch);
@@ -744,7 +895,7 @@ std::string Measurements::buildMeasuresPayload(Measures &mc) {
oss << ","; oss << ",";
// Temperature // Temperature
if (utils::isValidTemperature(mc.temperature[0]) && utils::isValidTemperature(mc.temperature[1])) { if (utils::isValidTemperature(mc.temperature[0]) && utils::isValidTemperature(mc.temperature[1])) {
float temp = (mc.temperature[0] + mc.temperature[1]) / 2.0f; float temp = (mc.temperature[0] + mc.temperature[1]) / 2.0f;
oss << std::round(temp * 10); oss << std::round(temp * 10);
@@ -804,16 +955,16 @@ std::string Measurements::buildMeasuresPayload(Measures &mc) {
oss << ","; oss << ",";
// NOx // TVOC
if (utils::isValidNOx(mc.nox)) { if (utils::isValidVOC(mc.tvoc)) {
oss << std::round(mc.nox); oss << std::round(mc.tvoc);
} }
oss << ","; oss << ",";
// TVOC // NOx
if (utils::isValidVOC(mc.tvoc)) { if (utils::isValidNOx(mc.nox)) {
oss << std::round(mc.tvoc); oss << std::round(mc.nox);
} }
oss << ","; oss << ",";
@@ -827,9 +978,11 @@ std::string Measurements::buildMeasuresPayload(Measures &mc) {
oss << std::round(mc.pm_03_pc[1]); oss << std::round(mc.pm_03_pc[1]);
} }
// char datapoint[128] = {0}; oss << ",";
// snprintf(datapoint, 128, "%d,%.0f,%.0f,%.0f,%.0f,%.0f,%d,%d,%d", co2, temp * 10,
// hum * 10, pm01 * 10, pm25 * 10, pm10 * 10, tvoc, nox, pm003Count); if (mc.signal < 0) {
oss << mc.signal;
}
return oss.str(); return oss.str();
} }

View File

@@ -88,6 +88,8 @@ public:
PM10_PC, // Particle 10 count PM10_PC, // Particle 10 count
}; };
void printCurrentAverage();
/** /**
* @brief Set each MeasurementType maximum period length for moving average * @brief Set each MeasurementType maximum period length for moving average
* *
@@ -147,7 +149,7 @@ public:
* *
* @param type measurement type that will be retrieve * @param type measurement type that will be retrieve
* @param ch target type value channel * @param ch target type value channel
* @return moving average value of target measurements type * @return moving average value of target measurements type
*/ */
float getAverage(MeasurementType type, int ch = 1); float getAverage(MeasurementType type, int ch = 1);
@@ -258,6 +260,8 @@ private:
*/ */
void validateChannel(int ch); void validateChannel(int ch);
void printCurrentPMAverage(int ch);
JSONVar buildOutdoor(bool localServer, AgFirmwareMode fwMode); JSONVar buildOutdoor(bool localServer, AgFirmwareMode fwMode);
JSONVar buildIndoor(bool localServer); JSONVar buildIndoor(bool localServer);
JSONVar buildPMS(int ch, bool allCh, bool withTempHum, bool compensate); JSONVar buildPMS(int ch, bool allCh, bool withTempHum, bool compensate);

View File

@@ -358,7 +358,7 @@ bool WifiConnector::isConnected(void) { return WiFi.isConnected(); }
* this method * this method
* *
*/ */
void WifiConnector::reset(void) { void WifiConnector::reset(void) {
if(this->wifi == NULL) { if(this->wifi == NULL) {
this->wifi = new WiFiManager(); this->wifi = new WiFiManager();
if(this->wifi == NULL){ if(this->wifi == NULL){
@@ -366,7 +366,7 @@ void WifiConnector::reset(void) {
return; return;
} }
} }
WIFI()->resetSettings(); WIFI()->resetSettings();
} }
/** /**
@@ -406,7 +406,7 @@ bool WifiConnector::isConfigurePorttalTimeout(void) { return connectorTimeout; }
/** /**
* @brief Set wifi connect to default WiFi * @brief Set wifi connect to default WiFi
* *
*/ */
void WifiConnector::setDefault(void) { void WifiConnector::setDefault(void) {
WiFi.begin("airgradient", "cleanair"); WiFi.begin("airgradient", "cleanair");

View File

@@ -1,7 +1,7 @@
#include "AirGradient.h" #include "AirGradient.h"
#ifdef ESP8266 #ifdef ESP8266
#include <ESP8266WiFi.h> #include <ESP8266WiFi.h>
#else #else
#include "WiFi.h" #include "WiFi.h"
#endif #endif
@@ -57,7 +57,7 @@ String AirGradient::getBoardName(void) {
/** /**
* @brief Board Type is ONE_INDOOR * @brief Board Type is ONE_INDOOR
* *
* @return true ONE_INDOOR * @return true ONE_INDOOR
* @return false Other * @return false Other
*/ */
@@ -65,15 +65,15 @@ bool AirGradient::isOne(void) {
return boardType == BoardType::ONE_INDOOR; return boardType == BoardType::ONE_INDOOR;
} }
bool AirGradient::isOpenAir(void) { bool AirGradient::isOpenAir(void) {
return boardType == BoardType::OPEN_AIR_OUTDOOR; return boardType == BoardType::OPEN_AIR_OUTDOOR;
} }
bool AirGradient::isPro4_2(void) { bool AirGradient::isPro4_2(void) {
return boardType == BoardType::DIY_PRO_INDOOR_V4_2; return boardType == BoardType::DIY_PRO_INDOOR_V4_2;
} }
bool AirGradient::isPro3_3(void) { bool AirGradient::isPro3_3(void) {
return boardType == BoardType::DIY_PRO_INDOOR_V3_3; return boardType == BoardType::DIY_PRO_INDOOR_V3_3;
} }

View File

@@ -15,7 +15,7 @@
#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.9-snap"
#endif #endif
@@ -177,9 +177,9 @@ public:
/** /**
* @brief Check that Airgradient object is OPEN_AIR * @brief Check that Airgradient object is OPEN_AIR
* *
* @return true * @return true
* @return false * @return false
*/ */
bool isOpenAir(void); bool isOpenAir(void);

View File

@@ -27,7 +27,7 @@ enum BoardType {
/** /**
* @brief Board definitions * @brief Board definitions
* *
*/ */
struct BoardDef { struct BoardDef {
/** Board Support CO2 SenseS8 */ /** Board Support CO2 SenseS8 */

View File

@@ -6,7 +6,7 @@
/** /**
* @brief The class define how to handle the LED * @brief The class define how to handle the LED
* *
*/ */
class StatusLed { class StatusLed {
public: public:

View File

@@ -260,13 +260,14 @@ bool MqttClient::connect(String id) {
connected = false; connected = false;
if (user.isEmpty()) { if (user.isEmpty()) {
logInfo("Connect without auth"); logInfo("Connect without auth");
if(CLIENT()->connect(id.c_str())) { connected = CLIENT()->connect(id.c_str());
connected = true; } else {
} logInfo("Connect with auth");
return connected; connected = CLIENT()->connect(id.c_str(), user.c_str(), password.c_str());
} }
return CLIENT()->connect(id.c_str(), user.c_str(), password.c_str()); return connected;
} }
void MqttClient::handle(void) { void MqttClient::handle(void) {
if (isBegin == false) { if (isBegin == false) {
return; return;

View File

@@ -21,10 +21,15 @@ bool PMSBase::begin(Stream *stream) {
} }
Serial.printf("cleared %d byte(s)\n", bytesCleared); Serial.printf("cleared %d byte(s)\n", bytesCleared);
uint8_t command[] = {0x42, 0x4D, 0xE4, 0x00, 0x01, 0x01, 0x74};
size_t bytesWritten = stream->write(command, sizeof(command));
Serial.printf("%d byte(s) written\n", bytesWritten);
// explicitly put the sensor into active mode, this seems to be be needed for the Cubic PM2009X // explicitly put the sensor into active mode, this seems to be be needed for the Cubic PM2009X
Serial.printf("setting active mode\n"); Serial.printf("setting active mode\n");
uint8_t activeModeCommand[] = { 0x42, 0x4D, 0xE1, 0x00, 0x01, 0x01, 0x71 }; uint8_t activeModeCommand[] = {0x42, 0x4D, 0xE1, 0x00, 0x01, 0x01, 0x71};
size_t bytesWritten = stream->write(activeModeCommand, sizeof(activeModeCommand)); bytesWritten = stream->write(activeModeCommand, sizeof(activeModeCommand));
Serial.printf("%d byte(s) written\n", bytesWritten); Serial.printf("%d byte(s) written\n", bytesWritten);
// Run and check sensor data for 4sec // Run and check sensor data for 4sec
@@ -314,12 +319,11 @@ int PMSBase::pm25ToAQI(int pm02) {
return 500; return 500;
} }
/** /**
* @brief SLR correction for PM2.5 * @brief SLR correction for PM2.5
* *
* Reference: https://www.airgradient.com/blog/low-readings-from-pms5003/ * Reference: https://www.airgradient.com/blog/low-readings-from-pms5003/
* *
* @param pm25 PM2.5 raw value * @param pm25 PM2.5 raw value
* @param pm003Count PM0.3 count * @param pm003Count PM0.3 count
* @param scalingFactor Scaling factor * @param scalingFactor Scaling factor

View File

@@ -35,7 +35,7 @@ public:
/** For PMS5003T*/ /** For PMS5003T*/
int16_t getTemp(void); int16_t getTemp(void);
uint16_t getHum(void); uint16_t getHum(void);
uint8_t getFirmwareVersion(void); uint8_t getFirmwareVersion(void);
uint8_t getErrorCode(void); uint8_t getErrorCode(void);
int pm25ToAQI(int pm02); int pm25ToAQI(int pm02);
@@ -48,7 +48,7 @@ private:
/** In normal package interval is 200-800ms, In case small changed on sensor /** In normal package interval is 200-800ms, In case small changed on sensor
* it's will interval reach to 2.3sec * it's will interval reach to 2.3sec
*/ */
const uint16_t READ_PACKGE_TIMEOUT = 3000; /** ms */ const uint16_t READ_PACKGE_TIMEOUT = 3000; /** ms */
const int failCountMax = 10; const int failCountMax = 10;
int failCount = 0; int failCount = 0;
@@ -76,15 +76,15 @@ private:
uint16_t pms_count2_5; uint16_t pms_count2_5;
uint16_t pms_count5_0; uint16_t pms_count5_0;
uint16_t pms_count10; uint16_t pms_count10;
int16_t pms_temp; int16_t pms_temp;
uint16_t pms_hum; uint16_t pms_hum;
uint8_t pms_errorCode; uint8_t pms_errorCode;
uint8_t pms_firmwareVersion; uint8_t pms_firmwareVersion;
int16_t toI16(const uint8_t *buf); int16_t toI16(const uint8_t *buf);
uint16_t toU16(const uint8_t *buf); uint16_t toU16(const uint8_t *buf);
bool validate(const uint8_t *buf); bool validate(const uint8_t *buf);
void parse(const uint8_t* buf); void parse(const uint8_t *buf);
}; };
#endif /** _PMS5003_BASE_H_ */ #endif /** _PMS5003_BASE_H_ */

View File

@@ -189,21 +189,21 @@ float PMS5003::compensate(float pm25, float humidity) { return pms.compensate(pm
/** /**
* @brief Get sensor firmware version * @brief Get sensor firmware version
* *
* @return int * @return int
*/ */
int PMS5003::getFirmwareVersion(void) { return _ver; } int PMS5003::getFirmwareVersion(void) { return _ver; }
/** /**
* @brief Get sensor error code * @brief Get sensor error code
* *
* @return uint8_t * @return uint8_t
*/ */
uint8_t PMS5003::getErrorCode(void) { return pms.getErrorCode(); } uint8_t PMS5003::getErrorCode(void) { return pms.getErrorCode(); }
/** /**
* @brief Is sensor connect with device * @brief Is sensor connect with device
* *
* @return true Connected * @return true Connected
* @return false Removed * @return false Removed
*/ */
@@ -255,14 +255,55 @@ void PMS5003::resetFailCount(void) {
/** /**
* @brief Get number of fail count * @brief Get number of fail count
* *
* @return int * @return int
*/ */
int PMS5003::getFailCount(void) { return pms.getFailCount(); } int PMS5003::getFailCount(void) { return pms.getFailCount(); }
/** /**
* @brief Get number of fail count max * @brief Get number of fail count max
* *
* @return int * @return int
*/ */
int PMS5003::getFailCountMax(void) { return pms.getFailCountMax(); } int PMS5003::getFailCountMax(void) { return pms.getFailCountMax(); }
// Standby mode. For low power consumption and prolong the life of the sensor.
void PMS5003::sleep() {
uint8_t command[] = {0x42, 0x4D, 0xE4, 0x00, 0x00, 0x01, 0x73};
size_t bytesWritten = this->_serial->write(command, sizeof(command));
Serial.printf("%d byte(s) written\n", bytesWritten);
}
// Operating mode. Stable data should be got at least 30 seconds after the sensor wakeup from the sleep mode because of the fan's performance.
void PMS5003::wakeUp() {
uint8_t command[] = {0x42, 0x4D, 0xE4, 0x00, 0x01, 0x01, 0x74};
size_t bytesWritten = this->_serial->write(command, sizeof(command));
Serial.printf("%d byte(s) written\n", bytesWritten);
}
// Active mode. Default mode after power up. In this mode sensor would send serial data to the host automatically.
void PMS5003::activeMode() {
uint8_t command[] = {0x42, 0x4D, 0xE1, 0x00, 0x01, 0x01, 0x71};
size_t bytesWritten = this->_serial->write(command, sizeof(command));
Serial.printf("%d byte(s) written\n", bytesWritten);
// _mode = MODE_ACTIVE;
}
// Passive mode. In this mode sensor would send serial data to the host only for request.
void PMS5003::passiveMode() {
uint8_t command[] = {0x42, 0x4D, 0xE1, 0x00, 0x00, 0x01, 0x70};
size_t bytesWritten = this->_serial->write(command, sizeof(command));
Serial.printf("%d byte(s) written\n", bytesWritten);
// _mode = MODE_PASSIVE;
}
// Request read in Passive Mode.
void PMS5003::requestRead() {
// if (_mode == MODE_PASSIVE)
// {
uint8_t command[] = {0x42, 0x4D, 0xE2, 0x00, 0x00, 0x01, 0x71};
size_t bytesWritten = this->_serial->write(command, sizeof(command));
Serial.printf("%d byte(s) written\n", bytesWritten);
// }
}

View File

@@ -19,6 +19,14 @@ public:
#else #else
bool begin(HardwareSerial &serial); bool begin(HardwareSerial &serial);
#endif #endif
// Modes
void sleep();
void wakeUp();
void activeMode();
void passiveMode();
void requestRead();
void end(void); void end(void);
void handle(void); void handle(void);
void updateFailCount(void); void updateFailCount(void);

View File

@@ -218,21 +218,21 @@ float PMS5003T::compensate(float pm25, float humidity) { return pms.compensate(p
/** /**
* @brief Get module(s) firmware version * @brief Get module(s) firmware version
* *
* @return int Version code * @return int Version code
*/ */
int PMS5003T::getFirmwareVersion(void) { return _ver; } int PMS5003T::getFirmwareVersion(void) { return _ver; }
/** /**
* @brief Get sensor error code * @brief Get sensor error code
* *
* @return uint8_t * @return uint8_t
*/ */
uint8_t PMS5003T::getErrorCode(void) { return pms.getErrorCode(); } uint8_t PMS5003T::getErrorCode(void) { return pms.getErrorCode(); }
/** /**
* @brief Is sensor connect to device * @brief Is sensor connect to device
* *
* @return true Connected * @return true Connected
* @return false Removed * @return false Removed
*/ */
@@ -281,14 +281,14 @@ void PMS5003T::resetFailCount(void) {
/** /**
* @brief Get fail count * @brief Get fail count
* *
* @return int * @return int
*/ */
int PMS5003T::getFailCount(void) { return pms.getFailCount(); } int PMS5003T::getFailCount(void) { return pms.getFailCount(); }
/** /**
* @brief Get fail count max * @brief Get fail count max
* *
* @return int * @return int
*/ */
int PMS5003T::getFailCountMax(void) { return pms.getFailCountMax(); } int PMS5003T::getFailCountMax(void) { return pms.getFailCountMax(); }

View File

@@ -6,11 +6,11 @@ PMS5003TBase::~PMS5003TBase() {}
/** /**
* @brief Compensate the temperature * @brief Compensate the temperature
* *
* Reference formula: https://www.airgradient.com/documentation/correction-algorithms/ * Reference formula: https://www.airgradient.com/documentation/correction-algorithms/
* *
* @param temp * @param temp
* @return * float * @return * float
*/ */
float PMS5003TBase::compensateTemp(float temp) { float PMS5003TBase::compensateTemp(float temp) {
if (temp < 10.0f) { if (temp < 10.0f) {
@@ -21,11 +21,11 @@ float PMS5003TBase::compensateTemp(float temp) {
/** /**
* @brief Compensate the humidity * @brief Compensate the humidity
* *
* Reference formula: https://www.airgradient.com/documentation/correction-algorithms/ * Reference formula: https://www.airgradient.com/documentation/correction-algorithms/
* *
* @param temp * @param temp
* @return * float * @return * float
*/ */
float PMS5003TBase::compensateHum(float hum) { float PMS5003TBase::compensateHum(float hum) {
hum = hum * 1.259f + 7.34f; hum = hum * 1.259f + 7.34f;

View File

@@ -4,7 +4,7 @@
class PMS5003TBase class PMS5003TBase
{ {
private: private:
public: public:
PMS5003TBase(); PMS5003TBase();
~PMS5003TBase(); ~PMS5003TBase();

View File

@@ -835,3 +835,13 @@ bool S8::setAbcPeriod(int hours) {
* @return int Hour * @return int Hour
*/ */
int S8::getAbcPeriod(void) { return getCalibPeriodABC(); } int S8::getAbcPeriod(void) { return getCalibPeriodABC(); }
void S8::printInformation(void) {
Serial.print("S8 type ID: 0x");
Serial.println(getSensorTypeId(), HEX);
Serial.print("S8 serial number: 0x");
Serial.println(getSensorId(), HEX);
Serial.print("S8 memory map version: 0x");
Serial.println(getMemoryMapVersion(), HEX);
}

View File

@@ -80,6 +80,7 @@ public:
bool isBaseLineCalibrationDone(void); bool isBaseLineCalibrationDone(void);
bool setAbcPeriod(int hours); bool setAbcPeriod(int hours);
int getAbcPeriod(void); int getAbcPeriod(void);
void printInformation(void);
private: private:
/** Variables */ /** Variables */

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;