Compare commits

...

426 Commits
3.0.3 ... 3.1.5

Author SHA1 Message Date
b1aaa04421 update GIT_BUILD version 2024-08-23 09:12:43 +07:00
186f0d27ab Update version 2024-08-23 08:59:28 +07:00
e25aa87ecc Merge pull request #215 from airgradienthq/develop
Develop
2024-08-23 08:50:04 +07:00
1cc8941a5c csv alignment format 2024-08-22 11:27:25 +07:00
9bf1495be7 Merge branch 'hotfix/firmware-version' into develop 2024-08-22 11:24:26 +07:00
73089b51f5 add firmware version for arduino build 2024-08-22 11:23:53 +07:00
625e60a5bf Merge branch 'hotfix/local-configuration-return-result' into develop 2024-08-21 12:54:47 +07:00
88e3d0bd3f Ignore and return failed if configurationControl is cloud 2024-08-21 12:54:06 +07:00
171821cfcf Merge pull request #214 from airgradienthq/hotfix/api-client-log
API client log post data
2024-08-21 12:28:50 +07:00
900a2da2ac Log POST data 2024-08-21 12:27:56 +07:00
fb57a112c9 Merge pull request #201 from McJoppy/feature/root-api
Reintroduce 'ROOTAPI' so domain and protocol can be configured
2024-08-20 09:25:48 +07:00
ab69b686ec Merge branch 'develop' into feature/root-api 2024-08-20 09:22:36 +07:00
6746d25dc2 Add comment to all example scripts showing how to change APIROOT 2024-08-18 16:46:56 +12:00
be150e105a Merge pull request #204 from airgradienthq/feature/add-http-request-to-ag-log
Add log message HTTP request and response to AG server
2024-08-18 11:37:42 +07:00
ecadeeb156 Merge pull request #209 from airgradienthq/hotfix/json-round-2-decimal-place-support-negative-value
`round2` support negative value
2024-08-18 11:33:39 +07:00
219ff73132 Merge pull request #212 from airgradienthq/feature/correct-pm2.5-formula
correct pm2.5 formula to show value on display
2024-08-18 11:32:50 +07:00
0a9142204d Merge pull request #213 from airgradienthq/hotfix/sensor-value-out-of-range
sensor value out of range
2024-08-18 11:30:27 +07:00
f3a9c722b2 Change pm25Compensated to compensated 2024-08-16 06:39:52 +07:00
3be3218115 Update invalid value and optimize code operator. 2024-08-15 09:11:38 +07:00
5edb21cfe9 Fix: PMS5003T only return positive temperature value 2024-08-15 09:10:48 +07:00
6cd587b008 Merge remote-tracking branch 'origin/develop' into feature/correct-pm2.5-formula 2024-08-15 08:19:10 +07:00
1a347e9cfe Merge pull request #206 from airgradienthq/hotfix/display-show-nox-incorrect-position
`NOx` show incorrect position on display
2024-08-12 09:20:21 +07:00
6432e4451e Merge pull request #210 from airgradienthq/hotfix/set-connect-to-default-wifi-on-factory-reset
Set WiFi connect to default WiFi on factory reset
2024-08-10 07:45:06 +07:00
97f0696002 set default wifi on factory reset. 2024-08-09 13:19:28 +07:00
e46e11c030 round2 support negative value 2024-08-08 05:53:24 +07:00
dc261f668d Update local-server.md II 2024-08-07 11:08:48 +07:00
b5cced40d2 Update local-server.md 2024-08-07 11:05:39 +07:00
b0ae851427 Fix nox position 2024-07-30 20:26:19 +07:00
ed7b8df6fe log URLs of all HTTP requests to AG backend / log status codes of responses 2024-07-29 06:00:54 +07:00
6c1c914716 Merge pull request #196 from airgradienthq/hotfix/led-bar-show-sensor-data-incorrect
Correct LED bar show sensor value level color
2024-07-26 06:16:33 +07:00
6a0d88ff10 Merge pull request #197 from airgradienthq/hotfix/ignore-parameter-out-of-range
Ignore parameter values out of range
2024-07-26 06:12:04 +07:00
9097eed137 [fix] typo comment 2024-07-24 20:31:43 +07:00
c9b5e5f0d7 Merge branch 'develop' into hotfix/ignore-parameter-out-of-range 2024-07-24 20:24:27 +07:00
c12bac4ce3 Update invalid temperature value 2024-07-24 20:19:06 +07:00
9ae9b2ac9c display float value on display with 1 digit 2024-07-24 20:18:48 +07:00
7fb3e68b6d Merge pull request #195 from airgradienthq/hotfix/change-tvoc-to-VOC
Change `tvoc` to `VOC` on display
2024-07-24 09:19:03 +02:00
cf65a1f901 Merge pull request #200 from airgradienthq/feature/doc-quote-properties-name
Update local-server.md
2024-07-24 09:16:09 +02:00
5fb27b6d1e Check value sensor value 2024-07-24 09:05:57 +07:00
7b9dac756b Reintroduce 'ROOTAPI' so domain and protocol can be configured
eg. setter for api root added as comment in examples/BASIC/BASIC.ino
2024-07-21 19:38:50 +12:00
4b2a5f5540 Add PM2.5 correction formula, #182 2024-07-21 07:13:34 +07:00
4af77d532e Update local-server.md
Quote properties name
2024-07-20 09:39:29 +07:00
0ece16f434 Update screen layout, #139 2024-07-18 11:28:12 +07:00
df6cca3714 Ignore parameter values out of range #190 2024-07-11 06:25:31 +07:00
c8aa07ae20 Correct LED bar show sensor value level color, #161 2024-07-09 06:44:04 +07:00
a1d216ac77 Change tvoc to VOC, #139 2024-07-09 06:10:07 +07:00
a9d9c60dfa Merge branch 'develop' 2024-06-30 07:23:06 +07:00
e58ce1cbea Upgraded version number for Arduino library manager 2024-06-29 15:08:36 +07:00
64827223ec Merge pull request #191 from airgradienthq/feature/add-esp8266-examples
Add esp8266 examples
2024-06-29 15:03:56 +07:00
bddd4fef25 Update BASIC CO2 calibration display message. 2024-06-25 17:32:19 +07:00
0eba54fd28 Remove don't use code 2024-06-25 17:20:04 +07:00
a4176f966a Update board name comment 2024-06-25 17:13:18 +07:00
2de54ca6a6 update platformio.ini 2024-06-25 16:54:42 +07:00
bfd20a73da Rename board type 3.7 to 3.3 2024-06-25 16:43:50 +07:00
3ebce4ac44 Update firmware model name 2024-06-25 16:29:21 +07:00
f08c8edd19 Rename example 2024-06-25 16:27:35 +07:00
6b3e8e3096 Update default configuration json for special board 2024-06-25 16:24:12 +07:00
beb17de7dc [Fix] WiFi reset failed and crash in offline mode 2024-06-25 16:08:57 +07:00
bccadd17d7 [Fix] Wrong firmware model name 2024-06-25 15:35:18 +07:00
863b1f908e Merge commit '5a2c1bd1d0290e5cae0b8dec02eb2a931211d1af' into feature/add-esp8266-examples 2024-06-25 15:19:28 +07:00
5a2c1bd1d0 Merge pull request #175 from airgradienthq/fix/local-configuration-response-result
[Fix] Local `configurationControl` return failed if set `cloud`
2024-06-25 15:18:49 +07:00
dbc63194e6 Update BASIC.ino example 2024-06-24 18:34:24 +07:00
57c33e4900 check to handle sgp sensor 2024-06-24 18:33:46 +07:00
48f1a8042a Fixed FW_MODE_I_37PS 2024-06-22 13:47:13 +07:00
fbd5779fe6 platformio.ini update 2024-06-20 11:36:07 +07:00
45a4d98267 Fix wrong firmware mode 2024-06-20 11:34:43 +07:00
0119a4d62a Add support board DIY Pro 3.7 and add example 2024-06-20 11:34:23 +07:00
7560251deb Update mqtt print log message 2024-06-20 10:12:37 +07:00
31655b0a4f [Fix] PCB 4.3: Boot loop if not connected to Dashboard, #186 2024-06-20 10:12:19 +07:00
7d15c37ad9 Update firmware mode, fix #185 2024-06-20 09:34:25 +07:00
411beda220 Update tvocRaw and noxRaw, fix #184 2024-06-20 09:03:21 +07:00
0e869d1c0c Update README.md 2024-06-19 15:18:51 +07:00
f25f816428 Add MQTT 2024-06-19 15:17:48 +07:00
c8745e123b Update board support handle and fix connect timeout handle. 2024-06-19 10:33:02 +07:00
5ea01b8e9f [Fix] WiFi reset and set connect to factory wifi 2024-06-18 20:35:50 +07:00
a11eaea532 [fix] build fail on example BASIC 2024-06-18 19:59:29 +07:00
3b6859f483 Updated documentation for local server 2024-06-17 08:46:56 +07:00
bd237ed95d Add DiyProIndoorV4_2.ino example 2024-06-15 15:40:50 +07:00
d24b20a734 Merge pull request #177 from airgradienthq/feature/factory-reset-open-air-connect-to-airgradient-wifi
OpenAir set WiFi connect to `airgradient` on factory reset
2024-06-14 09:38:21 +07:00
227a4f76f7 Merge pull request #176 from airgradienthq/fix/display-show-configureation-failed-status
Fix display message is not correct.
2024-06-12 06:07:01 +07:00
adabb9baa4 OpenAir set WiFi connect to airgradient on factory reset 2024-06-12 06:04:13 +07:00
935e7f365f Fix display message is not correct. 2024-06-10 22:04:23 +07:00
d4ea03f39e Handle configurationControl set cloud return success 2024-06-08 12:28:17 +07:00
89aefdda43 fix type 2024-06-08 12:01:42 +07:00
8a83e408d3 Merge pull request #173 from airgradienthq/develop
Next version 3.1.3
2024-06-06 13:17:08 +07:00
7a1a0337d1 Merge pull request #163 from joostlek/typo
Fix typos in docs
2024-06-06 13:13:00 +07:00
6bdd4cb02f Merge pull request #172 from airgradienthq/hotfix/display-show-rebooting-but-not-rebooting
[Fix] Reboot device after WiFi Configure porttal timeout
2024-06-06 13:11:33 +07:00
38bd758b69 Merge pull request #171 from airgradienthq/feature/update-display-message
Update the message show on display
2024-06-06 13:11:19 +07:00
6aa19ea3e6 Set next version 2024-06-06 08:44:12 +07:00
da323b1a46 fix reboot device after WiFi portal configure timeout. 2024-06-05 19:01:25 +07:00
2ae90444bb Fix reboot device after WiFi Connector perform but not connected 2024-06-05 14:31:34 +07:00
21e802da33 Merge pull request #170 from airgradienthq/feature/regularly-call-ota-handler
OTA update each 60 min
2024-06-05 13:38:05 +07:00
e0869fbaf0 Merge pull request #167 from airgradienthq/hotfix/too-many-get-config-request
[Fix] Too many server GET config request
2024-06-05 13:24:59 +07:00
70662091ec Merge pull request #166 from airgradienthq/hotfix/wifi-not-reset-in-offline-mode
[Fix] WiFi not reset on offline mode
2024-06-05 13:24:44 +07:00
960d2bad64 Merge pull request #169 from airgradienthq/feature/add-boott-support-ha
Add bootCount to support on HA
2024-06-05 13:22:19 +07:00
7abf7f5e6a Merge pull request #162 from joostlek/update-put-table
Update configuration parameters
2024-06-05 10:34:03 +07:00
6bad4fd04b Merge pull request #164 from joostlek/json
Reformat JSON in docs
2024-06-05 10:30:56 +07:00
a17f18b3db Merge pull request #165 from joostlek/table
Reformat table
2024-06-05 10:29:59 +07:00
3da4900462 Update show mesage on display 2024-06-05 09:58:08 +07:00
d2d81f6b4b OTA update each 60 min 2024-06-05 09:29:30 +07:00
0b12f56513 add bootCount to measure report 2024-06-04 22:17:20 +07:00
f7e811e34b Update server GET configure period to 60 sec 2024-06-04 18:30:33 +07:00
037bb37184 Change macro name SERVER_CONFIG_UPDATE_INTERVAL' to SERVER_CONFIG_SYNC_INTERVAL` 2024-06-04 18:29:35 +07:00
fde510ba96 Merge pull request #137 from airgradienthq/hotfix/correct-ota-update-message-on-display
Correct OTA process message show on display
2024-06-04 18:15:01 +07:00
14b152a2a7 Merge branch 'develop' into hotfix/correct-ota-update-message-on-display 2024-06-04 18:09:04 +07:00
ef6b041529 Fix: ota not callback on first powerup perform 2024-06-04 18:01:55 +07:00
11ecea1493 Change showFirmwareUpdateSuccess input fromString to int 2024-06-04 18:01:28 +07:00
3e2e8b15eb Fix: WiFi configuration not reset in offline mode. 2024-06-04 15:51:01 +07:00
4249a86fd5 Reformat table 2024-06-04 09:21:29 +02:00
4a58b0b1c7 Reformat JSON in docs 2024-06-04 09:20:18 +02:00
5f93329e96 Fix typos in docs 2024-06-04 09:17:33 +02:00
59d189e1d3 Update configuration parameters 2024-06-04 09:14:30 +02:00
2e62abe2d7 Merge pull request #156 from airgradienthq/develop
Develop
2024-05-29 18:02:27 +07:00
7569b114bf Merge pull request #155 from airgradienthq/feature/update-led-bar-button-test
Handle LED bar test button before init sensor initialize
2024-05-29 16:36:08 +07:00
3c1d0a862f update comment 2024-05-29 16:34:00 +07:00
4d883af77e Merge pull request #138 from airgradienthq/hotfix/ledbar-flickers
Fix LED bar flickers
2024-05-29 08:01:05 +07:00
e9224a5de0 Remove test logging message 2024-05-29 07:58:49 +07:00
af0fbadd80 Handle LED bar button test before init sensor. 2024-05-29 07:55:27 +07:00
79f6c040c7 Merge pull request #143 from airgradienthq/feature/reboot-after-wifi-connect-timeout
Reboot After 180s WiFi Manager not Connect.
2024-05-28 13:43:08 +07:00
f262866148 Merge pull request #154 from airgradienthq/hotfix/display-co2-calibration-message
Update CO2 calibration message show on display
2024-05-28 13:42:14 +07:00
78a2a78020 Update CO2 calibration message show on display 2024-05-25 20:25:50 +07:00
65e759fb90 Merge branch 'develop' into hotfix/correct-ota-update-message-on-display 2024-05-24 13:30:09 +07:00
3fc7e4b55e Merge pull request #149 from airgradienthq/hotfix/reporting-fw-version-on-dashboard
Official OTA 3.1.1 version not reporting FW version on dashboard
2024-05-24 09:30:13 +07:00
5857388c2d Merge pull request #152 from airgradienthq/hotfix/remove-configuration-missleading-message
Remove `Update ignored due to local unofficial changes`
2024-05-24 09:01:04 +07:00
5770b41fd4 remove Update ignored due to local unofficial changes 2024-05-24 08:45:50 +07:00
d85d890878 Change OTA update period from 2h to 1h 2024-05-24 08:26:24 +07:00
9fbd31d0c8 Merge pull request #150 from airgradienthq/hotfix/show-message-monitor-not-setup-on-dashboard
disp.setText("Monitor not", "setup on", "dashboard") called when monitor is actually already on dashboard
2024-05-24 08:23:52 +07:00
c5b7c43293 disp.setText("Monitor not", "setup on", "dashboard") called when monitor is actually already on dashboard 2024-05-22 12:13:57 +07:00
7550ef7b0c change OTA update period each 2h. 2024-05-22 11:45:56 +07:00
805546b78e check firmware update after powerup 2024-05-22 11:34:42 +07:00
59880f4be5 fix typo 2024-05-22 11:17:11 +07:00
ee7837a471 Merge pull request #148 from MallocArray/patch-1
Correct API value for boot
2024-05-20 08:50:57 +07:00
ebbf0adf2f Correct API value for boot 2024-05-18 21:19:40 -05:00
d9551dc560 Reboot After 180s WiFi Manager not Connect. 2024-05-18 08:47:49 +07:00
6ea0ab9272 Set process 100% after received firmware file. 2024-05-17 20:18:59 +07:00
6e54409512 rename function 2024-05-17 20:17:49 +07:00
f35bc4feaa Fix LED bar flickers 2024-05-17 11:52:22 +07:00
c04ab90fd2 Update OTA processing message. Fix always retry of bootCount = 0 2024-05-17 10:32:20 +07:00
ed02f66ca2 Correct OTA update process show message on display 2024-05-16 21:12:02 +07:00
4b94926651 Merge pull request #136 from airgradienthq/develop
merge to master to prepare for release
2024-05-16 16:36:17 +07:00
f505b39247 Merge pull request #135 from airgradienthq/hotfix/pm25-appear-colors
Update PM2.5 LED bar color and level
2024-05-16 13:09:58 +07:00
9e44cd89d9 Update PM2.5 LED bar color and level 2024-05-16 12:50:38 +07:00
e7b95c0bde Merge pull request #126 from airgradienthq/OTA-dev
added doc regarding the OTA mechanism
2024-05-16 11:55:57 +07:00
b72394b004 Merge pull request #127 from airgradienthq/hotfix/purple-color
Hotfix/purple color
2024-05-16 11:55:31 +07:00
1544989fe6 Merge pull request #128 from airgradienthq/hotfix/change-ota-update-period
Change the OTA update period use timestamp to bootCount
2024-05-16 11:54:56 +07:00
d7b5e999c1 Merge pull request #129 from airgradienthq/hotfix/turn-led-off-on-offlinemode
Turn LED indicate wifi failed, cloud failed and get cloud configuration failed to off
2024-05-16 11:54:00 +07:00
9b0210f7b5 Merge pull request #130 from airgradienthq/hotfilx/print-log-wrong-number-format
Fix print number value wrong format
2024-05-16 11:53:16 +07:00
3715266f8c Merge pull request #131 from airgradienthq/hotfix/update-local-configure-repsonse-fail-message
Update local configuration response failed message
2024-05-16 11:52:57 +07:00
a4eb607174 Merge pull request #132 from airgradienthq/hotfix/update-configuration-log-message
Update configuration log message
2024-05-16 11:52:21 +07:00
f55fa6a617 Update configuration log mesasge 2024-05-15 17:20:10 +07:00
4f9f800cce Update local configuration response failed message 2024-05-15 17:15:36 +07:00
cad5d1f0e7 fix print number value wrong format 2024-05-15 17:13:39 +07:00
55ede2b04d turn led indicate wifi failed, cloud fail and get cloud configuration fail to off. 2024-05-15 17:11:26 +07:00
563bdfe4b2 Change the OTA update period use timestamp to bootCount 2024-05-15 17:09:06 +07:00
e2154af85f Merge commit '9a2bbd7a57a7dd4a6eaa27b85576c20a85d7ca09' into hotfix/purple-color 2024-05-15 16:10:08 +07:00
9a2bbd7a57 fix: Purple color 2024-05-15 14:02:49 +07:00
a8c8246632 added doc regarding the OTA mechanism 2024-05-15 11:05:37 +07:00
a71c038864 Fix camelCase, #122 2024-05-13 21:28:37 +07:00
799217e724 Remove displayMode from configuration, #123 2024-05-13 21:25:00 +07:00
b3f02f0a58 Merge pull request #125 from airgradienthq/hotfix/openair-sync-miss-data
Fix: Firmware mode `O-1PP` send miss data channel-2
2024-05-13 20:44:26 +07:00
c640cf773e Merge pull request #124 from airgradienthq/hotfix/configuration-handle
Configuration failed response message and failed condition handle.
2024-05-13 20:39:45 +07:00
c145666fcb Merge pull request #121 from airgradienthq/hotfix/led-bar-power-up-test-wifi-connection-still-perform
Fix issue: LED bar test presssed but WiFi connection still perform
2024-05-13 20:39:26 +07:00
466bb0eb21 Merge pull request #120 from airgradienthq/hotfix/not-show-the-message-add-to-dashboard
Fix issue: dashboard not show if get cloud configuration failed.
2024-05-13 20:39:11 +07:00
4612f4b793 Add condition to handle configuration changed. 2024-05-13 19:00:34 +07:00
45c7279866 Fix: Firmware mode O-1PP send miss data channel-2 2024-05-13 18:25:03 +07:00
5cb838af29 fix: OpenAir send incorrect model(firmware mode) 2024-05-13 18:11:46 +07:00
3201fd8d9c Fix: Configuratoin failed response mesasge and failed condition handle. 2024-05-13 17:43:42 +07:00
f23c7e9e31 Set offline mode incase wifi is not configuraion or configuration ignored. 2024-05-13 15:07:10 +07:00
5b18a8353d Fix issue: LED bar button test pressed but WiFi connection still perform. 2024-05-13 14:43:53 +07:00
1e81c9b125 Fix issue: dashboard not show if get cloud configuration failed. 2024-05-13 13:52:14 +07:00
3dae4cb06d Merge pull request #119 from airgradienthq/hotfix/fix-issue-before-release-new-version
Hotfix: Fix some issue before release new version
2024-05-13 12:26:53 +07:00
b4745ef55d Merge pull request #118 from airgradienthq/hotfix/offline-mode-should-not-show-server-status
Offline mode should not show server status on display
2024-05-13 12:09:59 +07:00
348cb2663a Merge pull request #116 from airgradienthq/hotfix/add-debug-mesage-when-hw-watchdog-reset
Update watchdog reset message
2024-05-13 12:09:45 +07:00
1b69e8a599 Offline mode should not shown status on display. #111 2024-05-13 12:02:17 +07:00
d17ad3cbab Merge branch 'hotfix/factory-reset-offline-mode' into hotfix/offline-mode-should-not-show-server-status 2024-05-13 11:52:01 +07:00
a6d8936ea6 Fix factory reset failed, the configuration not set to default. #112 2024-05-13 11:47:37 +07:00
8b428855b0 Merge branch 'hotfix/add-debug-mesage-when-hw-watchdog-reset' into hotfix/factory-reset-offline-mode 2024-05-13 11:21:01 +07:00
22dc2136e4 Update watchdog reset message 2024-05-13 11:18:08 +07:00
f23f81f575 Merge branch 'hotfix/ledbar-display-should-off-when-brightness-0-percent' into hotfix/fix-issue-before-release-new-version 2024-05-13 10:39:48 +07:00
7a4255b2bb Turn off LED bar and Display if brightness is 0%, fix #114 2024-05-13 10:34:06 +07:00
d844ab09fa Merge pull request #107 from airgradienthq/hotfix/configuration-default-after-ota-success-and-new-firmware-has-change-configuration-param
Move structure configure to JSON
2024-05-12 18:19:39 +07:00
31c60dcbec fix build failed 2024-05-12 11:48:03 +07:00
f0749783fe Merge branch 'develop' into hotfix/configuration-default-after-ota-success-and-new-firmware-has-change-configuration-param 2024-05-12 10:49:13 +07:00
d34605a018 Merge pull request #109 from airgradienthq/feature/adjust-co2-color-and-ranges
Adjust CO2 Colors and Ranges
2024-05-11 20:12:11 +07:00
1bcb9bf5ee Adjust CO2 Colors and Ranges 2024-05-11 19:29:02 +07:00
296bf49e5e Merge pull request #108 from airgradienthq/feature/press-button-for-offline-mode
Display show message for offline/online mode
2024-05-10 09:18:32 +07:00
e3dee42b4b fix esp8266 build fail 2024-05-10 09:16:22 +07:00
279ccb8bfb update configuration filename, log mesage and add configuration default value 2024-05-10 09:11:43 +07:00
a3c9727b02 Update variable a descriptive name 2024-05-10 07:19:11 +07:00
1b886d9843 Merge branch 'hotfix/configuration-default-after-ota-success-and-new-firmware-has-change-configuration-param' into feature/press-button-for-offline-mode 2024-05-09 15:09:00 +07:00
066e81b186 fix esp8266 build fail 2024-05-09 15:03:27 +07:00
c98d078d4c Resolve complex build failed 2024-05-09 14:56:40 +07:00
cb98183e20 Merge branch 'develop' into feature/press-button-for-offline-mode 2024-05-09 14:48:30 +07:00
0e1734b35d Merge branch 'develop' into hotfix/configuration-default-after-ota-success-and-new-firmware-has-change-configuration-param 2024-05-09 14:46:00 +07:00
da6326db0f Add display ask for offline/online mode 2024-05-09 14:32:42 +07:00
ad1da129c0 Merge branch 'hotfix/configuration-default-after-ota-success-and-new-firmware-has-change-configuration-param' into feature/press-button-for-offline-mode 2024-05-09 13:35:46 +07:00
8a90fe511b Merge pull request #104 from airgradienthq/feature/show-ota-process-on-display
Feature/show ota process on display
2024-05-09 06:23:03 +07:00
cca1ab69bb Move rebooting process out of OtaHandler 2024-05-08 12:31:54 +07:00
955172d3d3 Move structure configure to JSON 2024-05-08 12:22:34 +07:00
4500f41ed3 Merge pull request #106 from airgradienthq/feature/wifi-auto-connect-to-airgradient
Code for autoconnect to: airgradient:cleanair
2024-05-07 15:47:22 +07:00
7c2f8e5b9b Add WiFi reset to factory default: connect to SSID airgradient after led bar test and button still keep pressed. 2024-05-07 15:00:32 +07:00
fb84b53077 Merge pull request #102 from airgradienthq/hotfix/led-button-test-not-handle-on-power-up
fix led bar button test not work on power up
2024-05-03 21:03:15 +07:00
3359b1817b Merge pull request #98 from airgradienthq/hotfix/remove-wifi-qrcode
Remove WiFi QrCode
2024-05-03 21:02:16 +07:00
8eb8d4a1ec Remove dependency from StateMachine 2024-05-03 17:27:05 +07:00
d2723de0f8 Remove OtaHandler constructor 2024-05-02 18:22:26 +07:00
4493156739 Implement regular OTA update attempt / indicate OTA processing on display 2024-05-02 10:19:49 +07:00
0acb7d470d Merge remote-tracking branch 'origin/develop' into feature/show-ota-process-on-display 2024-05-02 09:50:28 +07:00
8428442dea Merge branch 'develop' into hotfix/remove-wifi-qrcode 2024-05-02 09:19:14 +07:00
f32d6b1bbe Remove country dependency on LED bar button test 2024-05-02 09:10:43 +07:00
46600f59a3 Merge pull request #103 from airgradienthq/feature/display-and-ledbar-brightness
Feature/display and ledbar brightness
2024-05-02 08:58:45 +07:00
01c42387ed fix: typo 2024-05-01 21:27:01 +07:00
f08438db46 Implement display / ledBar brightness 2024-05-01 21:25:35 +07:00
221730160b fix led bar button test not work on power up 2024-05-01 21:02:57 +07:00
d40b1d37a8 Merge pull request #99 from airgradienthq/hotfix/git-workflow-ota-failed
fix github workflows build fail and platformio multiple project build
2024-05-01 09:54:59 +07:00
ecd3aa988f Merge pull request #101 from airgradienthq/hotfix/EEPROM-data-override-by-after-OTA-perform-success
fix issue EEPROM override after OTA perform by use `spiffs`
2024-05-01 09:54:37 +07:00
095787d1f2 Merge pull request #100 from airgradienthq/hotfix/temperature-unit-supported-shortname-value
Temperature configuration unit support shortname value `c` and `f`
2024-04-30 22:26:53 +07:00
d94d074abe fix issue EEPROM override after OTA perform by use spiffs 2024-04-30 20:51:08 +07:00
cdef9822b3 Merge pull request #97 from airgradienthq/dev/fix-source-code-format
re-format source code
2024-04-30 20:46:14 +07:00
29c1989e78 Temperature configuration unit support shortname value c and f 2024-04-30 20:32:53 +07:00
bc3872e631 fix github workflows build fail and platformio multiple project build 2024-04-30 20:28:34 +07:00
7a182ebb12 Remove WiFi QrCode 2024-04-30 19:59:43 +07:00
5e6f801534 re-format source code 2024-04-29 19:46:13 +07:00
88808a2ad2 Revert "fix: source code formating"
This reverts commit 1580cd51aa.
2024-04-29 19:34:15 +07:00
a4fc954712 Merge remote-tracking branch 'origin/master' into develop 2024-04-29 10:01:10 +07:00
1580cd51aa fix: source code formating 2024-04-29 09:54:37 +07:00
3efba24e24 Merge pull request #96 from danielmoore/prom-pms-data
Report PM data to metrics API for AirGradient ONE
2024-04-29 09:43:45 +07:00
940dd0167c Fix OpenMetrics humidity metric 2024-04-28 22:10:39 -04:00
6b4f86e7e4 Report PM data to metrics API for AirGradient ONE 2024-04-28 21:47:38 -04:00
e5a0c6bc7b Merge pull request #95 from airgradienthq/ota
OTA Update, better handle the 304 and 400 case
2024-04-27 14:14:09 +07:00
b8fdb38db8 Merge branch 'develop' into ota 2024-04-25 16:47:28 +07:00
b2c55c38dc better handle the 304 and 400 case 2024-04-25 16:11:49 +07:00
4fac3fddb8 Merge pull request #94 from airgradienthq/develop
Next version 3.1.0-beta.1
2024-04-25 10:15:52 +07:00
c025bae3df next version 3.1.0-beta.1 2024-04-25 10:13:07 +07:00
e82b5f8369 Merge branch 'develop' of https://github.com/airgradienthq/arduino into develop 2024-04-25 06:49:18 +07:00
1ec5d84043 show wifi connect on display by qrcode 2024-04-25 06:49:14 +07:00
26546a6079 Merge pull request #93 from airgradienthq/develop
Develop to Master
2024-04-24 20:59:44 +07:00
5078b35341 moved fallback GIT_VERSION to AirGradient.h 2024-04-24 20:27:45 +07:00
a3cbca61ee Merge branch 'master' into develop 2024-04-24 20:03:33 +07:00
6d7750d917 Merge branch 'develop' of https://github.com/airgradienthq/arduino into develop 2024-04-24 20:02:40 +07:00
1c8d7b04e9 ota handling implemented 2024-04-24 19:56:37 +07:00
e12a154235 ota handling code added, wip 2024-04-23 06:35:21 +07:00
12f03aff30 ota handler added, wip 2024-04-23 05:59:58 +07:00
5275f5a810 compile git version into firmware 2024-04-22 19:52:13 +07:00
442f0fd942 [update message log and fix some bug] add miss 2024-04-22 16:32:17 +07:00
9feac035eb update message log and fix some bug 2024-04-22 16:31:40 +07:00
21d984c95a add get nox/tvoc learning offset 2024-04-22 16:31:21 +07:00
b91a3058fc Move common function to separate file 2024-04-22 16:30:57 +07:00
2d96fc28c5 fix postDataToAirGradient set false after device power up and connected to WiFi 2024-04-22 14:24:01 +07:00
7561017b3d update log message and fix ledBarTestRequested, co2CalibrationRequested not execute 2024-04-22 06:27:57 +07:00
2b891c0194 Merge branch 'develop' of https://github.com/airgradienthq/arduino into develop 2024-04-21 21:03:06 +07:00
d5fc35df2f Add _compensated to log output 2024-04-21 21:03:02 +07:00
a91f6c1fa0 Added documentation for Open Metrics 2024-04-21 17:28:56 +07:00
89802551a0 update CO2CalibrationAbcDays 2024-04-21 16:44:43 +07:00
556a6fbd6d update mqtt publish data is locally 2024-04-21 16:33:55 +07:00
302bec9d37 Added documentation 2024-04-21 09:36:46 +07:00
e61cd9ba6a Update OpenMetrics with compensated values 2024-04-21 06:10:17 +07:00
71b45fcdc1 [platformio] update configuration 2024-04-21 06:10:05 +07:00
a2b30a5467 ignore compensated value send to AG server. And udpate JSON properties 2024-04-21 05:46:20 +07:00
4063536078 Merge branch 'develop' of https://github.com/airgradienthq/arduino into develop 2024-04-20 18:29:44 +07:00
563fc062cb add platformio configuration 2024-04-20 18:29:40 +07:00
5534a2cf7e Added documentation 2024-04-19 14:09:48 +07:00
cb4ae82b1b Update properties JSON Snake Case to Camel Case 2024-04-14 22:15:37 +07:00
8adca3a9ee Update pmStandard JSON stringtify value 2024-04-14 22:14:50 +07:00
5f15e29c76 fix configuration pmStandard value 2024-04-14 21:36:50 +07:00
6e1ac26187 update handle noxLearningOffset and tvocLearningOffset configuration 2024-04-14 21:30:56 +07:00
ccee987d05 add firmware mode O-1P 2024-04-13 21:07:20 +07:00
09cbbed856 fix typo 2024-04-13 20:50:11 +07:00
e1115659e2 add atmp_compensated and rhum_compensated to sync data 2024-04-13 20:39:05 +07:00
25ef1ced9e Remove correction Temperature and Humidity for PMS5003T 2024-04-12 12:16:31 +07:00
2ccddf0e19 Update: Post to Airgradient on WiFiConnector content 2024-04-12 12:08:02 +07:00
9a1e0f4cdd Merge branch 'develop' of https://github.com/airgradienthq/arduino into develop 2024-04-12 06:18:52 +07:00
86c6095362 update: WiFiConnector Post To AirGradient default checked and add explain text 2024-04-12 06:18:48 +07:00
1d6a0a06c0 Move Arduino_JSON to AirGradient libries 2024-04-11 09:29:29 +07:00
bd1197971f Remove text before checkbox Post to AirGradient 2024-04-11 06:45:36 +07:00
c28a937384 Add postDataToAirGradient checkbox to WiFi Manager 2024-04-11 06:33:56 +07:00
b2195219ab Update: SHT read failed 2024-04-08 10:29:58 +07:00
cb7a6a2dfd fix: Local Configuration 2024-04-08 10:15:45 +07:00
51ff8f8df4 fix: WiFi reconnect on ESP8266 2024-04-07 17:01:54 +07:00
5b271f4ed9 Remove WiFiManager from Build Instructions 2024-04-07 16:51:03 +07:00
4577082731 clarifying method/variable/class names 2024-04-07 16:39:01 +07:00
9a03fb2bd7 added comments to code 2024-04-05 11:45:02 +07:00
dfba4fa4b1 [temporary commit] 2024-04-05 06:39:59 +07:00
f681d4b2e8 add WiFiManager and 'U8g2 to Libraries` 2024-04-04 18:35:15 +07:00
dba385f5bb [temporary commit] 2024-04-04 10:36:59 +07:00
027ffeaa92 [temporary commit] 2024-04-03 21:26:04 +07:00
c1ab99ba8d [temporary commit] 2024-04-03 11:40:46 +07:00
8e032927c6 [temporary commit] 2024-04-03 07:04:55 +07:00
f52eab87d2 [temporary commit] 2024-04-03 07:04:28 +07:00
954a7751cc fix issue: multiple define UseLedBar 2024-04-01 09:23:03 +07:00
7d68b02f76 Remove example ONE.ino and OpenAir.ino 2024-04-01 09:18:25 +07:00
3788aa2746 change locallyControlled to configurationControl and update relate logic 2024-04-01 09:15:10 +07:00
260e904326 Merge branch 'develop' of https://github.com/airgradienthq/arduino into develop 2024-03-31 09:37:48 +07:00
9055bbb690 add comment 2024-03-31 09:37:45 +07:00
5e20b9e8ec Changed defaults 2024-03-31 09:15:30 +07:00
adce439ce7 Ignore update locallyControlled if configuration from server 2024-03-31 07:52:33 +07:00
dc875dd5a8 Ignore call to LED Bar in OpenAir 2024-03-31 07:51:56 +07:00
378688d2fa add GET/ PUT from local server on monitor 2024-03-30 19:33:03 +07:00
a2200795d7 fix: firmware model name FW_MODE_I_1PSL -> FW_MODE_I_9PSL 2024-03-29 19:32:09 +07:00
3889aa660e Merge pull request #88 from airgradienthq/develop
Bugfix and add example
2024-03-24 20:20:22 +07:00
efe68a54a4 update version 3.0.8 to 3.0.9 2024-03-24 20:04:48 +07:00
a960d086e1 Update instruction comment and clean code. 2024-03-24 08:51:39 +07:00
3537a3012c fix: WiFi connect after LedBar test with button. And update comments 2024-03-24 08:51:14 +07:00
d255e6ad04 Update workflows for OneOpenAir 2024-03-24 07:45:32 +07:00
e47096feac Limit show CO2 index within 4 character number (max = 9999) 2024-03-24 07:43:40 +07:00
063612e08f fix: Ledbar off in offline mode 2024-03-24 07:40:26 +07:00
7cfa722684 update show log log message and fix bug 2024-03-23 17:44:11 +07:00
53285ab4ff Combine One and OpenAir into one: Just worked 2024-03-23 16:58:17 +07:00
f46c66a77f Merge pull request #87 from airgradienthq/develop
fix: After factory reset LEDBar is off
2024-03-18 08:48:28 +07:00
9c8ae315a0 fix: After factory reset LEDBar is off 2024-03-18 08:45:16 +07:00
3ef438412f Merge pull request #85 from airgradienthq/develop
Develop: optimize and bugfix
2024-03-16 12:00:20 +07:00
ce1373141a Updated version number 2024-03-16 11:13:04 +07:00
aceecde7b6 Format code 2024-03-16 10:02:59 +07:00
6926abd6f7 Standardize result of /measures/current for OpenAir 2024-03-16 10:02:46 +07:00
15dec40dfc Merge remote-tracking branch 'origin/master' into develop 2024-03-16 09:11:25 +07:00
4a36cf0c13 Merge pull request #83 from austvik/master
Standardize result of /measures/current
2024-03-16 09:08:53 +07:00
ecc92a6824 Merge pull request #81 from dechamps/celsius
Fix OpenMetrics typo: celcius -> celsius
2024-03-16 09:07:49 +07:00
3d243cb8ca Revert: version 3.0.7 2024-03-16 08:52:50 +07:00
471448a0f1 Prevent reboot in offline mode 2024-03-16 08:50:43 +07:00
ea3e976232 update next version 3.0.7 2024-03-16 08:21:44 +07:00
87f2463233 Update comment 2024-03-16 08:21:14 +07:00
49c7877ec3 Standardize result of /measures/current
Makes it easier for integrations which talks both to the cloud and to the
Cloud API to support the same format for reading current measures.

In practice this adds the firmware version and led mode to the output, and
changes the writing of pm003Count, tvocIndex and noxIndex to use the same
spelling as for the documented cloud API.
2024-03-15 19:22:00 +01:00
be1a9778e6 Fix: PMS Read Failed 2024-03-14 21:17:43 +07:00
ed1d45cea1 Fix OpenMetrics typo: celcius -> celsius
See #78
2024-03-10 11:02:04 +00:00
db31b39ce2 Merge pull request #80 from airgradienthq/develop
Develop
2024-03-10 12:01:35 +07:00
d92d312b0c add openmetrics 2024-03-10 11:57:52 +07:00
6837529096 add compensation temperature and humidity for SGP41 2024-03-10 11:20:52 +07:00
b94ae9eff0 Merge remote-tracking branch 'origin/master' into develop 2024-03-10 10:10:58 +07:00
1810c0f355 Merge pull request #78 from gouthamve/better-openmetrics-names
[ONE/Prometheus] Use full unit in temperature metric
2024-03-10 09:58:09 +07:00
eb0f45750d Merge pull request #79 from austvik/master
Fix MDNS Service Discovery:
2024-03-10 09:53:25 +07:00
9ae8fb2355 fix: Completely turn off LEDbar 2024-03-10 09:34:04 +07:00
512509c2e2 Print HTTP Response in logs in case of error 2024-03-10 08:44:49 +07:00
66815f590c Merge remote-tracking branch 'origin/develop' into develop 2024-03-10 08:27:40 +07:00
f60e9bbe3e Fix MDNS Service Discovery:
- Underscore before names per https://github.com/espressif/arduino-esp32/issues/962
- Only one service per port

The combination of both changes is needed to make the service discoverable in OpenHAB

The removal of the published http service is maybe something you don't want,
but as long as it doesn't serve web pages it is maybe OK?
2024-03-08 23:37:12 +01:00
f361e3c9a9 [ONE/Prometheus] Use full unit in temperature metric
Please see: https://prometheus.io/docs/practices/naming/#base-units

Also, thanks a lot for this support, I had my own exporter that I can now delete :)

Signed-off-by: gouthamve <gouthamve@gmail.com>
2024-03-08 19:02:19 +01:00
e76dcf07c8 Updated log texts 2024-03-08 13:47:28 +07:00
e6fe489be7 Updated display texts 2024-03-08 13:29:17 +07:00
9ddb606a00 Merge pull request #77 from airgradienthq/develop
Turn all LED on while init
2024-03-07 21:57:16 +07:00
cd5ee2da18 Turn all LED on while init 2024-03-07 21:54:22 +07:00
4c42a9ddc8 Merge pull request #76 from airgradienthq/develop
Develop
2024-03-07 21:49:10 +07:00
78b1b0975c update: Connect to Dashboard show also S/N, remove test code 2024-03-07 21:48:13 +07:00
d99881aa46 update: Connect to Dashboard show also S/N 2024-03-07 21:44:46 +07:00
df937fe65f update mDNS servicce and attribute 2024-03-07 21:30:42 +07:00
dc742d3c92 Updated log messages and version number 2024-03-07 16:11:36 +07:00
a7b2ad526f Merge pull request #74 from airgradienthq/develop
fix: `O-1PS` not recognize without `SGP` sensor
2024-03-06 18:13:34 +07:00
bb804b9f6a fix: O-1PS not recognize without SGP sensor 2024-03-06 18:02:32 +07:00
1a00073cf6 Merge pull request #73 from airgradienthq/develop
Develop
2024-03-06 17:24:52 +07:00
469d07a2d6 fix: O-1PS not recognized 2024-03-06 17:20:55 +07:00
6cf5e31843 add nox_index to payload 2024-03-03 22:24:58 +07:00
3f1da6387b Update mDNS service model attribute 2024-03-03 22:03:23 +07:00
99b4858f1d Merge pull request #72 from airgradienthq/develop
Develop: update workflows build configure
2024-03-03 21:57:41 +07:00
4374c980ec Update workflows example build configure 2024-03-03 21:51:53 +07:00
ded7637b06 Update workflows example build configure 2024-03-03 21:47:50 +07:00
6a79ab6b5b Update workflows example build configure 2024-03-03 21:46:07 +07:00
7baff75524 Merge pull request #71 from dechamps/checkpr
Fix check workflow failing on pull requests
2024-03-03 21:38:01 +07:00
d421c94647 Merge branch 'master' into develop 2024-03-03 21:35:25 +07:00
d78205aa20 Changed measurement and update interval for Open Air. Added fw version to logs. 2024-03-02 15:04:30 +07:00
c1228bbd06 Changed measurement and update intervalls 2024-03-02 14:05:00 +07:00
1eb43f684b Fix SHT read error. 2024-03-02 13:41:08 +07:00
4798e44cb7 MDNS replace board with model 2024-03-01 21:56:21 +07:00
a867e9af38 revert SENSOR_TEMP_HUM_UPDATE_INTERVAL value 2024-03-01 19:51:58 +07:00
8fcf257726 Fix check workflow failing on pull requests
This fixes the following check GitHub Actions workflow failure that
would otherwise occur on pull requests (but not on pushes/branches):

  Error installing Git Library: Library install failed: object not found
2024-02-29 23:52:36 +00:00
a59d5a1bb8 Update check.yml
Adjusted new name for example files
2024-02-29 20:05:16 +07:00
b4d6006678 Changed PM polling frequency for Open Air to 2s 2024-02-29 18:30:52 +07:00
236c5bab84 Added offline mode after LED test. 2024-02-29 18:15:22 +07:00
852fdc4360 Renamed example file. 2024-02-29 17:58:58 +07:00
f7e85a92e8 Uped version Nr. Renamed examples. 2024-02-29 17:58:14 +07:00
e99fc2ecdc Merge pull request #69 from airgradienthq/develop
Update API naming
2024-02-29 15:26:53 +07:00
67785ed99b Update API naming 2024-02-29 15:20:19 +07:00
45ac4f116b Merge pull request #68 from airgradienthq/develop
Develop
2024-02-29 15:12:28 +07:00
173e3caf2f Update API naming 2024-02-29 15:07:23 +07:00
351af57591 move pm25ToAQI into PMSUtils 2024-02-29 14:45:44 +07:00
0bda7a1c4b Change pmPoll interval from 5s to 2s 2024-02-29 14:00:19 +07:00
9a31c107fa add get BoardName from AirGradient library 2024-02-29 13:58:48 +07:00
5449fa15ea Merge branch 'master' into develop 2024-02-29 13:50:46 +07:00
3e4e2affa8 Merge pull request #59 from dechamps/prometheus
Add support for Prometheus/OpenMetrics to One V9
2024-02-29 13:47:49 +07:00
d3a242a0b7 Merge branch 'master' into develop 2024-02-29 13:45:45 +07:00
e5e2887c4d Merge pull request #58 from dechamps/githubactions
Add compile check GitHub Actions workflow
2024-02-29 13:41:50 +07:00
4eda2e4cb5 Merge pull request #67 from airgradienthq/develop
Develop
2024-02-29 10:57:10 +07:00
fcee721d58 Merge pull request #66 from airgradienthq/feature/add-mdns-attributes
Add mDNS attribute
2024-02-29 10:56:48 +07:00
7d12e63e34 Merge pull request #65 from airgradienthq/feature/relative-humidity-pms5003t-formula
Feature/relative humidity pms5003t formula
2024-02-29 10:50:21 +07:00
8ff8b7929e Update correction relative humdity formula 2024-02-29 10:49:38 +07:00
66c53daed6 Implement relative humidity correction 2024-02-29 10:39:17 +07:00
5de3a34dd0 Add mDNS attribute 2024-02-29 10:22:05 +07:00
b94112e22a Merge pull request #64 from airgradienthq/feature/led-bar-color-for-pms-and-co2
Feature/led bar color for pms and co2
2024-02-29 09:38:13 +07:00
99e925e7bd Merge pull request #63 from airgradienthq/hotfix/build-fail-if-set-core-debug-level-to-verbose
fix: build fail if set `Core Debug Level` to `Verbose`, #57
2024-02-29 09:34:26 +07:00
d8cba0d346 fix: build fail if set Core Debug Level to Verbose, #57 2024-02-29 09:33:33 +07:00
39de897621 Merge pull request #56 from dechamps/includecap
Fix path capitalization
2024-02-29 09:05:30 +07:00
9f1a793848 Merge pull request #62 from airgradienthq/hotfix/compile-fail-cause-value-type
fix: BoardDef pin number invalid type, #51
2024-02-29 08:49:03 +07:00
be9ba88d52 fix: BoardDef pin number invalid type, #51 2024-02-29 08:45:38 +07:00
0084b6fb91 fix: update cloud sync json noxIndex by nox_index 2024-02-29 08:39:05 +07:00
75b579bafa Merge pull request #61 from airgradienthq/feature/factory-config-reset
add Factory RESET
2024-02-26 17:51:17 +07:00
cf5ff99d8a add Factory RESET 2024-02-26 17:48:22 +07:00
ea204d90b1 Merge pull request #60 from airgradienthq/bugfix/mqtt-sending-interval
fix: Mqtt sending interval
2024-02-26 15:56:30 +07:00
13f6c2c747 fix: Mqtt sending interval 2024-02-26 15:55:33 +07:00
760f827d0d Add support for Prometheus/OpenMetrics to One V9
This commit adds a new feature to the One V9 (ONE_I-9PSL) firmware:
support for exposing metrics to Prometheus (or any other ingestor
compatible with the OpenMetrics format).

With this change, the AirGradient device will make metrics available
on the standard HTTP /metrics endpoint, out-of-the-box, with no need to
do anything else. All the user has to do is add their device address as
a target to their scrape config on their Prometheus server.

For more information on Prometheus and OpenMetrics, see:

- https://prometheus.io/docs/instrumenting/exposition_formats/
- https://openmetrics.io/
- https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md

This obsoletes projects such as:

- ebfa8d0ac6/AirGradient-DIY
- https://forum.airgradient.com/t/prometheus-integration/1504
2024-02-25 17:51:58 +00:00
8c8e0d4dea Add compile check GitHub Actions workflow
This commit adds a GitHub Actions workflow that, on every push/pull
request, will check that every single example successfully compiles on
every board it supports. That is, it it will check compilation on:

- ESP8266 for BASIC_v4, TestCO2, TestPM and TestSht
- ESP32 for ONE_I-9PSL, Open_Air, TestCO2, TestPM and TestSht

This provides the first building block towards a Continuous Integration
(CI) pipeline and prevents build breakages from making it to master.

Ideally this should also run tests on the examples (i.e. verifying that
the example boots successfully and sends metrics), but for now this will
at least ensure the build is not obviously broken.
2024-02-25 12:12:16 +00:00
b749495bf4 Fix path capitalization
This fixes build breakage on case-sensitive filesystems (e.g. Linux)
with errors like:

  src/Display/Display.h:4:10: fatal error: ../main/BoardDef.h: No such file or directory
2024-02-24 21:43:03 +00:00
7e3eabf09f Remove old color set process for PM 2024-02-21 21:18:44 +07:00
e636876c9b Update .gitignore 2024-02-21 21:16:12 +07:00
68953d7390 Update LED for PM and CO2 2024-02-21 21:16:01 +07:00
1a52c2d9f8 Merge pull request #54 from airgradienthq/feature/bugfix-and-example-update
Feature/bugfix and example update
2024-02-21 20:19:54 +07:00
6afcf6d4c3 Udpate version: 3.0.4 2024-02-20 21:06:09 +07:00
af139331b1 Show message when sensor module not found on display 2024-02-20 21:05:13 +07:00
e79a798b88 Update show invalid value into display 2024-02-20 21:05:04 +07:00
14fb790e2a PMS5003 add failed count to 3 before show invalid value to display 2024-02-20 20:36:06 +07:00
2aab02940d add local webserver mDNS airgradient_<devId>.local 2024-02-18 15:20:31 +07:00
da07067661 Save configuration on device persistently 2024-02-18 15:01:30 +07:00
b2091114b3 add serialno to local server data GET response 2024-02-18 12:50:46 +07:00
e09128572c fix: FW stops if some sensor not found 2024-02-18 12:43:37 +07:00
e16966d092 Add webserver to get measure data on example Open_Air 2024-02-18 11:06:06 +07:00
26a8b065bc fix: wifi not connect if LED test with button request 2024-02-18 10:59:01 +07:00
589b98d97e Better server configure for abcDays debug message 2024-02-18 10:49:06 +07:00
cb4d9372f8 Add device webserver to get measure data at <IPAddress>/measures/current 2024-02-18 10:35:20 +07:00
6cb7fa8a1b Fix: capitalize folder and file 2024-02-17 17:34:01 +07:00
6cdbb8a0a3 Fix capitalize folder and file name ignored 2024-02-17 17:28:51 +07:00
781fb51c6f Add mqtt client 2024-02-17 17:19:29 +07:00
17646f3067 Update typo, #49 2024-02-17 14:05:17 +07:00
7a4b665bb5 [Update] optimize display not show on power up or after flash firmware 2024-02-17 13:56:07 +07:00
571b36d05f Optimize Serial nr show and display do not show content after power up or flash firmware 2024-02-17 13:50:22 +07:00
23513cf88c Add parameter tvoc_raw for SGP41 2024-02-17 13:36:32 +07:00
b475c5c1ec capitalize folder names and file names Same like class file names 2024-02-17 13:17:45 +07:00
ee9f26ee04 Update multiple typos, #50 2024-02-17 13:02:24 +07:00
8c94cea764 round real value with 2 decimal on server sync data json 2024-02-17 12:47:51 +07:00
7c63af5ba9 Add Serial Nr into log 2024-02-17 12:11:44 +07:00
7c1eae83e4 Add logging for abcDays 2024-02-17 12:04:11 +07:00
e48ff0e41c Fix model PST send data to cloud with channel 2024-02-17 10:45:56 +07:00
c9e3a2a9b4 Rename example Open_Air_O to Open_Air 2024-02-17 10:38:10 +07:00
565 changed files with 532149 additions and 3673 deletions

61
.github/workflows/check.yml vendored Normal file
View File

@ -0,0 +1,61 @@
on: [push, pull_request]
jobs:
compile:
strategy:
fail-fast: false
matrix:
example:
- "BASIC"
- "DiyProIndoorV4_2"
- "DiyProIndoorV3_3"
- "TestCO2"
- "TestPM"
- "TestSht"
- "OneOpenAir"
fqbn:
- "esp8266:esp8266:d1_mini"
- "esp32:esp32:esp32c3"
include:
- fqbn: "esp8266:esp8266:d1_mini"
core: "esp8266:esp8266@3.1.2"
core_url: "https://arduino.esp8266.com/stable/package_esp8266com_index.json"
- fqbn: "esp32:esp32:esp32c3"
board_options: "JTAGAdapter=default,CDCOnBoot=cdc,PartitionScheme=min_spiffs,CPUFreq=160,FlashMode=qio,FlashFreq=80,FlashSize=4M,UploadSpeed=921600,DebugLevel=verbose,EraseFlash=none"
core: "esp32:esp32@2.0.11"
exclude:
- example: "BASIC"
fqbn: "esp32:esp32:esp32c3"
- example: "DiyProIndoorV4_2"
fqbn: "esp32:esp32:esp32c3"
- example: "DiyProIndoorV3_3"
fqbn: "esp32:esp32:esp32c3"
- example: "OneOpenAir"
fqbn: "esp8266:esp8266:d1_mini"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run:
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh |
sh -s 0.35.3
- run: bin/arduino-cli --verbose core install '${{ matrix.core }}'
--additional-urls '${{ matrix.core_url }}'
- run: bin/arduino-cli --verbose lib install
WiFiManager@2.0.16-rc.2
Arduino_JSON@0.2.0
U8g2@2.34.22
# In some cases, actions/checkout@v4 will check out a detached HEAD; for
# example, this happens on pull request events, where an hypothetical
# PR merge commit is checked out. This tends to confuse
# `arduino-cli lib install --git-url`, making it fail with errors such as:
# Error installing Git Library: Library install failed: object not found
# Create and check out a dummy branch to work around this issue.
- run: git checkout -b check
- run: bin/arduino-cli --verbose lib install --git-url .
env:
ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL: "true"
- run: bin/arduino-cli --verbose compile 'examples/${{ matrix.example }}'
--fqbn '${{ matrix.fqbn }}' --board-options '${{ matrix.board_options }}'
# TODO: at this point it would be a good idea to run some smoke tests on
# the resulting image (e.g. that it boots successfully and sends metrics)
# but that would either require a high fidelity device emulator, or a
# "hardware lab" runner that is directly connected to a relevant device.

5
.gitignore vendored
View File

@ -1,2 +1,5 @@
.vscode
*.DS_Store
build
.vscode
/.idea/
.pio

View File

@ -24,6 +24,10 @@ If you have an older version of the AirGradient PCB not mentioned in the example
If you have any questions or problems, check out [our forum](https://forum.airgradient.com/).
## Documentation
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/).
## The following libraries have been integrated into this library for ease of use
- [Adafruit BusIO](https://github.com/adafruit/Adafruit_BusIO)
@ -35,7 +39,9 @@ If you have any questions or problems, check out [our forum](https://forum.airgr
- [Sensirion Core](https://github.com/Sensirion/arduino-core/)
- [Sensirion I2C SGP41](https://github.com/Sensirion/arduino-i2c-sgp41)
- [Sensirion I2C SHT](https://github.com/Sensirion/arduino-sht)
- [PMS](https://github.com/fu-hsi/pms)
- [WiFiManager](https://github.com/tzapu/WiFiManager)
- [Arduino_JSON](https://github.com/arduino-libraries/Arduino_JSON)
- [PubSubClient](https://github.com/knolleary/pubsubclient)
## License
CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License

130
docs/local-server.md Normal file
View File

@ -0,0 +1,130 @@
## Local Server API
From [firmware version 3.0.10](firmwares) onwards, the AirGradient ONE and Open Air monitors have below API available.
#### Discovery
The monitors run a mDNS discovery. So within the same network, the monitor can be accessed through:
http://airgradient_{{serialnumber}}.local
The following requests are possible:
#### Get Current Air Quality (GET)
With the path "/measures/current" you can get the current air quality data.
http://airgradient_ecda3b1eaaaf.local/measures/current
“ecda3b1eaaaf” being the serial number of your monitor.
You get the following response:
```json
{
"wifi": -46,
"serialno": "ecda3b1eaaaf",
"rco2": 447,
"pm01": 3,
"pm02": 7,
"pm10": 8,
"pm003Count": 442,
"atmp": 25.87,
"atmpCompensated": 24.47,
"rhum": 43,
"rhumCompensated": 49,
"tvocIndex": 100,
"tvocRaw": 33051,
"noxIndex": 1,
"noxRaw": 16307,
"boot": 6,
"bootCount": 6,
"ledMode": "pm",
"firmware": "3.1.3",
"model": "I-9PSL"
}
```
| Properties | Type | Explanation |
|------------------|--------|--------------------------------------------------------------------|
| `serialno` | String | Serial Number of the monitor |
| `wifi` | Number | WiFi signal strength |
| `pm01` | Number | PM1 in ug/m3 |
| `pm02` | Number | PM2.5 in ug/m3 |
| `pm10` | Number | PM10 in ug/m3 |
| `pm02Compensated` | Number | PM2.5 in ug/m3 with correction applied (from fw version 3.1.4 onwards) |
| `rco2` | Number | CO2 in ppm |
| `pm003Count` | Number | Particle count per dL |
| `atmp` | Number | Temperature in Degrees Celsius |
| `atmpCompensated` | Number | Temperature in Degrees Celsius with correction applied |
| `rhum` | Number | Relative Humidity |
| `rhumCompensated` | Number | Relative Humidity with correction applied |
| `tvocIndex` | Number | Senisiron VOC Index |
| `tvocRaw` | Number | VOC raw value |
| `noxIndex` | Number | Senisirion NOx Index |
| `noxRaw` | Number | NOx raw value |
| `boot` | Number | Counts every measurement cycle. Low boot counts indicate restarts. |
| `bootCount` | Number | Same as boot property. Required for Home Assistant compatability. Will be depreciated. |
| `ledMode` | String | Current configuration of the LED mode |
| `firmware` | String | Current firmware version |
| `model` | String | Current model name |
Compensated values apply correction algorithms to make the sensor values more accurate. Temperature and relative humidity correction is only applied on the outdoor monitor Open Air but the properties _compensated will still be send also for the indoor monitor AirGradient ONE.
#### Get Configuration Parameters (GET)
With the path "/config" you can get the current configuration.
```json
{
"country": "US",
"pmStandard": "ugm3",
"ledBarMode": "pm",
"displayMode": "on",
"abcDays": 30,
"tvocLearningOffset": 12,
"noxLearningOffset": 12,
"mqttBrokerUrl": "",
"temperatureUnit": "f",
"configurationControl": "both",
"postDataToAirGradient": true
}
```
#### Set Configuration Parameters (PUT)
Configuration parameters can be changed with a put request to the monitor, e.g.
Example to force CO2 calibration
```curl -X PUT -H "Content-Type: application/json" -d '{"co2CalibrationRequested":true}' http://airgradient_84fce612eff4.local/config ```
Example to set monitor to Celsius
```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:
``` -d "{\"param\":\"value\"}" ```
#### Avoiding Conflicts with Configuration on AirGradient Server
If the monitor is set up on the AirGradient dashboard, it will also receive configurations 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)
| Properties | Description | Type | Accepted Values | Example |
|-------------------------|:-------------------------------------------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------|
| `country` | Country where the device is. | String | Country code as [ALPHA-2 notation](https://www.iban.com/country-codes) | {"country": "TH"} |
| `model` | Hardware identifier (only GET). | String | I-9PSL-DE | {"model": "I-9PSL-DE"} |
| `pmStandard` | Particle matter standard used on the display. | String | `ugm3`: ug/m3 <br> `us-aqi`: USAQI | {"pmStandard": "ugm3"} |
| `ledBarMode` | Mode in which the led bar can be set. | String | `co2`: LED bar displays CO2 <br>`pm`: LED bar displays PM <br>`off`: Turn off LED bar | {"ledBarMode": "off"} |
| `displayBrightness` | Brightness of the Display. | Number | 0-100 | {"displayBrightness": 50} |
| `ledBarBrightness` | Brightness of the LEDBar. | Number | 0-100 | {"ledBarBrightness": 40} |
| `abcDays` | Number of days for CO2 automatic baseline calibration. | Number | Maximum 200 days. Default 8 days. | {"abcDays": 8} |
| `mqttBrokerUrl` | MQTT broker URL. | String | | {"mqttBrokerUrl": "mqtt://192.168.0.18:1883"} |
| `temperatureUnit` | Temperature unit shown on the display. | String | `c` or `C`: Degree Celsius °C <br>`f` or `F`: Degree Fahrenheit °F | {"temperatureUnit": "c"} |
| `configurationControl` | The configuration source of the device. | String | `both`: Accept local and cloud configuration <br>`local`: Accept only local configuration <br>`cloud`: Accept only cloud configuration | {"configurationControl": "both"} |
| `postDataToAirGradient` | Send data to AirGradient cloud. | Boolean | `true`: Enabled <br>`false`: Disabled | {"postDataToAirGradient": true} |
| `co2CalibrationRequested` | Can be set to trigger a calibration. | Boolean | `true`: CO2 calibration (400ppm) will be triggered | {"co2CalibrationRequested": 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} |
| `tvocLearningOffset` | Set VOC learning gain offset. | Number | 0-720 (default 12) | {"tvocLearningOffset": 12} |
| `offlineMode` | Set monitor to run without WiFi. | Boolean | `false`: Disabled (default) <br> `true`: Enabled | {"offlineMode": true} |

22
docs/ota-updates.md Normal file
View File

@ -0,0 +1,22 @@
## 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
Upon compilation of an official release the git tag (GIT_VERSION) is compiled into the binary.
The device attempts to update to the latest version on startup and in regular intervals using URL
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'.
This allows the server to identify if the device is already running on the latest version or should update.
The following scenarios are possible
1. The device is already on the latest firmware. Then the server returns a 304 with a short explanation text in the body saying this.
2. The device reports a firmware unknown to the server. A 400 with an empty payload is returned in this case and the update is not performed. This case is relevant for local changes. The GIT_VERSION then defaults to "snapshot" which is unknown to the server.
3. There is an update available. A 200 along with the binary data of the new version is returned and the update is performed.
More information about the implementation details are available here: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/ota.html

573
examples/BASIC/BASIC.ino Normal file
View File

@ -0,0 +1,573 @@
/*
This is the code for the AirGradient DIY BASIC Air Quality Monitor with an D1
ESP8266 Microcontroller.
It is an air quality monitor for PM2.5, CO2, Temperature and Humidity with a
small display and can send data over Wifi.
Open source air quality monitors and kits are available:
Indoor Monitor: https://www.airgradient.com/indoor/
Outdoor Monitor: https://www.airgradient.com/outdoor/
Build Instructions:
https://www.airgradient.com/documentation/diy-v4/
Please make sure you have esp8266 board manager installed. Tested with
version 3.1.2.
Set board to "LOLIN(WEMOS) D1 R2 & mini"
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
can be set through the AirGradient dashboard.
If you have any questions please visit our forum at
https://forum.airgradient.com/
CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
*/
#include "AgApiClient.h"
#include "AgConfigure.h"
#include "AgSchedule.h"
#include "AgWiFiConnector.h"
#include "LocalServer.h"
#include "OpenMetrics.h"
#include "MqttClient.h"
#include <AirGradient.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <WiFiClient.h>
#define LED_BAR_ANIMATION_PERIOD 100 /** ms */
#define DISP_UPDATE_INTERVAL 2500 /** ms */
#define SERVER_CONFIG_SYNC_INTERVAL 60000 /** ms */
#define SERVER_SYNC_INTERVAL 60000 /** ms */
#define MQTT_SYNC_INTERVAL 60000 /** ms */
#define SENSOR_CO2_CALIB_COUNTDOWN_MAX 5 /** sec */
#define SENSOR_TVOC_UPDATE_INTERVAL 1000 /** ms */
#define SENSOR_CO2_UPDATE_INTERVAL 4000 /** ms */
#define SENSOR_PM_UPDATE_INTERVAL 2000 /** ms */
#define SENSOR_TEMP_HUM_UPDATE_INTERVAL 2000 /** ms */
#define DISPLAY_DELAY_SHOW_CONTENT_MS 2000 /** ms */
#define FIRMWARE_CHECK_FOR_UPDATE_MS (60 * 60 * 1000) /** ms */
static AirGradient ag(DIY_BASIC);
static Configuration configuration(Serial);
static AgApiClient apiClient(Serial, configuration);
static Measurements measurements;
static OledDisplay oledDisplay(configuration, measurements, Serial);
static StateMachine stateMachine(oledDisplay, Serial, measurements,
configuration);
static WifiConnector wifiConnector(oledDisplay, Serial, stateMachine,
configuration);
static OpenMetrics openMetrics(measurements, configuration, wifiConnector,
apiClient);
static LocalServer localServer(Serial, openMetrics, measurements, configuration,
wifiConnector);
static MqttClient mqttClient(Serial);
static int pmFailCount = 0;
static int getCO2FailCount = 0;
static AgFirmwareMode fwMode = FW_MODE_I_BASIC_40PS;
static String fwNewVersion;
static void boardInit(void);
static void failedHandler(String msg);
static void configurationUpdateSchedule(void);
static void appDispHandler(void);
static void oledDisplaySchedule(void);
static void updateTvoc(void);
static void updatePm(void);
static void sendDataToServer(void);
static void tempHumUpdate(void);
static void co2Update(void);
static void mdnsInit(void);
static void initMqtt(void);
static void factoryConfigReset(void);
static void wdgFeedUpdate(void);
static bool sgp41Init(void);
static void wifiFactoryConfigure(void);
static void mqttHandle(void);
AgSchedule dispLedSchedule(DISP_UPDATE_INTERVAL, oledDisplaySchedule);
AgSchedule configSchedule(SERVER_CONFIG_SYNC_INTERVAL,
configurationUpdateSchedule);
AgSchedule agApiPostSchedule(SERVER_SYNC_INTERVAL, sendDataToServer);
AgSchedule co2Schedule(SENSOR_CO2_UPDATE_INTERVAL, co2Update);
AgSchedule pmsSchedule(SENSOR_PM_UPDATE_INTERVAL, updatePm);
AgSchedule tempHumSchedule(SENSOR_TEMP_HUM_UPDATE_INTERVAL, tempHumUpdate);
AgSchedule tvocSchedule(SENSOR_TVOC_UPDATE_INTERVAL, updateTvoc);
AgSchedule watchdogFeedSchedule(60000, wdgFeedUpdate);
AgSchedule mqttSchedule(MQTT_SYNC_INTERVAL, mqttHandle);
void setup() {
/** Serial for print debug message */
Serial.begin(115200);
delay(100); /** For bester show log */
/** Print device ID into log */
Serial.println("Serial nr: " + ag.deviceId());
/** Initialize local configure */
configuration.begin();
/** Init I2C */
Wire.begin(ag.getI2cSdaPin(), ag.getI2cSclPin());
delay(1000);
configuration.setAirGradient(&ag);
oledDisplay.setAirGradient(&ag);
stateMachine.setAirGradient(&ag);
wifiConnector.setAirGradient(&ag);
apiClient.setAirGradient(&ag);
openMetrics.setAirGradient(&ag);
localServer.setAirGraident(&ag);
/** Example set custom API root URL */
// apiClient.setApiRoot("https://example.custom.api");
/** Init sensor */
boardInit();
/** Connecting wifi */
bool connectToWifi = false;
connectToWifi = !configuration.isOfflineMode();
if (connectToWifi) {
apiClient.begin();
if (wifiConnector.connect()) {
if (wifiConnector.isConnected()) {
mdnsInit();
localServer.begin();
initMqtt();
sendDataToAg();
apiClient.fetchServerConfiguration();
configSchedule.update();
if (apiClient.isFetchConfigureFailed()) {
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
stateMachine.displayHandle(
AgStateMachineWiFiOkServerOkSensorConfigFailed);
} else {
stateMachine.displayClearAddToDashBoard();
}
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
}
} else {
if (wifiConnector.isConfigurePorttalTimeout()) {
oledDisplay.showRebooting();
delay(2500);
oledDisplay.setText("", "", "");
ESP.restart();
}
}
}
}
/** Set offline mode without saving, cause wifi is not configured */
if (wifiConnector.hasConfigurated() == false) {
Serial.println("Set offline mode cause wifi is not configurated");
configuration.setOfflineModeWithoutSave(true);
}
/** Show display Warning up */
String sn = "SN:" + ag.deviceId();
oledDisplay.setText("Warming Up", sn.c_str(), "");
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
Serial.println("Display brightness: " +
String(configuration.getDisplayBrightness()));
oledDisplay.setBrightness(configuration.getDisplayBrightness());
appDispHandler();
}
void loop() {
/** Handle schedule */
dispLedSchedule.run();
configSchedule.run();
agApiPostSchedule.run();
if (configuration.hasSensorS8) {
co2Schedule.run();
}
if (configuration.hasSensorPMS1) {
pmsSchedule.run();
ag.pms5003.handle();
}
if (configuration.hasSensorSHT) {
tempHumSchedule.run();
}
if (configuration.hasSensorSGP) {
tvocSchedule.run();
}
/** Auto reset watchdog timer if offline mode or postDataToAirGradient */
if (configuration.isOfflineMode() ||
(configuration.isPostDataToAirGradient() == false)) {
watchdogFeedSchedule.run();
}
/** Check for handle WiFi reconnect */
wifiConnector.handle();
/** factory reset handle */
// factoryConfigReset();
/** check that local configura changed then do some action */
configUpdateHandle();
localServer._handle();
if (configuration.hasSensorSGP) {
ag.sgp41.handle();
}
MDNS.update();
mqttSchedule.run();
mqttClient.handle();
}
static void co2Update(void) {
int value = ag.s8.getCo2();
if (utils::isValidCO2(value)) {
measurements.CO2 = value;
getCO2FailCount = 0;
Serial.printf("CO2 (ppm): %d\r\n", measurements.CO2);
} else {
getCO2FailCount++;
Serial.printf("Get CO2 failed: %d\r\n", getCO2FailCount);
if (getCO2FailCount >= 3) {
measurements.CO2 = utils::getInvalidCO2();
}
}
}
static void mdnsInit(void) {
Serial.println("mDNS init");
if (!MDNS.begin(localServer.getHostname().c_str())) {
Serial.println("Init mDNS failed");
return;
}
MDNS.addService("_airgradient", "_tcp", 80);
MDNS.addServiceTxt("_airgradient", "_tcp", "model",
AgFirmwareModeName(fwMode));
MDNS.addServiceTxt("_airgradient", "_tcp", "serialno", ag.deviceId());
MDNS.addServiceTxt("_airgradient", "_tcp", "fw_ver", ag.getVersion());
MDNS.addServiceTxt("_airgradient", "_tcp", "vendor", "AirGradient");
MDNS.announce();
}
static void initMqtt(void) {
if (mqttClient.begin(configuration.getMqttBrokerUri())) {
Serial.println("Setup connect to MQTT broker successful");
} else {
Serial.println("setup Connect to MQTT broker failed");
}
}
static void wdgFeedUpdate(void) {
ag.watchdog.reset();
Serial.println();
Serial.println("Offline mode or isPostToAirGradient = false: watchdog reset");
Serial.println();
}
static bool sgp41Init(void) {
ag.sgp41.setNoxLearningOffset(configuration.getNoxLearningOffset());
ag.sgp41.setTvocLearningOffset(configuration.getTvocLearningOffset());
if (ag.sgp41.begin(Wire)) {
Serial.println("Init SGP41 success");
configuration.hasSensorSGP = true;
return true;
} else {
Serial.println("Init SGP41 failuire");
configuration.hasSensorSGP = false;
}
return false;
}
static void wifiFactoryConfigure(void) {
WiFi.persistent(true);
WiFi.begin("airgradient", "cleanair");
WiFi.persistent(false);
oledDisplay.setText("Configure WiFi", "connect to", "\'airgradient\'");
delay(2500);
oledDisplay.setText("Rebooting...", "", "");
delay(2500);
oledDisplay.setText("", "", "");
ESP.restart();
}
static void mqttHandle(void) {
if(mqttClient.isConnected() == false) {
mqttClient.connect(String("airgradient-") + ag.deviceId());
}
if (mqttClient.isConnected()) {
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI(),
&ag, &configuration);
String topic = "airgradient/readings/" + ag.deviceId();
if (mqttClient.publish(topic.c_str(), payload.c_str(), payload.length())) {
Serial.println("MQTT sync success");
} else {
Serial.println("MQTT sync failure");
}
}
}
static void sendDataToAg() {
/** Change oledDisplay and led state */
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting);
delay(1500);
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount)) {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected);
} else {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed);
}
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
}
void dispSensorNotFound(String ss) {
oledDisplay.setText("Sensor", ss.c_str(), "not found");
delay(2000);
}
static void boardInit(void) {
/** Display init */
oledDisplay.begin();
/** Show boot display */
Serial.println("Firmware Version: " + ag.getVersion());
if (ag.isBasic()) {
oledDisplay.setText("DIY Basic", ag.getVersion().c_str(), "");
} else {
oledDisplay.setText("AirGradient ONE",
"FW Version: ", ag.getVersion().c_str());
}
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
ag.watchdog.begin();
/** Show message init sensor */
oledDisplay.setText("Sensor", "init...", "");
/** Init sensor SGP41 */
configuration.hasSensorSGP = false;
// if (sgp41Init() == false) {
// dispSensorNotFound("SGP41");
// }
/** Init SHT */
if (ag.sht.begin(Wire) == false) {
Serial.println("SHTx sensor not found");
configuration.hasSensorSHT = false;
dispSensorNotFound("SHT");
}
/** Init S8 CO2 sensor */
if (ag.s8.begin(&Serial) == false) {
Serial.println("CO2 S8 sensor not found");
configuration.hasSensorS8 = false;
dispSensorNotFound("S8");
}
/** Init PMS5003 */
configuration.hasSensorPMS1 = true;
configuration.hasSensorPMS2 = false;
if (ag.pms5003.begin(&Serial) == false) {
Serial.println("PMS sensor not found");
configuration.hasSensorPMS1 = false;
dispSensorNotFound("PMS");
}
/** Set S8 CO2 abc days period */
if (configuration.hasSensorS8) {
if (ag.s8.setAbcPeriod(configuration.getCO2CalibrationAbcDays() * 24)) {
Serial.println("Set S8 AbcDays successful");
} else {
Serial.println("Set S8 AbcDays failure");
}
}
localServer.setFwMode(fwMode);
}
static void failedHandler(String msg) {
while (true) {
Serial.println(msg);
delay(1000);
}
}
static void configurationUpdateSchedule(void) {
if (apiClient.fetchServerConfiguration()) {
configUpdateHandle();
}
}
static void configUpdateHandle() {
if (configuration.isUpdated() == false) {
return;
}
stateMachine.executeCo2Calibration();
String mqttUri = configuration.getMqttBrokerUri();
if (mqttClient.isCurrentUri(mqttUri) == false) {
mqttClient.end();
initMqtt();
}
if (configuration.hasSensorSGP) {
if (configuration.noxLearnOffsetChanged() ||
configuration.tvocLearnOffsetChanged()) {
ag.sgp41.end();
int oldTvocOffset = ag.sgp41.getTvocLearningOffset();
int oldNoxOffset = ag.sgp41.getNoxLearningOffset();
bool result = sgp41Init();
const char *resultStr = "successful";
if (!result) {
resultStr = "failure";
}
if (oldTvocOffset != configuration.getTvocLearningOffset()) {
Serial.printf("Setting tvocLearningOffset from %d to %d hours %s\r\n",
oldTvocOffset, configuration.getTvocLearningOffset(),
resultStr);
}
if (oldNoxOffset != configuration.getNoxLearningOffset()) {
Serial.printf("Setting noxLearningOffset from %d to %d hours %s\r\n",
oldNoxOffset, configuration.getNoxLearningOffset(),
resultStr);
}
}
}
if (configuration.isDisplayBrightnessChanged()) {
oledDisplay.setBrightness(configuration.getDisplayBrightness());
}
appDispHandler();
}
static void appDispHandler(void) {
AgStateMachineState state = AgStateMachineNormal;
/** Only show display status on online mode. */
if (configuration.isOfflineMode() == false) {
if (wifiConnector.isConnected() == false) {
state = AgStateMachineWiFiLost;
} else if (apiClient.isFetchConfigureFailed()) {
state = AgStateMachineSensorConfigFailed;
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
} else {
stateMachine.displayClearAddToDashBoard();
}
} else if (apiClient.isPostToServerFailed()) {
state = AgStateMachineServerLost;
}
}
stateMachine.displayHandle(state);
}
static void oledDisplaySchedule(void) {
appDispHandler();
}
static void updateTvoc(void) {
measurements.TVOC = ag.sgp41.getTvocIndex();
measurements.TVOCRaw = ag.sgp41.getTvocRaw();
measurements.NOx = ag.sgp41.getNoxIndex();
measurements.NOxRaw = ag.sgp41.getNoxRaw();
Serial.println();
Serial.printf("TVOC index: %d\r\n", measurements.TVOC);
Serial.printf("TVOC raw: %d\r\n", measurements.TVOCRaw);
Serial.printf("NOx index: %d\r\n", measurements.NOx);
Serial.printf("NOx raw: %d\r\n", measurements.NOxRaw);
}
static void updatePm(void) {
if (ag.pms5003.isFailed() == false) {
measurements.pm01_1 = ag.pms5003.getPm01Ae();
measurements.pm25_1 = ag.pms5003.getPm25Ae();
measurements.pm10_1 = ag.pms5003.getPm10Ae();
measurements.pm03PCount_1 = ag.pms5003.getPm03ParticleCount();
Serial.println();
Serial.printf("PM1 ug/m3: %d\r\n", measurements.pm01_1);
Serial.printf("PM2.5 ug/m3: %d\r\n", measurements.pm25_1);
Serial.printf("PM10 ug/m3: %d\r\n", measurements.pm10_1);
Serial.printf("PM0.3 Count: %d\r\n", measurements.pm03PCount_1);
pmFailCount = 0;
} else {
pmFailCount++;
Serial.printf("PMS read failed: %d\r\n", pmFailCount);
if (pmFailCount >= 3) {
measurements.pm01_1 = utils::getInvalidPMS();
measurements.pm25_1 = utils::getInvalidPMS();
measurements.pm10_1 = utils::getInvalidPMS();
measurements.pm03PCount_1 = utils::getInvalidPMS();
}
}
}
static void sendDataToServer(void) {
/** Ignore send data to server if postToAirGradient disabled */
if (configuration.isPostDataToAirGradient() == false ||
configuration.isOfflineMode()) {
return;
}
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI(),
&ag, &configuration);
if (apiClient.postToServer(syncData)) {
ag.watchdog.reset();
Serial.println();
Serial.println(
"Online mode and isPostToAirGradient = true: watchdog reset");
Serial.println();
}
measurements.bootCount++;
}
static void tempHumUpdate(void) {
delay(100);
if (ag.sht.measure()) {
measurements.Temperature = ag.sht.getTemperature();
measurements.Humidity = ag.sht.getRelativeHumidity();
Serial.printf("Temperature in C: %0.2f\r\n", measurements.Temperature);
Serial.printf("Relative Humidity: %d\r\n", measurements.Humidity);
Serial.printf("Temperature compensated in C: %0.2f\r\n",
measurements.Temperature);
Serial.printf("Relative Humidity compensated: %d\r\n",
measurements.Humidity);
// Update compensation temperature and humidity for SGP41
if (configuration.hasSensorSGP) {
ag.sgp41.setCompensationTemperatureHumidity(measurements.Temperature,
measurements.Humidity);
}
} else {
Serial.println("SHT read failed");
measurements.Temperature = utils::getInvalidTemperature();
measurements.Humidity = utils::getInvalidHumidity();
}
}

View File

@ -0,0 +1,61 @@
#include "LocalServer.h"
LocalServer::LocalServer(Stream &log, OpenMetrics &openMetrics,
Measurements &measure, Configuration &config,
WifiConnector &wifiConnector)
: PrintLog(log, "LocalServer"), openMetrics(openMetrics), measure(measure),
config(config), wifiConnector(wifiConnector), server(80) {}
LocalServer::~LocalServer() {}
bool LocalServer::begin(void) {
server.on("/measures/current", HTTP_GET, [this]() { _GET_measure(); });
server.on(openMetrics.getApi(), HTTP_GET, [this]() { _GET_metrics(); });
server.on("/config", HTTP_GET, [this]() { _GET_config(); });
server.on("/config", HTTP_PUT, [this]() { _PUT_config(); });
server.begin();
logInfo("Init: " + getHostname() + ".local");
return true;
}
void LocalServer::setAirGraident(AirGradient *ag) { this->ag = ag; }
String LocalServer::getHostname(void) {
return "airgradient_" + ag->deviceId();
}
void LocalServer::_handle(void) { server.handleClient(); }
void LocalServer::_GET_config(void) {
if(ag->isOne()) {
server.send(200, "application/json", config.toString());
} else {
server.send(200, "application/json", config.toString(fwMode));
}
}
void LocalServer::_PUT_config(void) {
String data = server.arg(0);
String response = "";
int statusCode = 400; // Status code for data invalid
if (config.parse(data, true)) {
statusCode = 200;
response = "Success";
} else {
response = config.getFailedMesage();
}
server.send(statusCode, "text/plain", response);
}
void LocalServer::_GET_metrics(void) {
server.send(200, openMetrics.getApiContentType(), openMetrics.getPayload());
}
void LocalServer::_GET_measure(void) {
server.send(
200, "application/json",
measure.toString(true, fwMode, wifiConnector.RSSI(), ag, &config));
}
void LocalServer::setFwMode(AgFirmwareMode fwMode) { this->fwMode = fwMode; }

View File

@ -0,0 +1,38 @@
#ifndef _LOCAL_SERVER_H_
#define _LOCAL_SERVER_H_
#include "AgConfigure.h"
#include "AgValue.h"
#include "AirGradient.h"
#include "OpenMetrics.h"
#include "AgWiFiConnector.h"
#include <Arduino.h>
#include <ESP8266WebServer.h>
class LocalServer : public PrintLog {
private:
AirGradient *ag;
OpenMetrics &openMetrics;
Measurements &measure;
Configuration &config;
WifiConnector &wifiConnector;
ESP8266WebServer server;
AgFirmwareMode fwMode;
public:
LocalServer(Stream &log, OpenMetrics &openMetrics, Measurements &measure,
Configuration &config, WifiConnector& wifiConnector);
~LocalServer();
bool begin(void);
void setAirGraident(AirGradient *ag);
String getHostname(void);
void setFwMode(AgFirmwareMode fwMode);
void _handle(void);
void _GET_config(void);
void _PUT_config(void);
void _GET_metrics(void);
void _GET_measure(void);
};
#endif /** _LOCAL_SERVER_H_ */

View File

@ -0,0 +1,186 @@
#include "OpenMetrics.h"
OpenMetrics::OpenMetrics(Measurements &measure, Configuration &config,
WifiConnector &wifiConnector, AgApiClient &apiClient)
: measure(measure), config(config), wifiConnector(wifiConnector),
apiClient(apiClient) {}
OpenMetrics::~OpenMetrics() {}
void OpenMetrics::setAirGradient(AirGradient *ag) { this->ag = ag; }
const char *OpenMetrics::getApiContentType(void) {
return "application/openmetrics-text; version=1.0.0; charset=utf-8";
}
const char *OpenMetrics::getApi(void) { return "/metrics"; }
String OpenMetrics::getPayload(void) {
String response;
String current_metric_name;
const auto add_metric = [&](const String &name, const String &help,
const String &type, const String &unit = "") {
current_metric_name = "airgradient_" + name;
if (!unit.isEmpty())
current_metric_name += "_" + unit;
response += "# HELP " + current_metric_name + " " + help + "\n";
response += "# TYPE " + current_metric_name + " " + type + "\n";
if (!unit.isEmpty())
response += "# UNIT " + current_metric_name + " " + unit + "\n";
};
const auto add_metric_point = [&](const String &labels, const String &value) {
response += current_metric_name + "{" + labels + "} " + value + "\n";
};
add_metric("info", "AirGradient device information", "info");
add_metric_point("airgradient_serial_number=\"" + ag->deviceId() +
"\",airgradient_device_type=\"" + ag->getBoardName() +
"\",airgradient_library_version=\"" + ag->getVersion() +
"\"",
"1");
add_metric("config_ok",
"1 if the AirGradient device was able to successfully fetch its "
"configuration from the server",
"gauge");
add_metric_point("", apiClient.isFetchConfigureFailed() ? "0" : "1");
add_metric(
"post_ok",
"1 if the AirGradient device was able to successfully send to the server",
"gauge");
add_metric_point("", apiClient.isPostToServerFailed() ? "0" : "1");
add_metric(
"wifi_rssi",
"WiFi signal strength from the AirGradient device perspective, in dBm",
"gauge", "dbm");
add_metric_point("", String(wifiConnector.RSSI()));
if (config.hasSensorS8 && measure.CO2 >= 0) {
add_metric("co2",
"Carbon dioxide concentration as measured by the AirGradient S8 "
"sensor, in parts per million",
"gauge", "ppm");
add_metric_point("", String(measure.CO2));
}
float _temp = utils::getInvalidTemperature();
float _hum = utils::getInvalidHumidity();
int pm01 = utils::getInvalidPMS();
int pm25 = utils::getInvalidPMS();
int pm10 = utils::getInvalidPMS();
int pm03PCount = utils::getInvalidPMS();
int atmpCompensated = utils::getInvalidTemperature();
int ahumCompensated = utils::getInvalidHumidity();
if (config.hasSensorSHT) {
_temp = measure.Temperature;
_hum = measure.Humidity;
atmpCompensated = _temp;
ahumCompensated = _hum;
}
if (config.hasSensorPMS1) {
pm01 = measure.pm01_1;
pm25 = measure.pm25_1;
pm10 = measure.pm10_1;
pm03PCount = measure.pm03PCount_1;
}
if (config.hasSensorPMS1) {
if (utils::isValidPMS(pm01)) {
add_metric("pm1",
"PM1.0 concentration as measured by the AirGradient PMS "
"sensor, in micrograms per cubic meter",
"gauge", "ugm3");
add_metric_point("", String(pm01));
}
if (utils::isValidPMS(pm25)) {
add_metric("pm2d5",
"PM2.5 concentration as measured by the AirGradient PMS "
"sensor, in micrograms per cubic meter",
"gauge", "ugm3");
add_metric_point("", String(pm25));
}
if (utils::isValidPMS(pm10)) {
add_metric("pm10",
"PM10 concentration as measured by the AirGradient PMS "
"sensor, in micrograms per cubic meter",
"gauge", "ugm3");
add_metric_point("", String(pm10));
}
if (utils::isValidPMS03Count(pm03PCount)) {
add_metric("pm0d3",
"PM0.3 concentration as measured by the AirGradient PMS "
"sensor, in number of particules per 100 milliliters",
"gauge", "p100ml");
add_metric_point("", String(pm03PCount));
}
}
if (config.hasSensorSGP) {
if (utils::isValidVOC(measure.TVOC)) {
add_metric("tvoc_index",
"The processed Total Volatile Organic Compounds (TVOC) index "
"as measured by the AirGradient SGP sensor",
"gauge");
add_metric_point("", String(measure.TVOC));
}
if (utils::isValidVOC(measure.TVOCRaw)) {
add_metric("tvoc_raw",
"The raw input value to the Total Volatile Organic Compounds "
"(TVOC) index as measured by the AirGradient SGP sensor",
"gauge");
add_metric_point("", String(measure.TVOCRaw));
}
if (utils::isValidNOx(measure.NOx)) {
add_metric("nox_index",
"The processed Nitrous Oxide (NOx) index as measured by the "
"AirGradient SGP sensor",
"gauge");
add_metric_point("", String(measure.NOx));
}
if (utils::isValidNOx(measure.NOxRaw)) {
add_metric("nox_raw",
"The raw input value to the Nitrous Oxide (NOx) index as "
"measured by the AirGradient SGP sensor",
"gauge");
add_metric_point("", String(measure.NOxRaw));
}
}
if (utils::isValidTemperature(_temp)) {
add_metric(
"temperature",
"The ambient temperature as measured by the AirGradient SHT / PMS "
"sensor, in degrees Celsius",
"gauge", "celsius");
add_metric_point("", String(_temp));
}
if (utils::isValidTemperature(atmpCompensated)) {
add_metric("temperature_compensated",
"The compensated ambient temperature as measured by the "
"AirGradient SHT / PMS "
"sensor, in degrees Celsius",
"gauge", "celsius");
add_metric_point("", String(atmpCompensated));
}
if (utils::isValidHumidity(_hum)) {
add_metric(
"humidity",
"The relative humidity as measured by the AirGradient SHT sensor",
"gauge", "percent");
add_metric_point("", String(_hum));
}
if (utils::isValidHumidity(ahumCompensated)) {
add_metric("humidity_compensated",
"The compensated relative humidity as measured by the "
"AirGradient SHT / PMS sensor",
"gauge", "percent");
add_metric_point("", String(ahumCompensated));
}
response += "# EOF\n";
return response;
}

View File

@ -0,0 +1,28 @@
#ifndef _OPEN_METRICS_H_
#define _OPEN_METRICS_H_
#include "AgConfigure.h"
#include "AgValue.h"
#include "AgWiFiConnector.h"
#include "AirGradient.h"
#include "AgApiClient.h"
class OpenMetrics {
private:
AirGradient *ag;
Measurements &measure;
Configuration &config;
WifiConnector &wifiConnector;
AgApiClient &apiClient;
public:
OpenMetrics(Measurements &measure, Configuration &conig,
WifiConnector &wifiConnector, AgApiClient& apiClient);
~OpenMetrics();
void setAirGradient(AirGradient *ag);
const char *getApiContentType(void);
const char* getApi(void);
String getPayload(void);
};
#endif /** _OPEN_METRICS_H_ */

View File

@ -1,667 +0,0 @@
/*
This is the code for the AirGradient DIY BASIC Air Quality Monitor with an D1
ESP8266 Microcontroller.
It is an air quality monitor for PM2.5, CO2, Temperature and Humidity with a
small display and can send data over Wifi.
Open source air quality monitors and kits are available:
Indoor Monitor: https://www.airgradient.com/indoor/
Outdoor Monitor: https://www.airgradient.com/outdoor/
Build Instructions:
https://www.airgradient.com/documentation/diy-v4/
Following libraries need to be installed:
“WifiManager by tzapu, tablatronix” tested with version 2.0.16-rc.2
"Arduino_JSON" by Arduino version 0.2.0
"U8g2" by oliver version 2.34.22
Please make sure you have esp8266 board manager installed. Tested with
version 3.1.2.
Set board to "LOLIN(WEMOS) D1 R2 & mini"
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
can be set through the AirGradient dashboard.
If you have any questions please visit our forum at
https://forum.airgradient.com/
CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
*/
#include <AirGradient.h>
#include <Arduino_JSON.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <WiFiManager.h>
#define WIFI_CONNECT_COUNTDOWN_MAX 180 /** sec */
#define WIFI_CONNECT_RETRY_MS 10000 /** ms */
#define LED_BAR_COUNT_INIT_VALUE (-1) /** */
#define LED_BAR_ANIMATION_PERIOD 100 /** ms */
#define DISP_UPDATE_INTERVAL 5000 /** ms */
#define SERVER_CONFIG_UPDATE_INTERVAL 30000 /** ms */
#define SERVER_SYNC_INTERVAL 60000 /** ms */
#define SENSOR_CO2_CALIB_COUNTDOWN_MAX 5 /** sec */
#define SENSOR_TVOC_UPDATE_INTERVAL 1000 /** ms */
#define SENSOR_CO2_UPDATE_INTERVAL 5000 /** ms */
#define SENSOR_PM_UPDATE_INTERVAL 5000 /** ms */
#define SENSOR_TEMP_HUM_UPDATE_INTERVAL 5000 /** ms */
#define DISPLAY_DELAY_SHOW_CONTENT_MS 2000 /** ms */
#define WIFI_HOTSPOT_PASSWORD_DEFAULT \
"cleanair" /** default WiFi AP password \
*/
/**
* @brief Use use LED bar state
*/
typedef enum {
UseLedBarOff, /** Don't use LED bar */
UseLedBarPM, /** Use LED bar for PMS */
UseLedBarCO2, /** Use LED bar for CO2 */
} UseLedBar;
/**
* @brief Schedule handle with timing period
*
*/
class AgSchedule {
public:
AgSchedule(int period, void (*handler)(void))
: period(period), handler(handler) {}
void run(void) {
uint32_t ms = (uint32_t)(millis() - count);
if (ms >= period) {
/** Call handler */
handler();
Serial.printf("[AgSchedule] handle 0x%08x, period: %d(ms)\r\n",
(unsigned int)handler, period);
/** Update period time */
count = millis();
}
}
private:
void (*handler)(void);
int period;
int count;
};
/**
* @brief AirGradient server configuration and sync data
*
*/
class AgServer {
public:
void begin(void) {
inF = false;
inUSAQI = false;
configFailed = false;
serverFailed = false;
memset(models, 0, sizeof(models));
memset(mqttBroker, 0, sizeof(mqttBroker));
}
/**
* @brief Get server configuration
*
* @param id Device ID
* @return true Success
* @return false Failure
*/
bool pollServerConfig(String id) {
String uri =
"http://hw.airgradient.com/sensors/airgradient:" + id + "/one/config";
/** Init http client */
WiFiClient wifiClient;
HTTPClient client;
if (client.begin(wifiClient, uri) == false) {
configFailed = true;
return false;
}
/** Get */
int retCode = client.GET();
if (retCode != 200) {
client.end();
configFailed = true;
return false;
}
/** clear failed */
configFailed = false;
/** Get response string */
String respContent = client.getString();
client.end();
Serial.println("Get server config: " + respContent);
/** Parse JSON */
JSONVar root = JSON.parse(respContent);
if (JSON.typeof(root) == "undefined") {
/** JSON invalid */
return false;
}
/** Get "country" */
if (JSON.typeof_(root["country"]) == "string") {
String country = root["country"];
if (country == "US") {
inF = true;
} else {
inF = false;
}
}
/** Get "pmStandard" */
if (JSON.typeof_(root["pmStandard"]) == "string") {
String standard = root["pmStandard"];
if (standard == "ugm3") {
inUSAQI = false;
} else {
inUSAQI = true;
}
}
/** Get "co2CalibrationRequested" */
if (JSON.typeof_(root["co2CalibrationRequested"]) == "boolean") {
co2Calib = root["co2CalibrationRequested"];
}
/** Get "ledBarMode" */
if (JSON.typeof_(root["ledBarMode"]) == "string") {
String mode = root["ledBarMode"];
if (mode == "co2") {
ledBarMode = UseLedBarCO2;
} else if (mode == "pm") {
ledBarMode = UseLedBarPM;
} else if (mode == "off") {
ledBarMode = UseLedBarOff;
} else {
ledBarMode = UseLedBarOff;
}
}
/** Get model */
if (JSON.typeof_(root["model"]) == "string") {
String model = root["model"];
if (model.length()) {
int len =
model.length() < sizeof(models) ? model.length() : sizeof(models);
memset(models, 0, sizeof(models));
memcpy(models, model.c_str(), len);
}
}
/** Get "mqttBrokerUrl" */
if (JSON.typeof_(root["mqttBrokerUrl"]) == "string") {
String mqtt = root["mqttBrokerUrl"];
if (mqtt.length()) {
int len = mqtt.length() < sizeof(mqttBroker) ? mqtt.length()
: sizeof(mqttBroker);
memset(mqttBroker, 0, sizeof(mqttBroker));
memcpy(mqttBroker, mqtt.c_str(), len);
}
}
/** Get 'abcDays' */
if (JSON.typeof_(root["abcDays"]) == "number") {
co2AbcCalib = root["abcDays"];
} else {
co2AbcCalib = -1;
}
/** Show configuration */
showServerConfig();
return true;
}
bool postToServer(String id, String payload) {
/**
* @brief Only post data if WiFi is connected
*/
if (WiFi.isConnected() == false) {
return false;
}
Serial.printf("Post payload: %s\r\n", payload.c_str());
String uri =
"http://hw.airgradient.com/sensors/airgradient:" + id + "/measures";
WiFiClient wifiClient;
HTTPClient client;
if (client.begin(wifiClient, uri.c_str()) == false) {
return false;
}
client.addHeader("content-type", "application/json");
int retCode = client.POST(payload);
client.end();
if ((retCode == 200) || (retCode == 429)) {
serverFailed = false;
return true;
}
serverFailed = true;
return false;
}
/**
* @brief Get temperature configuration unit
*
* @return true F unit
* @return false C Unit
*/
bool isTemperatureUnitF(void) { return inF; }
/**
* @brief Get PMS standard unit
*
* @return true USAQI
* @return false ugm3
*/
bool isPMSinUSAQI(void) { return inUSAQI; }
/**
* @brief Get status of get server coniguration is failed
*
* @return true Failed
* @return false Success
*/
bool isConfigFailed(void) { return configFailed; }
/**
* @brief Get status of post server configuration is failed
*
* @return true Failed
* @return false Success
*/
bool isServerFailed(void) { return serverFailed; }
/**
* @brief Get request calibration CO2
*
* @return true Requested. If result = true, it's clear after function call
* @return false Not-requested
*/
bool isCo2Calib(void) {
bool ret = co2Calib;
if (ret) {
co2Calib = false;
}
return ret;
}
/**
* @brief Get the Co2 auto calib period
*
* @return int days, -1 if invalid.
*/
int getCo2Abccalib(void) { return co2AbcCalib; }
/**
* @brief Get device configuration model name
*
* @return String Model name, empty string if server failed
*/
String getModelName(void) { return String(models); }
/**
* @brief Get mqttBroker url
*
* @return String Broker url, empty if server failed
*/
String getMqttBroker(void) { return String(mqttBroker); }
/**
* @brief Show server configuration parameter
*/
void showServerConfig(void) {
Serial.println("Server configuration: ");
Serial.printf(" inF: %s\r\n", inF ? "true" : "false");
Serial.printf(" inUSAQI: %s\r\n", inUSAQI ? "true" : "false");
Serial.printf(" useRGBLedBar: %d\r\n", (int)ledBarMode);
Serial.printf(" Model: %s\r\n", models);
Serial.printf(" Mqtt Broker: %s\r\n", mqttBroker);
Serial.printf(" S8 calib period: %d\r\n", co2AbcCalib);
}
/**
* @brief Get server config led bar mode
*
* @return UseLedBar
*/
UseLedBar getLedBarMode(void) { return ledBarMode; }
private:
bool inF; /** Temperature unit, true: F, false: C */
bool inUSAQI; /** PMS unit, true: USAQI, false: ugm3 */
bool configFailed; /** Flag indicate get server configuration failed */
bool serverFailed; /** Flag indicate post data to server failed */
bool co2Calib; /** Is co2Ppmcalibration requset */
int co2AbcCalib = -1; /** update auto calibration number of day */
UseLedBar ledBarMode = UseLedBarCO2; /** */
char models[20]; /** */
char mqttBroker[256]; /** */
};
AgServer agServer;
/** Create airgradient instance for 'DIY_BASIC' board */
AirGradient ag = AirGradient(DIY_BASIC);
static int co2Ppm = -1;
static int pm25 = -1;
static float temp = -1;
static int hum = -1;
static long val;
static String wifiSSID = "";
static bool wifiHasConfig = false; /** */
static void boardInit(void);
static void failedHandler(String msg);
static void co2Calibration(void);
static void serverConfigPoll(void);
static void co2Poll(void);
static void pmPoll(void);
static void tempHumPoll(void);
static void sendDataToServer(void);
static void dispHandler(void);
static String getDevId(void);
static void updateWiFiConnect(void);
AgSchedule configSchedule(SERVER_CONFIG_UPDATE_INTERVAL, serverConfigPoll);
AgSchedule serverSchedule(SERVER_SYNC_INTERVAL, sendDataToServer);
AgSchedule dispSchedule(DISP_UPDATE_INTERVAL, dispHandler);
AgSchedule co2Schedule(SENSOR_CO2_UPDATE_INTERVAL, co2Poll);
AgSchedule pmsSchedule(SENSOR_PM_UPDATE_INTERVAL, pmPoll);
AgSchedule tempHumSchedule(SENSOR_TEMP_HUM_UPDATE_INTERVAL, tempHumPoll);
void setup() {
Serial.begin(115200);
/** Init I2C */
Wire.begin(ag.getI2cSdaPin(), ag.getI2cSclPin());
delay(1000);
/** Board init */
boardInit();
/** Init AirGradient server */
agServer.begin();
/** Show boot display */
displayShowText("DIY basic", "Lib:" + ag.getVersion(), "");
delay(2000);
/** WiFi connect */
connectToWifi();
if (WiFi.status() == WL_CONNECTED) {
wifiHasConfig = true;
sendPing();
agServer.pollServerConfig(getDevId());
if (agServer.isCo2Calib()) {
co2Calibration();
}
}
/** Show serial number display */
ag.display.clear();
ag.display.setCursor(1, 1);
ag.display.setText("Warm Up");
ag.display.setCursor(1, 15);
ag.display.setText("Serial#");
ag.display.setCursor(1, 29);
String id = getNormalizedMac();
Serial.println("Device id: " + id);
String id1 = id.substring(0, 9);
String id2 = id.substring(9, 12);
ag.display.setText("\'" + id1);
ag.display.setCursor(1, 40);
ag.display.setText(id2 + "\'");
ag.display.show();
delay(5000);
}
void loop() {
configSchedule.run();
serverSchedule.run();
dispSchedule.run();
co2Schedule.run();
pmsSchedule.run();
tempHumSchedule.run();
updateWiFiConnect();
}
static void sendPing() {
JSONVar root;
root["wifi"] = WiFi.RSSI();
root["boot"] = 0;
// delay(1500);
if (agServer.postToServer(getDevId(), JSON.stringify(root))) {
// Ping Server succses
} else {
// Ping server failed
}
// delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
}
void displayShowText(String ln1, String ln2, String ln3) {
char buf[9];
ag.display.clear();
ag.display.setCursor(1, 1);
ag.display.setText(ln1);
ag.display.setCursor(1, 19);
ag.display.setText(ln2);
ag.display.setCursor(1, 37);
ag.display.setText(ln3);
ag.display.show();
}
// Wifi Manager
void connectToWifi() {
WiFiManager wifiManager;
wifiSSID = "AG-" + String(ESP.getChipId(), HEX);
wifiManager.setConfigPortalBlocking(false);
wifiManager.setConfigPortalTimeout(WIFI_CONNECT_COUNTDOWN_MAX);
wifiManager.autoConnect(wifiSSID.c_str(), WIFI_HOTSPOT_PASSWORD_DEFAULT);
uint32_t lastTime = millis();
int count = WIFI_CONNECT_COUNTDOWN_MAX;
displayShowText(String(WIFI_CONNECT_COUNTDOWN_MAX) + " sec",
"SSID:", wifiSSID);
while (wifiManager.getConfigPortalActive()) {
wifiManager.process();
uint32_t ms = (uint32_t)(millis() - lastTime);
if (ms >= 1000) {
lastTime = millis();
displayShowText(String(count) + " sec", "SSID:", wifiSSID);
count--;
// Timeout
if (count == 0) {
break;
}
}
}
if (!WiFi.isConnected()) {
displayShowText("Booting", "offline", "mode");
Serial.println("failed to connect and hit timeout");
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
}
}
static void boardInit(void) {
/** Init SHT sensor */
if (ag.sht.begin(Wire) == false) {
failedHandler("SHT init failed");
}
/** CO2 init */
if (ag.s8.begin(&Serial) == false) {
failedHandler("SenseAirS8 init failed");
}
/** PMS init */
if (ag.pms5003.begin(&Serial) == false) {
failedHandler("PMS5003 init failed");
}
/** Display init */
ag.display.begin(Wire);
ag.display.setTextColor(1);
}
static void failedHandler(String msg) {
while (true) {
Serial.println(msg);
delay(1000);
}
}
static void co2Calibration(void) {
/** Count down for co2CalibCountdown secs */
for (int i = 0; i < SENSOR_CO2_CALIB_COUNTDOWN_MAX; i++) {
displayShowText("CO2 calib", "after",
String(SENSOR_CO2_CALIB_COUNTDOWN_MAX - i) + " sec");
delay(1000);
}
if (ag.s8.setBaselineCalibration()) {
displayShowText("Calib", "success", "");
delay(1000);
displayShowText("Wait for", "finish", "...");
int count = 0;
while (ag.s8.isBaseLineCalibrationDone() == false) {
delay(1000);
count++;
}
displayShowText("Finish", "after", String(count) + " sec");
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
} else {
displayShowText("Calib", "failure!!!", "");
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
}
}
static void serverConfigPoll(void) {
if (agServer.pollServerConfig(getDevId())) {
if (agServer.isCo2Calib()) {
co2Calibration();
}
if (agServer.getCo2Abccalib() > 0) {
if (ag.s8.setAutoCalib(agServer.getCo2Abccalib() * 24) == false) {
Serial.println("Set S8 auto calib failed");
}
}
}
}
static void co2Poll() {
co2Ppm = ag.s8.getCo2();
Serial.printf("CO2 index: %d\r\n", co2Ppm);
}
void pmPoll() {
if (ag.pms5003.readData()) {
pm25 = ag.pms5003.getPm25Ae();
Serial.printf("PMS2.5: %d\r\n", pm25);
} else {
pm25 = -1;
}
}
static void tempHumPoll() {
if (ag.sht.measure()) {
temp = ag.sht.getTemperature();
hum = ag.sht.getRelativeHumidity();
Serial.printf("Temperature: %0.2f\r\n", temp);
Serial.printf(" Humidity: %d\r\n", hum);
} else {
Serial.println("Meaure SHT failed");
}
}
static void sendDataToServer() {
JSONVar root;
root["wifi"] = WiFi.RSSI();
if (co2Ppm >= 0) {
root["rco2"] = co2Ppm;
}
if (pm25 >= 0) {
root["pm02"] = pm25;
}
if (temp >= 0) {
root["atmp"] = temp;
}
if (hum >= 0) {
root["rhum"] = hum;
}
if (agServer.postToServer(getDevId(), JSON.stringify(root)) == false) {
Serial.println("Post to server failed");
}
}
static void dispHandler() {
String ln1 = "";
String ln2 = "";
String ln3 = "";
if (agServer.isPMSinUSAQI()) {
ln1 = "AQI:" + String(ag.pms5003.convertPm25ToUsAqi(pm25));
} else {
ln1 = "PM :" + String(pm25) + " ug";
}
ln2 = "CO2:" + String(co2Ppm);
if (agServer.isTemperatureUnitF()) {
ln3 = String((temp * 9 / 5) + 32).substring(0, 4) + " " + String(hum) + "%";
} else {
ln3 = String(temp).substring(0, 4) + " " + String(hum) + "%";
}
displayShowText(ln1, ln2, ln3);
}
static String getDevId(void) { return getNormalizedMac(); }
/**
* @brief WiFi reconnect handler
*/
static void updateWiFiConnect(void) {
static uint32_t lastRetry;
if (wifiHasConfig == false) {
return;
}
if (WiFi.isConnected()) {
lastRetry = millis();
return;
}
uint32_t ms = (uint32_t)(millis() - lastRetry);
if (ms >= WIFI_CONNECT_RETRY_MS) {
lastRetry = millis();
WiFi.reconnect();
Serial.printf("Re-Connect WiFi\r\n");
}
}
String getNormalizedMac() {
String mac = WiFi.macAddress();
mac.replace(":", "");
mac.toLowerCase();
return mac;
}

View File

@ -0,0 +1,625 @@
/*
This is the code for the AirGradient DIY PRO 3.3 Air Quality Monitor with an D1
ESP8266 Microcontroller.
It is an air quality monitor for PM2.5, CO2, Temperature and Humidity with a
small display and can send data over Wifi.
Open source air quality monitors and kits are available:
Indoor Monitor: https://www.airgradient.com/indoor/
Outdoor Monitor: https://www.airgradient.com/outdoor/
Build Instructions:
https://www.airgradient.com/documentation/diy-v4/
Please make sure you have esp8266 board manager installed. Tested with
version 3.1.2.
Set board to "LOLIN(WEMOS) D1 R2 & mini"
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
can be set through the AirGradient dashboard.
If you have any questions please visit our forum at
https://forum.airgradient.com/
CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
*/
#include "AgApiClient.h"
#include "AgConfigure.h"
#include "AgSchedule.h"
#include "AgWiFiConnector.h"
#include "LocalServer.h"
#include "OpenMetrics.h"
#include "MqttClient.h"
#include <AirGradient.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <WiFiClient.h>
#define LED_BAR_ANIMATION_PERIOD 100 /** ms */
#define DISP_UPDATE_INTERVAL 2500 /** ms */
#define SERVER_CONFIG_SYNC_INTERVAL 60000 /** ms */
#define SERVER_SYNC_INTERVAL 60000 /** ms */
#define MQTT_SYNC_INTERVAL 60000 /** ms */
#define SENSOR_CO2_CALIB_COUNTDOWN_MAX 5 /** sec */
#define SENSOR_TVOC_UPDATE_INTERVAL 1000 /** ms */
#define SENSOR_CO2_UPDATE_INTERVAL 4000 /** ms */
#define SENSOR_PM_UPDATE_INTERVAL 2000 /** ms */
#define SENSOR_TEMP_HUM_UPDATE_INTERVAL 2000 /** ms */
#define DISPLAY_DELAY_SHOW_CONTENT_MS 2000 /** ms */
#define FIRMWARE_CHECK_FOR_UPDATE_MS (60 * 60 * 1000) /** ms */
static AirGradient ag(DIY_PRO_INDOOR_V3_3);
static Configuration configuration(Serial);
static AgApiClient apiClient(Serial, configuration);
static Measurements measurements;
static OledDisplay oledDisplay(configuration, measurements, Serial);
static StateMachine stateMachine(oledDisplay, Serial, measurements,
configuration);
static WifiConnector wifiConnector(oledDisplay, Serial, stateMachine,
configuration);
static OpenMetrics openMetrics(measurements, configuration, wifiConnector,
apiClient);
static LocalServer localServer(Serial, openMetrics, measurements, configuration,
wifiConnector);
static MqttClient mqttClient(Serial);
static int pmFailCount = 0;
static int getCO2FailCount = 0;
static AgFirmwareMode fwMode = FW_MODE_I_33PS;
static String fwNewVersion;
static void boardInit(void);
static void failedHandler(String msg);
static void configurationUpdateSchedule(void);
static void appDispHandler(void);
static void oledDisplaySchedule(void);
static void updateTvoc(void);
static void updatePm(void);
static void sendDataToServer(void);
static void tempHumUpdate(void);
static void co2Update(void);
static void mdnsInit(void);
static void initMqtt(void);
static void factoryConfigReset(void);
static void wdgFeedUpdate(void);
static bool sgp41Init(void);
static void wifiFactoryConfigure(void);
static void mqttHandle(void);
AgSchedule dispLedSchedule(DISP_UPDATE_INTERVAL, oledDisplaySchedule);
AgSchedule configSchedule(SERVER_CONFIG_SYNC_INTERVAL,
configurationUpdateSchedule);
AgSchedule agApiPostSchedule(SERVER_SYNC_INTERVAL, sendDataToServer);
AgSchedule co2Schedule(SENSOR_CO2_UPDATE_INTERVAL, co2Update);
AgSchedule pmsSchedule(SENSOR_PM_UPDATE_INTERVAL, updatePm);
AgSchedule tempHumSchedule(SENSOR_TEMP_HUM_UPDATE_INTERVAL, tempHumUpdate);
AgSchedule tvocSchedule(SENSOR_TVOC_UPDATE_INTERVAL, updateTvoc);
AgSchedule watchdogFeedSchedule(60000, wdgFeedUpdate);
AgSchedule mqttSchedule(MQTT_SYNC_INTERVAL, mqttHandle);
void setup() {
/** Serial for print debug message */
Serial.begin(115200);
delay(100); /** For bester show log */
/** Print device ID into log */
Serial.println("Serial nr: " + ag.deviceId());
/** Initialize local configure */
configuration.begin();
/** Init I2C */
Wire.begin(ag.getI2cSdaPin(), ag.getI2cSclPin());
delay(1000);
configuration.setAirGradient(&ag);
oledDisplay.setAirGradient(&ag);
stateMachine.setAirGradient(&ag);
wifiConnector.setAirGradient(&ag);
apiClient.setAirGradient(&ag);
openMetrics.setAirGradient(&ag);
localServer.setAirGraident(&ag);
/** Example set custom API root URL */
// apiClient.setApiRoot("https://example.custom.api");
/** Init sensor */
boardInit();
/** Connecting wifi */
bool connectToWifi = false;
connectToWifi = !configuration.isOfflineMode();
if (connectToWifi) {
apiClient.begin();
if (wifiConnector.connect()) {
if (wifiConnector.isConnected()) {
mdnsInit();
localServer.begin();
initMqtt();
sendDataToAg();
apiClient.fetchServerConfiguration();
configSchedule.update();
if (apiClient.isFetchConfigureFailed()) {
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
stateMachine.displayHandle(
AgStateMachineWiFiOkServerOkSensorConfigFailed);
} else {
stateMachine.displayClearAddToDashBoard();
}
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
}
} else {
if (wifiConnector.isConfigurePorttalTimeout()) {
oledDisplay.showRebooting();
delay(2500);
oledDisplay.setText("", "", "");
ESP.restart();
}
}
}
}
/** Set offline mode without saving, cause wifi is not configured */
if (wifiConnector.hasConfigurated() == false) {
Serial.println("Set offline mode cause wifi is not configurated");
configuration.setOfflineModeWithoutSave(true);
}
/** Show display Warning up */
oledDisplay.setText("Warming Up", "Serial Number:", ag.deviceId().c_str());
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
Serial.println("Display brightness: " +
String(configuration.getDisplayBrightness()));
oledDisplay.setBrightness(configuration.getDisplayBrightness());
appDispHandler();
}
void loop() {
/** Handle schedule */
dispLedSchedule.run();
configSchedule.run();
agApiPostSchedule.run();
if (configuration.hasSensorS8) {
co2Schedule.run();
}
if (configuration.hasSensorPMS1) {
pmsSchedule.run();
ag.pms5003.handle();
}
if (configuration.hasSensorSHT) {
tempHumSchedule.run();
}
if (configuration.hasSensorSGP) {
tvocSchedule.run();
}
/** Auto reset watchdog timer if offline mode or postDataToAirGradient */
if (configuration.isOfflineMode() ||
(configuration.isPostDataToAirGradient() == false)) {
watchdogFeedSchedule.run();
}
/** Check for handle WiFi reconnect */
wifiConnector.handle();
/** factory reset handle */
// factoryConfigReset();
/** check that local configura changed then do some action */
configUpdateHandle();
localServer._handle();
if (configuration.hasSensorSGP) {
ag.sgp41.handle();
}
MDNS.update();
mqttSchedule.run();
mqttClient.handle();
}
static void co2Update(void) {
int value = ag.s8.getCo2();
if (utils::isValidCO2(value)) {
measurements.CO2 = value;
getCO2FailCount = 0;
Serial.printf("CO2 (ppm): %d\r\n", measurements.CO2);
} else {
getCO2FailCount++;
Serial.printf("Get CO2 failed: %d\r\n", getCO2FailCount);
if (getCO2FailCount >= 3) {
measurements.CO2 = utils::getInvalidCO2();
}
}
}
static void mdnsInit(void) {
Serial.println("mDNS init");
if (!MDNS.begin(localServer.getHostname().c_str())) {
Serial.println("Init mDNS failed");
return;
}
MDNS.addService("_airgradient", "_tcp", 80);
MDNS.addServiceTxt("_airgradient", "_tcp", "model",
AgFirmwareModeName(fwMode));
MDNS.addServiceTxt("_airgradient", "_tcp", "serialno", ag.deviceId());
MDNS.addServiceTxt("_airgradient", "_tcp", "fw_ver", ag.getVersion());
MDNS.addServiceTxt("_airgradient", "_tcp", "vendor", "AirGradient");
MDNS.announce();
}
static void initMqtt(void) {
if (mqttClient.begin(configuration.getMqttBrokerUri())) {
Serial.println("Setup connect to MQTT broker successful");
} else {
Serial.println("setup Connect to MQTT broker failed");
}
}
static void factoryConfigReset(void) {
#if 0
if (ag.button.getState() == ag.button.BUTTON_PRESSED) {
if (factoryBtnPressTime == 0) {
factoryBtnPressTime = millis();
} else {
uint32_t ms = (uint32_t)(millis() - factoryBtnPressTime);
if (ms >= 2000) {
// Show display message: For factory keep for x seconds
if (ag.isOne() || ag.isPro4_2()) {
oledDisplay.setText("Factory reset", "keep pressed", "for 8 sec");
} else {
Serial.println("Factory reset, keep pressed for 8 sec");
}
int count = 7;
while (ag.button.getState() == ag.button.BUTTON_PRESSED) {
delay(1000);
String str = "for " + String(count) + " sec";
oledDisplay.setText("Factory reset", "keep pressed", str.c_str());
count--;
if (count == 0) {
/** Stop MQTT task first */
// if (mqttTask) {
// vTaskDelete(mqttTask);
// mqttTask = NULL;
// }
/** Reset WIFI */
// WiFi.enableSTA(true); // Incase offline mode
// WiFi.disconnect(true, true);
wifiConnector.reset();
/** Reset local config */
configuration.reset();
oledDisplay.setText("Factory reset", "successful", "");
delay(3000);
oledDisplay.setText("", "", "");
ESP.restart();
}
}
/** Show current content cause reset ignore */
factoryBtnPressTime = 0;
appDispHandler();
}
}
} else {
if (factoryBtnPressTime != 0) {
appDispHandler();
}
factoryBtnPressTime = 0;
}
#endif
}
static void wdgFeedUpdate(void) {
ag.watchdog.reset();
Serial.println();
Serial.println("Offline mode or isPostToAirGradient = false: watchdog reset");
Serial.println();
}
static bool sgp41Init(void) {
ag.sgp41.setNoxLearningOffset(configuration.getNoxLearningOffset());
ag.sgp41.setTvocLearningOffset(configuration.getTvocLearningOffset());
if (ag.sgp41.begin(Wire)) {
Serial.println("Init SGP41 success");
configuration.hasSensorSGP = true;
return true;
} else {
Serial.println("Init SGP41 failuire");
configuration.hasSensorSGP = false;
}
return false;
}
static void wifiFactoryConfigure(void) {
WiFi.persistent(true);
WiFi.begin("airgradient", "cleanair");
WiFi.persistent(false);
oledDisplay.setText("Configure WiFi", "connect to", "\'airgradient\'");
delay(2500);
oledDisplay.setText("Rebooting...", "", "");
delay(2500);
oledDisplay.setText("", "", "");
ESP.restart();
}
static void mqttHandle(void) {
if(mqttClient.isConnected() == false) {
mqttClient.connect(String("airgradient-") + ag.deviceId());
}
if (mqttClient.isConnected()) {
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI(),
&ag, &configuration);
String topic = "airgradient/readings/" + ag.deviceId();
if (mqttClient.publish(topic.c_str(), payload.c_str(), payload.length())) {
Serial.println("MQTT sync success");
} else {
Serial.println("MQTT sync failure");
}
}
}
static void sendDataToAg() {
/** Change oledDisplay and led state */
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting);
delay(1500);
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount)) {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected);
} else {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed);
}
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
}
void dispSensorNotFound(String ss) {
ss = ss + " not found";
oledDisplay.setText("Sensor init", "Error:", ss.c_str());
delay(2000);
}
static void boardInit(void) {
/** Display init */
oledDisplay.begin();
/** Show boot display */
Serial.println("Firmware Version: " + ag.getVersion());
oledDisplay.setText("AirGradient ONE",
"FW Version: ", ag.getVersion().c_str());
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
ag.watchdog.begin();
/** Show message init sensor */
oledDisplay.setText("Sensor", "initializing...", "");
/** Init sensor SGP41 */
if (sgp41Init() == false) {
dispSensorNotFound("SGP41");
}
/** Init SHT */
if (ag.sht.begin(Wire) == false) {
Serial.println("SHTx sensor not found");
configuration.hasSensorSHT = false;
dispSensorNotFound("SHT");
}
/** Init S8 CO2 sensor */
if (ag.s8.begin(&Serial) == false) {
Serial.println("CO2 S8 sensor not found");
configuration.hasSensorS8 = false;
dispSensorNotFound("S8");
}
/** Init PMS5003 */
configuration.hasSensorPMS1 = true;
configuration.hasSensorPMS2 = false;
if (ag.pms5003.begin(&Serial) == false) {
Serial.println("PMS sensor not found");
configuration.hasSensorPMS1 = false;
dispSensorNotFound("PMS");
}
/** Set S8 CO2 abc days period */
if (configuration.hasSensorS8) {
if (ag.s8.setAbcPeriod(configuration.getCO2CalibrationAbcDays() * 24)) {
Serial.println("Set S8 AbcDays successful");
} else {
Serial.println("Set S8 AbcDays failure");
}
}
localServer.setFwMode(FW_MODE_I_33PS);
}
static void failedHandler(String msg) {
while (true) {
Serial.println(msg);
delay(1000);
}
}
static void configurationUpdateSchedule(void) {
if (apiClient.fetchServerConfiguration()) {
configUpdateHandle();
}
}
static void configUpdateHandle() {
if (configuration.isUpdated() == false) {
return;
}
stateMachine.executeCo2Calibration();
String mqttUri = configuration.getMqttBrokerUri();
if (mqttClient.isCurrentUri(mqttUri) == false) {
mqttClient.end();
initMqtt();
}
if (configuration.hasSensorSGP) {
if (configuration.noxLearnOffsetChanged() ||
configuration.tvocLearnOffsetChanged()) {
ag.sgp41.end();
int oldTvocOffset = ag.sgp41.getTvocLearningOffset();
int oldNoxOffset = ag.sgp41.getNoxLearningOffset();
bool result = sgp41Init();
const char *resultStr = "successful";
if (!result) {
resultStr = "failure";
}
if (oldTvocOffset != configuration.getTvocLearningOffset()) {
Serial.printf("Setting tvocLearningOffset from %d to %d hours %s\r\n",
oldTvocOffset, configuration.getTvocLearningOffset(),
resultStr);
}
if (oldNoxOffset != configuration.getNoxLearningOffset()) {
Serial.printf("Setting noxLearningOffset from %d to %d hours %s\r\n",
oldNoxOffset, configuration.getNoxLearningOffset(),
resultStr);
}
}
}
if (configuration.isDisplayBrightnessChanged()) {
oledDisplay.setBrightness(configuration.getDisplayBrightness());
}
appDispHandler();
}
static void appDispHandler(void) {
AgStateMachineState state = AgStateMachineNormal;
/** Only show display status on online mode. */
if (configuration.isOfflineMode() == false) {
if (wifiConnector.isConnected() == false) {
state = AgStateMachineWiFiLost;
} else if (apiClient.isFetchConfigureFailed()) {
state = AgStateMachineSensorConfigFailed;
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
} else {
stateMachine.displayClearAddToDashBoard();
}
} else if (apiClient.isPostToServerFailed()) {
state = AgStateMachineServerLost;
}
}
stateMachine.displayHandle(state);
}
static void oledDisplaySchedule(void) {
appDispHandler();
}
static void updateTvoc(void) {
measurements.TVOC = ag.sgp41.getTvocIndex();
measurements.TVOCRaw = ag.sgp41.getTvocRaw();
measurements.NOx = ag.sgp41.getNoxIndex();
measurements.NOxRaw = ag.sgp41.getNoxRaw();
Serial.println();
Serial.printf("TVOC index: %d\r\n", measurements.TVOC);
Serial.printf("TVOC raw: %d\r\n", measurements.TVOCRaw);
Serial.printf("NOx index: %d\r\n", measurements.NOx);
Serial.printf("NOx raw: %d\r\n", measurements.NOxRaw);
}
static void updatePm(void) {
if (ag.pms5003.isFailed() == false) {
measurements.pm01_1 = ag.pms5003.getPm01Ae();
measurements.pm25_1 = ag.pms5003.getPm25Ae();
measurements.pm10_1 = ag.pms5003.getPm10Ae();
measurements.pm03PCount_1 = ag.pms5003.getPm03ParticleCount();
Serial.println();
Serial.printf("PM1 ug/m3: %d\r\n", measurements.pm01_1);
Serial.printf("PM2.5 ug/m3: %d\r\n", measurements.pm25_1);
Serial.printf("PM10 ug/m3: %d\r\n", measurements.pm10_1);
Serial.printf("PM0.3 Count: %d\r\n", measurements.pm03PCount_1);
pmFailCount = 0;
} else {
pmFailCount++;
Serial.printf("PMS read failed: %d\r\n", pmFailCount);
if (pmFailCount >= 3) {
measurements.pm01_1 = utils::getInvalidPMS();
measurements.pm25_1 = utils::getInvalidPMS();
measurements.pm10_1 = utils::getInvalidPMS();
measurements.pm03PCount_1 = utils::getInvalidPMS();
}
}
}
static void sendDataToServer(void) {
/** Ignore send data to server if postToAirGradient disabled */
if (configuration.isPostDataToAirGradient() == false ||
configuration.isOfflineMode()) {
return;
}
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI(),
&ag, &configuration);
if (apiClient.postToServer(syncData)) {
ag.watchdog.reset();
Serial.println();
Serial.println(
"Online mode and isPostToAirGradient = true: watchdog reset");
Serial.println();
}
measurements.bootCount++;
}
static void tempHumUpdate(void) {
delay(100);
if (ag.sht.measure()) {
measurements.Temperature = ag.sht.getTemperature();
measurements.Humidity = ag.sht.getRelativeHumidity();
Serial.printf("Temperature in C: %0.2f\r\n", measurements.Temperature);
Serial.printf("Relative Humidity: %d\r\n", measurements.Humidity);
Serial.printf("Temperature compensated in C: %0.2f\r\n",
measurements.Temperature);
Serial.printf("Relative Humidity compensated: %d\r\n",
measurements.Humidity);
// Update compensation temperature and humidity for SGP41
if (configuration.hasSensorSGP) {
ag.sgp41.setCompensationTemperatureHumidity(measurements.Temperature,
measurements.Humidity);
}
} else {
Serial.println("SHT read failed");
measurements.Temperature = utils::getInvalidTemperature();
measurements.Humidity = utils::getInvalidHumidity();
}
}

View File

@ -0,0 +1,61 @@
#include "LocalServer.h"
LocalServer::LocalServer(Stream &log, OpenMetrics &openMetrics,
Measurements &measure, Configuration &config,
WifiConnector &wifiConnector)
: PrintLog(log, "LocalServer"), openMetrics(openMetrics), measure(measure),
config(config), wifiConnector(wifiConnector), server(80) {}
LocalServer::~LocalServer() {}
bool LocalServer::begin(void) {
server.on("/measures/current", HTTP_GET, [this]() { _GET_measure(); });
server.on(openMetrics.getApi(), HTTP_GET, [this]() { _GET_metrics(); });
server.on("/config", HTTP_GET, [this]() { _GET_config(); });
server.on("/config", HTTP_PUT, [this]() { _PUT_config(); });
server.begin();
logInfo("Init: " + getHostname() + ".local");
return true;
}
void LocalServer::setAirGraident(AirGradient *ag) { this->ag = ag; }
String LocalServer::getHostname(void) {
return "airgradient_" + ag->deviceId();
}
void LocalServer::_handle(void) { server.handleClient(); }
void LocalServer::_GET_config(void) {
if(ag->isOne()) {
server.send(200, "application/json", config.toString());
} else {
server.send(200, "application/json", config.toString(fwMode));
}
}
void LocalServer::_PUT_config(void) {
String data = server.arg(0);
String response = "";
int statusCode = 400; // Status code for data invalid
if (config.parse(data, true)) {
statusCode = 200;
response = "Success";
} else {
response = config.getFailedMesage();
}
server.send(statusCode, "text/plain", response);
}
void LocalServer::_GET_metrics(void) {
server.send(200, openMetrics.getApiContentType(), openMetrics.getPayload());
}
void LocalServer::_GET_measure(void) {
server.send(
200, "application/json",
measure.toString(true, fwMode, wifiConnector.RSSI(), ag, &config));
}
void LocalServer::setFwMode(AgFirmwareMode fwMode) { this->fwMode = fwMode; }

View File

@ -0,0 +1,38 @@
#ifndef _LOCAL_SERVER_H_
#define _LOCAL_SERVER_H_
#include "AgConfigure.h"
#include "AgValue.h"
#include "AirGradient.h"
#include "OpenMetrics.h"
#include "AgWiFiConnector.h"
#include <Arduino.h>
#include <ESP8266WebServer.h>
class LocalServer : public PrintLog {
private:
AirGradient *ag;
OpenMetrics &openMetrics;
Measurements &measure;
Configuration &config;
WifiConnector &wifiConnector;
ESP8266WebServer server;
AgFirmwareMode fwMode;
public:
LocalServer(Stream &log, OpenMetrics &openMetrics, Measurements &measure,
Configuration &config, WifiConnector& wifiConnector);
~LocalServer();
bool begin(void);
void setAirGraident(AirGradient *ag);
String getHostname(void);
void setFwMode(AgFirmwareMode fwMode);
void _handle(void);
void _GET_config(void);
void _PUT_config(void);
void _GET_metrics(void);
void _GET_measure(void);
};
#endif /** _LOCAL_SERVER_H_ */

View File

@ -0,0 +1,186 @@
#include "OpenMetrics.h"
OpenMetrics::OpenMetrics(Measurements &measure, Configuration &config,
WifiConnector &wifiConnector, AgApiClient &apiClient)
: measure(measure), config(config), wifiConnector(wifiConnector),
apiClient(apiClient) {}
OpenMetrics::~OpenMetrics() {}
void OpenMetrics::setAirGradient(AirGradient *ag) { this->ag = ag; }
const char *OpenMetrics::getApiContentType(void) {
return "application/openmetrics-text; version=1.0.0; charset=utf-8";
}
const char *OpenMetrics::getApi(void) { return "/metrics"; }
String OpenMetrics::getPayload(void) {
String response;
String current_metric_name;
const auto add_metric = [&](const String &name, const String &help,
const String &type, const String &unit = "") {
current_metric_name = "airgradient_" + name;
if (!unit.isEmpty())
current_metric_name += "_" + unit;
response += "# HELP " + current_metric_name + " " + help + "\n";
response += "# TYPE " + current_metric_name + " " + type + "\n";
if (!unit.isEmpty())
response += "# UNIT " + current_metric_name + " " + unit + "\n";
};
const auto add_metric_point = [&](const String &labels, const String &value) {
response += current_metric_name + "{" + labels + "} " + value + "\n";
};
add_metric("info", "AirGradient device information", "info");
add_metric_point("airgradient_serial_number=\"" + ag->deviceId() +
"\",airgradient_device_type=\"" + ag->getBoardName() +
"\",airgradient_library_version=\"" + ag->getVersion() +
"\"",
"1");
add_metric("config_ok",
"1 if the AirGradient device was able to successfully fetch its "
"configuration from the server",
"gauge");
add_metric_point("", apiClient.isFetchConfigureFailed() ? "0" : "1");
add_metric(
"post_ok",
"1 if the AirGradient device was able to successfully send to the server",
"gauge");
add_metric_point("", apiClient.isPostToServerFailed() ? "0" : "1");
add_metric(
"wifi_rssi",
"WiFi signal strength from the AirGradient device perspective, in dBm",
"gauge", "dbm");
add_metric_point("", String(wifiConnector.RSSI()));
if (config.hasSensorS8 && measure.CO2 >= 0) {
add_metric("co2",
"Carbon dioxide concentration as measured by the AirGradient S8 "
"sensor, in parts per million",
"gauge", "ppm");
add_metric_point("", String(measure.CO2));
}
float _temp = utils::getInvalidTemperature();
float _hum = utils::getInvalidHumidity();
int pm01 = utils::getInvalidPMS();
int pm25 = utils::getInvalidPMS();
int pm10 = utils::getInvalidPMS();
int pm03PCount = utils::getInvalidPMS();
int atmpCompensated = utils::getInvalidTemperature();
int ahumCompensated = utils::getInvalidHumidity();
if (config.hasSensorSHT) {
_temp = measure.Temperature;
_hum = measure.Humidity;
atmpCompensated = _temp;
ahumCompensated = _hum;
}
if (config.hasSensorPMS1) {
pm01 = measure.pm01_1;
pm25 = measure.pm25_1;
pm10 = measure.pm10_1;
pm03PCount = measure.pm03PCount_1;
}
if (config.hasSensorPMS1) {
if (utils::isValidPMS(pm01)) {
add_metric("pm1",
"PM1.0 concentration as measured by the AirGradient PMS "
"sensor, in micrograms per cubic meter",
"gauge", "ugm3");
add_metric_point("", String(pm01));
}
if (utils::isValidPMS(pm25)) {
add_metric("pm2d5",
"PM2.5 concentration as measured by the AirGradient PMS "
"sensor, in micrograms per cubic meter",
"gauge", "ugm3");
add_metric_point("", String(pm25));
}
if (utils::isValidPMS(pm10)) {
add_metric("pm10",
"PM10 concentration as measured by the AirGradient PMS "
"sensor, in micrograms per cubic meter",
"gauge", "ugm3");
add_metric_point("", String(pm10));
}
if (utils::isValidPMS03Count(pm03PCount)) {
add_metric("pm0d3",
"PM0.3 concentration as measured by the AirGradient PMS "
"sensor, in number of particules per 100 milliliters",
"gauge", "p100ml");
add_metric_point("", String(pm03PCount));
}
}
if (config.hasSensorSGP) {
if (utils::isValidVOC(measure.TVOC)) {
add_metric("tvoc_index",
"The processed Total Volatile Organic Compounds (TVOC) index "
"as measured by the AirGradient SGP sensor",
"gauge");
add_metric_point("", String(measure.TVOC));
}
if (utils::isValidVOC(measure.TVOCRaw)) {
add_metric("tvoc_raw",
"The raw input value to the Total Volatile Organic Compounds "
"(TVOC) index as measured by the AirGradient SGP sensor",
"gauge");
add_metric_point("", String(measure.TVOCRaw));
}
if (utils::isValidNOx(measure.NOx)) {
add_metric("nox_index",
"The processed Nitrous Oxide (NOx) index as measured by the "
"AirGradient SGP sensor",
"gauge");
add_metric_point("", String(measure.NOx));
}
if (utils::isValidNOx(measure.NOxRaw)) {
add_metric("nox_raw",
"The raw input value to the Nitrous Oxide (NOx) index as "
"measured by the AirGradient SGP sensor",
"gauge");
add_metric_point("", String(measure.NOxRaw));
}
}
if (utils::isValidTemperature(_temp)) {
add_metric(
"temperature",
"The ambient temperature as measured by the AirGradient SHT / PMS "
"sensor, in degrees Celsius",
"gauge", "celsius");
add_metric_point("", String(_temp));
}
if (utils::isValidTemperature(atmpCompensated)) {
add_metric("temperature_compensated",
"The compensated ambient temperature as measured by the "
"AirGradient SHT / PMS "
"sensor, in degrees Celsius",
"gauge", "celsius");
add_metric_point("", String(atmpCompensated));
}
if (utils::isValidHumidity(_hum)) {
add_metric(
"humidity",
"The relative humidity as measured by the AirGradient SHT sensor",
"gauge", "percent");
add_metric_point("", String(_hum));
}
if (utils::isValidHumidity(ahumCompensated)) {
add_metric("humidity_compensated",
"The compensated relative humidity as measured by the "
"AirGradient SHT / PMS sensor",
"gauge", "percent");
add_metric_point("", String(ahumCompensated));
}
response += "# EOF\n";
return response;
}

View File

@ -0,0 +1,28 @@
#ifndef _OPEN_METRICS_H_
#define _OPEN_METRICS_H_
#include "AgConfigure.h"
#include "AgValue.h"
#include "AgWiFiConnector.h"
#include "AirGradient.h"
#include "AgApiClient.h"
class OpenMetrics {
private:
AirGradient *ag;
Measurements &measure;
Configuration &config;
WifiConnector &wifiConnector;
AgApiClient &apiClient;
public:
OpenMetrics(Measurements &measure, Configuration &conig,
WifiConnector &wifiConnector, AgApiClient& apiClient);
~OpenMetrics();
void setAirGradient(AirGradient *ag);
const char *getApiContentType(void);
const char* getApi(void);
String getPayload(void);
};
#endif /** _OPEN_METRICS_H_ */

View File

@ -0,0 +1,668 @@
/*
This is the code for the AirGradient DIY PRO 4.2 Air Quality Monitor with an D1
ESP8266 Microcontroller.
It is an air quality monitor for PM2.5, CO2, Temperature and Humidity with a
small display and can send data over Wifi.
Open source air quality monitors and kits are available:
Indoor Monitor: https://www.airgradient.com/indoor/
Outdoor Monitor: https://www.airgradient.com/outdoor/
Build Instructions:
https://www.airgradient.com/documentation/diy-v4/
Please make sure you have esp8266 board manager installed. Tested with
version 3.1.2.
Set board to "LOLIN(WEMOS) D1 R2 & mini"
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
can be set through the AirGradient dashboard.
If you have any questions please visit our forum at
https://forum.airgradient.com/
CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
*/
#include "AgApiClient.h"
#include "AgConfigure.h"
#include "AgSchedule.h"
#include "AgWiFiConnector.h"
#include "LocalServer.h"
#include "OpenMetrics.h"
#include "MqttClient.h"
#include <AirGradient.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <WiFiClient.h>
#define LED_BAR_ANIMATION_PERIOD 100 /** ms */
#define DISP_UPDATE_INTERVAL 2500 /** ms */
#define SERVER_CONFIG_SYNC_INTERVAL 60000 /** ms */
#define SERVER_SYNC_INTERVAL 60000 /** ms */
#define MQTT_SYNC_INTERVAL 60000 /** ms */
#define SENSOR_CO2_CALIB_COUNTDOWN_MAX 5 /** sec */
#define SENSOR_TVOC_UPDATE_INTERVAL 1000 /** ms */
#define SENSOR_CO2_UPDATE_INTERVAL 4000 /** ms */
#define SENSOR_PM_UPDATE_INTERVAL 2000 /** ms */
#define SENSOR_TEMP_HUM_UPDATE_INTERVAL 2000 /** ms */
#define DISPLAY_DELAY_SHOW_CONTENT_MS 2000 /** ms */
#define FIRMWARE_CHECK_FOR_UPDATE_MS (60 * 60 * 1000) /** ms */
static AirGradient ag(DIY_PRO_INDOOR_V4_2);
static Configuration configuration(Serial);
static AgApiClient apiClient(Serial, configuration);
static Measurements measurements;
static OledDisplay oledDisplay(configuration, measurements, Serial);
static StateMachine stateMachine(oledDisplay, Serial, measurements,
configuration);
static WifiConnector wifiConnector(oledDisplay, Serial, stateMachine,
configuration);
static OpenMetrics openMetrics(measurements, configuration, wifiConnector,
apiClient);
static LocalServer localServer(Serial, openMetrics, measurements, configuration,
wifiConnector);
static MqttClient mqttClient(Serial);
static int pmFailCount = 0;
static uint32_t factoryBtnPressTime = 0;
static int getCO2FailCount = 0;
static AgFirmwareMode fwMode = FW_MODE_I_42PS;
static String fwNewVersion;
static void boardInit(void);
static void failedHandler(String msg);
static void configurationUpdateSchedule(void);
static void appDispHandler(void);
static void oledDisplaySchedule(void);
static void updateTvoc(void);
static void updatePm(void);
static void sendDataToServer(void);
static void tempHumUpdate(void);
static void co2Update(void);
static void mdnsInit(void);
static void initMqtt(void);
static void factoryConfigReset(void);
static void wdgFeedUpdate(void);
static bool sgp41Init(void);
static void wifiFactoryConfigure(void);
static void mqttHandle(void);
AgSchedule dispLedSchedule(DISP_UPDATE_INTERVAL, oledDisplaySchedule);
AgSchedule configSchedule(SERVER_CONFIG_SYNC_INTERVAL,
configurationUpdateSchedule);
AgSchedule agApiPostSchedule(SERVER_SYNC_INTERVAL, sendDataToServer);
AgSchedule co2Schedule(SENSOR_CO2_UPDATE_INTERVAL, co2Update);
AgSchedule pmsSchedule(SENSOR_PM_UPDATE_INTERVAL, updatePm);
AgSchedule tempHumSchedule(SENSOR_TEMP_HUM_UPDATE_INTERVAL, tempHumUpdate);
AgSchedule tvocSchedule(SENSOR_TVOC_UPDATE_INTERVAL, updateTvoc);
AgSchedule watchdogFeedSchedule(60000, wdgFeedUpdate);
AgSchedule mqttSchedule(MQTT_SYNC_INTERVAL, mqttHandle);
void setup() {
/** Serial for print debug message */
Serial.begin(115200);
delay(100); /** For bester show log */
/** Print device ID into log */
Serial.println("Serial nr: " + ag.deviceId());
/** Initialize local configure */
configuration.begin();
/** Init I2C */
Wire.begin(ag.getI2cSdaPin(), ag.getI2cSclPin());
delay(1000);
configuration.setAirGradient(&ag);
oledDisplay.setAirGradient(&ag);
stateMachine.setAirGradient(&ag);
wifiConnector.setAirGradient(&ag);
apiClient.setAirGradient(&ag);
openMetrics.setAirGradient(&ag);
localServer.setAirGraident(&ag);
/** Example set custom API root URL */
// apiClient.setApiRoot("https://example.custom.api");
/** Init sensor */
boardInit();
/** Connecting wifi */
bool connectToWifi = false;
/** Show message confirm offline mode, should me perform if LED bar button
* test pressed */
oledDisplay.setText(
"Press now for",
configuration.isOfflineMode() ? "online mode" : "offline mode", "");
uint32_t startTime = millis();
while (true) {
if (ag.button.getState() == ag.button.BUTTON_PRESSED) {
configuration.setOfflineMode(!configuration.isOfflineMode());
oledDisplay.setText(
"Offline Mode",
configuration.isOfflineMode() ? " = True" : " = False", "");
delay(1000);
break;
}
uint32_t periodMs = (uint32_t)(millis() - startTime);
if (periodMs >= 3000) {
Serial.println("Set for offline mode timeout");
break;
}
delay(1);
}
connectToWifi = !configuration.isOfflineMode();
if (connectToWifi) {
apiClient.begin();
if (wifiConnector.connect()) {
if (wifiConnector.isConnected()) {
mdnsInit();
localServer.begin();
initMqtt();
sendDataToAg();
apiClient.fetchServerConfiguration();
configSchedule.update();
if (apiClient.isFetchConfigureFailed()) {
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
stateMachine.displayHandle(
AgStateMachineWiFiOkServerOkSensorConfigFailed);
} else {
stateMachine.displayClearAddToDashBoard();
}
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
}
} else {
if (wifiConnector.isConfigurePorttalTimeout()) {
oledDisplay.showRebooting();
delay(2500);
oledDisplay.setText("", "", "");
ESP.restart();
}
}
}
}
/** Set offline mode without saving, cause wifi is not configured */
if (wifiConnector.hasConfigurated() == false) {
Serial.println("Set offline mode cause wifi is not configurated");
configuration.setOfflineModeWithoutSave(true);
}
/** Show display Warning up */
oledDisplay.setText("Warming Up", "Serial Number:", ag.deviceId().c_str());
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
Serial.println("Display brightness: " +
String(configuration.getDisplayBrightness()));
oledDisplay.setBrightness(configuration.getDisplayBrightness());
appDispHandler();
}
void loop() {
/** Handle schedule */
dispLedSchedule.run();
configSchedule.run();
agApiPostSchedule.run();
if (configuration.hasSensorS8) {
co2Schedule.run();
}
if (configuration.hasSensorPMS1) {
pmsSchedule.run();
ag.pms5003.handle();
}
if (configuration.hasSensorSHT) {
tempHumSchedule.run();
}
if (configuration.hasSensorSGP) {
tvocSchedule.run();
}
/** Auto reset watchdog timer if offline mode or postDataToAirGradient */
if (configuration.isOfflineMode() ||
(configuration.isPostDataToAirGradient() == false)) {
watchdogFeedSchedule.run();
}
/** Check for handle WiFi reconnect */
wifiConnector.handle();
/** factory reset handle */
factoryConfigReset();
/** check that local configura changed then do some action */
configUpdateHandle();
localServer._handle();
if (configuration.hasSensorSGP) {
ag.sgp41.handle();
}
MDNS.update();
mqttSchedule.run();
mqttClient.handle();
}
static void co2Update(void) {
int value = ag.s8.getCo2();
if (utils::isValidCO2(value)) {
measurements.CO2 = value;
getCO2FailCount = 0;
Serial.printf("CO2 (ppm): %d\r\n", measurements.CO2);
} else {
getCO2FailCount++;
Serial.printf("Get CO2 failed: %d\r\n", getCO2FailCount);
if (getCO2FailCount >= 3) {
measurements.CO2 = utils::getInvalidCO2();
}
}
}
static void mdnsInit(void) {
Serial.println("mDNS init");
if (!MDNS.begin(localServer.getHostname().c_str())) {
Serial.println("Init mDNS failed");
return;
}
MDNS.addService("_airgradient", "_tcp", 80);
MDNS.addServiceTxt("_airgradient", "_tcp", "model",
AgFirmwareModeName(fwMode));
MDNS.addServiceTxt("_airgradient", "_tcp", "serialno", ag.deviceId());
MDNS.addServiceTxt("_airgradient", "_tcp", "fw_ver", ag.getVersion());
MDNS.addServiceTxt("_airgradient", "_tcp", "vendor", "AirGradient");
MDNS.announce();
}
static void initMqtt(void) {
if (mqttClient.begin(configuration.getMqttBrokerUri())) {
Serial.println("Setup connect to MQTT broker successful");
} else {
Serial.println("setup Connect to MQTT broker failed");
}
}
static void factoryConfigReset(void) {
if (ag.button.getState() == ag.button.BUTTON_PRESSED) {
if (factoryBtnPressTime == 0) {
factoryBtnPressTime = millis();
} else {
uint32_t ms = (uint32_t)(millis() - factoryBtnPressTime);
if (ms >= 2000) {
// Show display message: For factory keep for x seconds
if (ag.isOne() || ag.isPro4_2()) {
oledDisplay.setText("Factory reset", "keep pressed", "for 8 sec");
} else {
Serial.println("Factory reset, keep pressed for 8 sec");
}
int count = 7;
while (ag.button.getState() == ag.button.BUTTON_PRESSED) {
delay(1000);
String str = "for " + String(count) + " sec";
oledDisplay.setText("Factory reset", "keep pressed", str.c_str());
count--;
if (count == 0) {
/** Stop MQTT task first */
// if (mqttTask) {
// vTaskDelete(mqttTask);
// mqttTask = NULL;
// }
/** Reset WIFI */
// WiFi.enableSTA(true); // Incase offline mode
// WiFi.disconnect(true, true);
wifiConnector.reset();
/** Reset local config */
configuration.reset();
oledDisplay.setText("Factory reset", "successful", "");
delay(3000);
oledDisplay.setText("", "", "");
ESP.restart();
}
}
/** Show current content cause reset ignore */
factoryBtnPressTime = 0;
appDispHandler();
}
}
} else {
if (factoryBtnPressTime != 0) {
appDispHandler();
}
factoryBtnPressTime = 0;
}
}
static void wdgFeedUpdate(void) {
ag.watchdog.reset();
Serial.println();
Serial.println("Offline mode or isPostToAirGradient = false: watchdog reset");
Serial.println();
}
static bool sgp41Init(void) {
ag.sgp41.setNoxLearningOffset(configuration.getNoxLearningOffset());
ag.sgp41.setTvocLearningOffset(configuration.getTvocLearningOffset());
if (ag.sgp41.begin(Wire)) {
Serial.println("Init SGP41 success");
configuration.hasSensorSGP = true;
return true;
} else {
Serial.println("Init SGP41 failuire");
configuration.hasSensorSGP = false;
}
return false;
}
static void wifiFactoryConfigure(void) {
WiFi.persistent(true);
WiFi.begin("airgradient", "cleanair");
WiFi.persistent(false);
oledDisplay.setText("Configure WiFi", "connect to", "\'airgradient\'");
delay(2500);
oledDisplay.setText("Rebooting...", "", "");
delay(2500);
oledDisplay.setText("", "", "");
ESP.restart();
}
static void mqttHandle(void) {
if(mqttClient.isConnected() == false) {
mqttClient.connect(String("airgradient-") + ag.deviceId());
}
if (mqttClient.isConnected()) {
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI(),
&ag, &configuration);
String topic = "airgradient/readings/" + ag.deviceId();
if (mqttClient.publish(topic.c_str(), payload.c_str(), payload.length())) {
Serial.println("MQTT sync success");
} else {
Serial.println("MQTT sync failure");
}
}
}
static void sendDataToAg() {
/** Change oledDisplay and led state */
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting);
delay(1500);
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount)) {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected);
} else {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed);
}
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
}
void dispSensorNotFound(String ss) {
ss = ss + " not found";
oledDisplay.setText("Sensor init", "Error:", ss.c_str());
delay(2000);
}
static void boardInit(void) {
/** Display init */
oledDisplay.begin();
/** Show boot display */
Serial.println("Firmware Version: " + ag.getVersion());
oledDisplay.setText("AirGradient ONE",
"FW Version: ", ag.getVersion().c_str());
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
ag.button.begin();
ag.watchdog.begin();
/** Run LED test on start up if button pressed */
oledDisplay.setText("Press now for", "factory WiFi", "configure");
uint32_t stime = millis();
while (true) {
if (ag.button.getState() == ag.button.BUTTON_PRESSED) {
wifiFactoryConfigure();
}
delay(1);
uint32_t ms = (uint32_t)(millis() - stime);
if (ms >= 3000) {
break;
}
delay(1);
}
/** Show message init sensor */
oledDisplay.setText("Sensor", "initializing...", "");
/** Init sensor SGP41 */
if (sgp41Init() == false) {
dispSensorNotFound("SGP41");
}
/** Init SHT */
if (ag.sht.begin(Wire) == false) {
Serial.println("SHTx sensor not found");
configuration.hasSensorSHT = false;
dispSensorNotFound("SHT");
}
/** Init S8 CO2 sensor */
if (ag.s8.begin(&Serial) == false) {
Serial.println("CO2 S8 sensor not found");
configuration.hasSensorS8 = false;
dispSensorNotFound("S8");
}
/** Init PMS5003 */
configuration.hasSensorPMS1 = true;
configuration.hasSensorPMS2 = false;
if (ag.pms5003.begin(&Serial) == false) {
Serial.println("PMS sensor not found");
configuration.hasSensorPMS1 = false;
dispSensorNotFound("PMS");
}
/** Set S8 CO2 abc days period */
if (configuration.hasSensorS8) {
if (ag.s8.setAbcPeriod(configuration.getCO2CalibrationAbcDays() * 24)) {
Serial.println("Set S8 AbcDays successful");
} else {
Serial.println("Set S8 AbcDays failure");
}
}
localServer.setFwMode(FW_MODE_I_42PS);
}
static void failedHandler(String msg) {
while (true) {
Serial.println(msg);
delay(1000);
}
}
static void configurationUpdateSchedule(void) {
if (apiClient.fetchServerConfiguration()) {
configUpdateHandle();
}
}
static void configUpdateHandle() {
if (configuration.isUpdated() == false) {
return;
}
stateMachine.executeCo2Calibration();
String mqttUri = configuration.getMqttBrokerUri();
if (mqttClient.isCurrentUri(mqttUri) == false) {
mqttClient.end();
initMqtt();
}
if (configuration.hasSensorSGP) {
if (configuration.noxLearnOffsetChanged() ||
configuration.tvocLearnOffsetChanged()) {
ag.sgp41.end();
int oldTvocOffset = ag.sgp41.getTvocLearningOffset();
int oldNoxOffset = ag.sgp41.getNoxLearningOffset();
bool result = sgp41Init();
const char *resultStr = "successful";
if (!result) {
resultStr = "failure";
}
if (oldTvocOffset != configuration.getTvocLearningOffset()) {
Serial.printf("Setting tvocLearningOffset from %d to %d hours %s\r\n",
oldTvocOffset, configuration.getTvocLearningOffset(),
resultStr);
}
if (oldNoxOffset != configuration.getNoxLearningOffset()) {
Serial.printf("Setting noxLearningOffset from %d to %d hours %s\r\n",
oldNoxOffset, configuration.getNoxLearningOffset(),
resultStr);
}
}
}
if (configuration.isDisplayBrightnessChanged()) {
oledDisplay.setBrightness(configuration.getDisplayBrightness());
}
appDispHandler();
}
static void appDispHandler(void) {
AgStateMachineState state = AgStateMachineNormal;
/** Only show display status on online mode. */
if (configuration.isOfflineMode() == false) {
if (wifiConnector.isConnected() == false) {
state = AgStateMachineWiFiLost;
} else if (apiClient.isFetchConfigureFailed()) {
state = AgStateMachineSensorConfigFailed;
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
} else {
stateMachine.displayClearAddToDashBoard();
}
} else if (apiClient.isPostToServerFailed()) {
state = AgStateMachineServerLost;
}
}
stateMachine.displayHandle(state);
}
static void oledDisplaySchedule(void) {
if (factoryBtnPressTime == 0) {
appDispHandler();
}
}
static void updateTvoc(void) {
measurements.TVOC = ag.sgp41.getTvocIndex();
measurements.TVOCRaw = ag.sgp41.getTvocRaw();
measurements.NOx = ag.sgp41.getNoxIndex();
measurements.NOxRaw = ag.sgp41.getNoxRaw();
Serial.println();
Serial.printf("TVOC index: %d\r\n", measurements.TVOC);
Serial.printf("TVOC raw: %d\r\n", measurements.TVOCRaw);
Serial.printf("NOx index: %d\r\n", measurements.NOx);
Serial.printf("NOx raw: %d\r\n", measurements.NOxRaw);
}
static void updatePm(void) {
if (ag.pms5003.isFailed() == false) {
measurements.pm01_1 = ag.pms5003.getPm01Ae();
measurements.pm25_1 = ag.pms5003.getPm25Ae();
measurements.pm10_1 = ag.pms5003.getPm10Ae();
measurements.pm03PCount_1 = ag.pms5003.getPm03ParticleCount();
Serial.println();
Serial.printf("PM1 ug/m3: %d\r\n", measurements.pm01_1);
Serial.printf("PM2.5 ug/m3: %d\r\n", measurements.pm25_1);
Serial.printf("PM10 ug/m3: %d\r\n", measurements.pm10_1);
Serial.printf("PM0.3 Count: %d\r\n", measurements.pm03PCount_1);
pmFailCount = 0;
} else {
pmFailCount++;
Serial.printf("PMS read failed: %d\r\n", pmFailCount);
if (pmFailCount >= 3) {
measurements.pm01_1 = utils::getInvalidPMS();
measurements.pm25_1 = utils::getInvalidPMS();
measurements.pm10_1 = utils::getInvalidPMS();
measurements.pm03PCount_1 = utils::getInvalidPMS();
}
}
}
static void sendDataToServer(void) {
/** Ignore send data to server if postToAirGradient disabled */
if (configuration.isPostDataToAirGradient() == false ||
configuration.isOfflineMode()) {
return;
}
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI(),
&ag, &configuration);
if (apiClient.postToServer(syncData)) {
ag.watchdog.reset();
Serial.println();
Serial.println(
"Online mode and isPostToAirGradient = true: watchdog reset");
Serial.println();
}
measurements.bootCount++;
}
static void tempHumUpdate(void) {
delay(100);
if (ag.sht.measure()) {
measurements.Temperature = ag.sht.getTemperature();
measurements.Humidity = ag.sht.getRelativeHumidity();
Serial.printf("Temperature in C: %0.2f\r\n", measurements.Temperature);
Serial.printf("Relative Humidity: %d\r\n", measurements.Humidity);
Serial.printf("Temperature compensated in C: %0.2f\r\n",
measurements.Temperature);
Serial.printf("Relative Humidity compensated: %d\r\n",
measurements.Humidity);
// Update compensation temperature and humidity for SGP41
if (configuration.hasSensorSGP) {
ag.sgp41.setCompensationTemperatureHumidity(measurements.Temperature,
measurements.Humidity);
}
} else {
Serial.println("SHT read failed");
measurements.Temperature = utils::getInvalidTemperature();
measurements.Humidity = utils::getInvalidHumidity();
}
}

View File

@ -0,0 +1,61 @@
#include "LocalServer.h"
LocalServer::LocalServer(Stream &log, OpenMetrics &openMetrics,
Measurements &measure, Configuration &config,
WifiConnector &wifiConnector)
: PrintLog(log, "LocalServer"), openMetrics(openMetrics), measure(measure),
config(config), wifiConnector(wifiConnector), server(80) {}
LocalServer::~LocalServer() {}
bool LocalServer::begin(void) {
server.on("/measures/current", HTTP_GET, [this]() { _GET_measure(); });
server.on(openMetrics.getApi(), HTTP_GET, [this]() { _GET_metrics(); });
server.on("/config", HTTP_GET, [this]() { _GET_config(); });
server.on("/config", HTTP_PUT, [this]() { _PUT_config(); });
server.begin();
logInfo("Init: " + getHostname() + ".local");
return true;
}
void LocalServer::setAirGraident(AirGradient *ag) { this->ag = ag; }
String LocalServer::getHostname(void) {
return "airgradient_" + ag->deviceId();
}
void LocalServer::_handle(void) { server.handleClient(); }
void LocalServer::_GET_config(void) {
if(ag->isOne()) {
server.send(200, "application/json", config.toString());
} else {
server.send(200, "application/json", config.toString(fwMode));
}
}
void LocalServer::_PUT_config(void) {
String data = server.arg(0);
String response = "";
int statusCode = 400; // Status code for data invalid
if (config.parse(data, true)) {
statusCode = 200;
response = "Success";
} else {
response = config.getFailedMesage();
}
server.send(statusCode, "text/plain", response);
}
void LocalServer::_GET_metrics(void) {
server.send(200, openMetrics.getApiContentType(), openMetrics.getPayload());
}
void LocalServer::_GET_measure(void) {
server.send(
200, "application/json",
measure.toString(true, fwMode, wifiConnector.RSSI(), ag, &config));
}
void LocalServer::setFwMode(AgFirmwareMode fwMode) { this->fwMode = fwMode; }

View File

@ -0,0 +1,38 @@
#ifndef _LOCAL_SERVER_H_
#define _LOCAL_SERVER_H_
#include "AgConfigure.h"
#include "AgValue.h"
#include "AirGradient.h"
#include "OpenMetrics.h"
#include "AgWiFiConnector.h"
#include <Arduino.h>
#include <ESP8266WebServer.h>
class LocalServer : public PrintLog {
private:
AirGradient *ag;
OpenMetrics &openMetrics;
Measurements &measure;
Configuration &config;
WifiConnector &wifiConnector;
ESP8266WebServer server;
AgFirmwareMode fwMode;
public:
LocalServer(Stream &log, OpenMetrics &openMetrics, Measurements &measure,
Configuration &config, WifiConnector& wifiConnector);
~LocalServer();
bool begin(void);
void setAirGraident(AirGradient *ag);
String getHostname(void);
void setFwMode(AgFirmwareMode fwMode);
void _handle(void);
void _GET_config(void);
void _PUT_config(void);
void _GET_metrics(void);
void _GET_measure(void);
};
#endif /** _LOCAL_SERVER_H_ */

View File

@ -0,0 +1,186 @@
#include "OpenMetrics.h"
OpenMetrics::OpenMetrics(Measurements &measure, Configuration &config,
WifiConnector &wifiConnector, AgApiClient &apiClient)
: measure(measure), config(config), wifiConnector(wifiConnector),
apiClient(apiClient) {}
OpenMetrics::~OpenMetrics() {}
void OpenMetrics::setAirGradient(AirGradient *ag) { this->ag = ag; }
const char *OpenMetrics::getApiContentType(void) {
return "application/openmetrics-text; version=1.0.0; charset=utf-8";
}
const char *OpenMetrics::getApi(void) { return "/metrics"; }
String OpenMetrics::getPayload(void) {
String response;
String current_metric_name;
const auto add_metric = [&](const String &name, const String &help,
const String &type, const String &unit = "") {
current_metric_name = "airgradient_" + name;
if (!unit.isEmpty())
current_metric_name += "_" + unit;
response += "# HELP " + current_metric_name + " " + help + "\n";
response += "# TYPE " + current_metric_name + " " + type + "\n";
if (!unit.isEmpty())
response += "# UNIT " + current_metric_name + " " + unit + "\n";
};
const auto add_metric_point = [&](const String &labels, const String &value) {
response += current_metric_name + "{" + labels + "} " + value + "\n";
};
add_metric("info", "AirGradient device information", "info");
add_metric_point("airgradient_serial_number=\"" + ag->deviceId() +
"\",airgradient_device_type=\"" + ag->getBoardName() +
"\",airgradient_library_version=\"" + ag->getVersion() +
"\"",
"1");
add_metric("config_ok",
"1 if the AirGradient device was able to successfully fetch its "
"configuration from the server",
"gauge");
add_metric_point("", apiClient.isFetchConfigureFailed() ? "0" : "1");
add_metric(
"post_ok",
"1 if the AirGradient device was able to successfully send to the server",
"gauge");
add_metric_point("", apiClient.isPostToServerFailed() ? "0" : "1");
add_metric(
"wifi_rssi",
"WiFi signal strength from the AirGradient device perspective, in dBm",
"gauge", "dbm");
add_metric_point("", String(wifiConnector.RSSI()));
if (config.hasSensorS8 && measure.CO2 >= 0) {
add_metric("co2",
"Carbon dioxide concentration as measured by the AirGradient S8 "
"sensor, in parts per million",
"gauge", "ppm");
add_metric_point("", String(measure.CO2));
}
float _temp = utils::getInvalidTemperature();
float _hum = utils::getInvalidHumidity();
int pm01 = utils::getInvalidPMS();
int pm25 = utils::getInvalidPMS();
int pm10 = utils::getInvalidPMS();
int pm03PCount = utils::getInvalidPMS();
int atmpCompensated = utils::getInvalidTemperature();
int ahumCompensated = utils::getInvalidHumidity();
if (config.hasSensorSHT) {
_temp = measure.Temperature;
_hum = measure.Humidity;
atmpCompensated = _temp;
ahumCompensated = _hum;
}
if (config.hasSensorPMS1) {
pm01 = measure.pm01_1;
pm25 = measure.pm25_1;
pm10 = measure.pm10_1;
pm03PCount = measure.pm03PCount_1;
}
if (config.hasSensorPMS1) {
if (utils::isValidPMS(pm01)) {
add_metric("pm1",
"PM1.0 concentration as measured by the AirGradient PMS "
"sensor, in micrograms per cubic meter",
"gauge", "ugm3");
add_metric_point("", String(pm01));
}
if (utils::isValidPMS(pm25)) {
add_metric("pm2d5",
"PM2.5 concentration as measured by the AirGradient PMS "
"sensor, in micrograms per cubic meter",
"gauge", "ugm3");
add_metric_point("", String(pm25));
}
if (utils::isValidPMS(pm10)) {
add_metric("pm10",
"PM10 concentration as measured by the AirGradient PMS "
"sensor, in micrograms per cubic meter",
"gauge", "ugm3");
add_metric_point("", String(pm10));
}
if (utils::isValidPMS03Count(pm03PCount)) {
add_metric("pm0d3",
"PM0.3 concentration as measured by the AirGradient PMS "
"sensor, in number of particules per 100 milliliters",
"gauge", "p100ml");
add_metric_point("", String(pm03PCount));
}
}
if (config.hasSensorSGP) {
if (utils::isValidVOC(measure.TVOC)) {
add_metric("tvoc_index",
"The processed Total Volatile Organic Compounds (TVOC) index "
"as measured by the AirGradient SGP sensor",
"gauge");
add_metric_point("", String(measure.TVOC));
}
if (utils::isValidVOC(measure.TVOCRaw)) {
add_metric("tvoc_raw",
"The raw input value to the Total Volatile Organic Compounds "
"(TVOC) index as measured by the AirGradient SGP sensor",
"gauge");
add_metric_point("", String(measure.TVOCRaw));
}
if (utils::isValidNOx(measure.NOx)) {
add_metric("nox_index",
"The processed Nitrous Oxide (NOx) index as measured by the "
"AirGradient SGP sensor",
"gauge");
add_metric_point("", String(measure.NOx));
}
if (utils::isValidNOx(measure.NOxRaw)) {
add_metric("nox_raw",
"The raw input value to the Nitrous Oxide (NOx) index as "
"measured by the AirGradient SGP sensor",
"gauge");
add_metric_point("", String(measure.NOxRaw));
}
}
if (utils::isValidTemperature(_temp)) {
add_metric(
"temperature",
"The ambient temperature as measured by the AirGradient SHT / PMS "
"sensor, in degrees Celsius",
"gauge", "celsius");
add_metric_point("", String(_temp));
}
if (utils::isValidTemperature(atmpCompensated)) {
add_metric("temperature_compensated",
"The compensated ambient temperature as measured by the "
"AirGradient SHT / PMS "
"sensor, in degrees Celsius",
"gauge", "celsius");
add_metric_point("", String(atmpCompensated));
}
if (utils::isValidHumidity(_hum)) {
add_metric(
"humidity",
"The relative humidity as measured by the AirGradient SHT sensor",
"gauge", "percent");
add_metric_point("", String(_hum));
}
if (utils::isValidHumidity(ahumCompensated)) {
add_metric("humidity_compensated",
"The compensated relative humidity as measured by the "
"AirGradient SHT / PMS sensor",
"gauge", "percent");
add_metric_point("", String(ahumCompensated));
}
response += "# EOF\n";
return response;
}

View File

@ -0,0 +1,28 @@
#ifndef _OPEN_METRICS_H_
#define _OPEN_METRICS_H_
#include "AgConfigure.h"
#include "AgValue.h"
#include "AgWiFiConnector.h"
#include "AirGradient.h"
#include "AgApiClient.h"
class OpenMetrics {
private:
AirGradient *ag;
Measurements &measure;
Configuration &config;
WifiConnector &wifiConnector;
AgApiClient &apiClient;
public:
OpenMetrics(Measurements &measure, Configuration &conig,
WifiConnector &wifiConnector, AgApiClient& apiClient);
~OpenMetrics();
void setAirGradient(AirGradient *ag);
const char *getApiContentType(void);
const char* getApi(void);
String getPayload(void);
};
#endif /** _OPEN_METRICS_H_ */

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,72 @@
#include "LocalServer.h"
LocalServer::LocalServer(Stream &log, OpenMetrics &openMetrics,
Measurements &measure, Configuration &config,
WifiConnector &wifiConnector)
: PrintLog(log, "LocalServer"), openMetrics(openMetrics), measure(measure),
config(config), wifiConnector(wifiConnector) {}
LocalServer::~LocalServer() {}
bool LocalServer::begin(void) {
server.on("/measures/current", HTTP_GET, [this]() { _GET_measure(); });
server.on(openMetrics.getApi(), HTTP_GET, [this]() { _GET_metrics(); });
server.on("/config", HTTP_GET, [this]() { _GET_config(); });
server.on("/config", HTTP_PUT, [this]() { _PUT_config(); });
server.begin();
if (xTaskCreate(
[](void *param) {
LocalServer *localServer = (LocalServer *)param;
for (;;) {
localServer->_handle();
}
},
"webserver", 1024 * 4, this, 5, NULL) != pdTRUE) {
Serial.println("Create task handle webserver failed");
}
logInfo("Init: " + getHostname() + ".local");
return true;
}
void LocalServer::setAirGraident(AirGradient *ag) { this->ag = ag; }
String LocalServer::getHostname(void) {
return "airgradient_" + ag->deviceId();
}
void LocalServer::_handle(void) { server.handleClient(); }
void LocalServer::_GET_config(void) {
if(ag->isOne()) {
server.send(200, "application/json", config.toString());
} else {
server.send(200, "application/json", config.toString(fwMode));
}
}
void LocalServer::_PUT_config(void) {
String data = server.arg(0);
String response = "";
int statusCode = 400; // Status code for data invalid
if (config.parse(data, true)) {
statusCode = 200;
response = "Success";
} else {
response = config.getFailedMesage();
}
server.send(statusCode, "text/plain", response);
}
void LocalServer::_GET_metrics(void) {
server.send(200, openMetrics.getApiContentType(), openMetrics.getPayload());
}
void LocalServer::_GET_measure(void) {
server.send(
200, "application/json",
measure.toString(true, fwMode, wifiConnector.RSSI(), ag, &config));
}
void LocalServer::setFwMode(AgFirmwareMode fwMode) { this->fwMode = fwMode; }

View File

@ -0,0 +1,38 @@
#ifndef _LOCAL_SERVER_H_
#define _LOCAL_SERVER_H_
#include "AgConfigure.h"
#include "AgValue.h"
#include "AirGradient.h"
#include "OpenMetrics.h"
#include "AgWiFiConnector.h"
#include <Arduino.h>
#include <WebServer.h>
class LocalServer : public PrintLog {
private:
AirGradient *ag;
OpenMetrics &openMetrics;
Measurements &measure;
Configuration &config;
WifiConnector &wifiConnector;
WebServer server;
AgFirmwareMode fwMode;
public:
LocalServer(Stream &log, OpenMetrics &openMetrics, Measurements &measure,
Configuration &config, WifiConnector& wifiConnector);
~LocalServer();
bool begin(void);
void setAirGraident(AirGradient *ag);
String getHostname(void);
void setFwMode(AgFirmwareMode fwMode);
void _handle(void);
void _GET_config(void);
void _PUT_config(void);
void _GET_metrics(void);
void _GET_measure(void);
};
#endif /** _LOCAL_SERVER_H_ */

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,219 @@
#include "OpenMetrics.h"
OpenMetrics::OpenMetrics(Measurements &measure, Configuration &config,
WifiConnector &wifiConnector, AgApiClient &apiClient)
: measure(measure), config(config), wifiConnector(wifiConnector),
apiClient(apiClient) {}
OpenMetrics::~OpenMetrics() {}
void OpenMetrics::setAirGradient(AirGradient *ag) { this->ag = ag; }
const char *OpenMetrics::getApiContentType(void) {
return "application/openmetrics-text; version=1.0.0; charset=utf-8";
}
const char *OpenMetrics::getApi(void) { return "/metrics"; }
String OpenMetrics::getPayload(void) {
String response;
String current_metric_name;
const auto add_metric = [&](const String &name, const String &help,
const String &type, const String &unit = "") {
current_metric_name = "airgradient_" + name;
if (!unit.isEmpty())
current_metric_name += "_" + unit;
response += "# HELP " + current_metric_name + " " + help + "\n";
response += "# TYPE " + current_metric_name + " " + type + "\n";
if (!unit.isEmpty())
response += "# UNIT " + current_metric_name + " " + unit + "\n";
};
const auto add_metric_point = [&](const String &labels, const String &value) {
response += current_metric_name + "{" + labels + "} " + value + "\n";
};
add_metric("info", "AirGradient device information", "info");
add_metric_point("airgradient_serial_number=\"" + ag->deviceId() +
"\",airgradient_device_type=\"" + ag->getBoardName() +
"\",airgradient_library_version=\"" + ag->getVersion() +
"\"",
"1");
add_metric("config_ok",
"1 if the AirGradient device was able to successfully fetch its "
"configuration from the server",
"gauge");
add_metric_point("", apiClient.isFetchConfigureFailed() ? "0" : "1");
add_metric(
"post_ok",
"1 if the AirGradient device was able to successfully send to the server",
"gauge");
add_metric_point("", apiClient.isPostToServerFailed() ? "0" : "1");
add_metric(
"wifi_rssi",
"WiFi signal strength from the AirGradient device perspective, in dBm",
"gauge", "dbm");
add_metric_point("", String(wifiConnector.RSSI()));
if (config.hasSensorS8 && measure.CO2 >= 0) {
add_metric("co2",
"Carbon dioxide concentration as measured by the AirGradient S8 "
"sensor, in parts per million",
"gauge", "ppm");
add_metric_point("", String(measure.CO2));
}
float _temp = utils::getInvalidTemperature();
float _hum = utils::getInvalidHumidity();
int pm01 = utils::getInvalidPMS();
int pm25 = utils::getInvalidPMS();
int pm10 = utils::getInvalidPMS();
int pm03PCount = utils::getInvalidPMS();
int atmpCompensated = utils::getInvalidTemperature();
int ahumCompensated = utils::getInvalidHumidity();
if (config.hasSensorPMS1 && config.hasSensorPMS2) {
_temp = (measure.temp_1 + measure.temp_2) / 2.0f;
_hum = (measure.hum_1 + measure.hum_2) / 2.0f;
pm01 = (measure.pm01_1 + measure.pm01_2) / 2;
pm25 = (measure.pm25_1 + measure.pm25_2) / 2;
pm10 = (measure.pm10_1 + measure.pm10_2) / 2;
pm03PCount = (measure.pm03PCount_1 + measure.pm03PCount_2) / 2;
} else {
if (ag->isOne()) {
if (config.hasSensorSHT) {
_temp = measure.Temperature;
_hum = measure.Humidity;
}
if (config.hasSensorPMS1) {
pm01 = measure.pm01_1;
pm25 = measure.pm25_1;
pm10 = measure.pm10_1;
pm03PCount = measure.pm03PCount_1;
}
} else {
if (config.hasSensorPMS1) {
_temp = measure.temp_1;
_hum = measure.hum_1;
pm01 = measure.pm01_1;
pm25 = measure.pm25_1;
pm10 = measure.pm10_1;
pm03PCount = measure.pm03PCount_1;
}
if (config.hasSensorPMS2) {
_temp = measure.temp_2;
_hum = measure.hum_2;
pm01 = measure.pm01_2;
pm25 = measure.pm25_2;
pm10 = measure.pm10_2;
pm03PCount = measure.pm03PCount_2;
}
}
}
/** Get temperature and humidity compensated */
if (ag->isOne()) {
atmpCompensated = _temp;
ahumCompensated = _hum;
} else {
atmpCompensated = ag->pms5003t_1.temperatureCompensated(_temp);
ahumCompensated = ag->pms5003t_1.humidityCompensated(_hum);
}
if (config.hasSensorPMS1 || config.hasSensorPMS2) {
if (utils::isValidPMS(pm01)) {
add_metric("pm1",
"PM1.0 concentration as measured by the AirGradient PMS "
"sensor, in micrograms per cubic meter",
"gauge", "ugm3");
add_metric_point("", String(pm01));
}
if (utils::isValidPMS(pm25)) {
add_metric("pm2d5",
"PM2.5 concentration as measured by the AirGradient PMS "
"sensor, in micrograms per cubic meter",
"gauge", "ugm3");
add_metric_point("", String(pm25));
}
if (utils::isValidPMS(pm10)) {
add_metric("pm10",
"PM10 concentration as measured by the AirGradient PMS "
"sensor, in micrograms per cubic meter",
"gauge", "ugm3");
add_metric_point("", String(pm10));
}
if (utils::isValidPMS03Count(pm03PCount)) {
add_metric("pm0d3",
"PM0.3 concentration as measured by the AirGradient PMS "
"sensor, in number of particules per 100 milliliters",
"gauge", "p100ml");
add_metric_point("", String(pm03PCount));
}
}
if (config.hasSensorSGP) {
if (utils::isValidVOC(measure.TVOC)) {
add_metric("tvoc_index",
"The processed Total Volatile Organic Compounds (TVOC) index "
"as measured by the AirGradient SGP sensor",
"gauge");
add_metric_point("", String(measure.TVOC));
}
if (utils::isValidVOC(measure.TVOCRaw)) {
add_metric("tvoc_raw",
"The raw input value to the Total Volatile Organic Compounds "
"(TVOC) index as measured by the AirGradient SGP sensor",
"gauge");
add_metric_point("", String(measure.TVOCRaw));
}
if (utils::isValidNOx(measure.NOx)) {
add_metric("nox_index",
"The processed Nitrous Oxide (NOx) index as measured by the "
"AirGradient SGP sensor",
"gauge");
add_metric_point("", String(measure.NOx));
}
if (utils::isValidNOx(measure.NOxRaw)) {
add_metric("nox_raw",
"The raw input value to the Nitrous Oxide (NOx) index as "
"measured by the AirGradient SGP sensor",
"gauge");
add_metric_point("", String(measure.NOxRaw));
}
}
if (utils::isValidTemperature(_temp)) {
add_metric("temperature",
"The ambient temperature as measured by the AirGradient SHT / PMS "
"sensor, in degrees Celsius",
"gauge", "celsius");
add_metric_point("", String(_temp));
}
if (utils::isValidTemperature(atmpCompensated)) {
add_metric(
"temperature_compensated",
"The compensated ambient temperature as measured by the AirGradient SHT / PMS "
"sensor, in degrees Celsius",
"gauge", "celsius");
add_metric_point("", String(atmpCompensated));
}
if (utils::isValidHumidity(_hum)) {
add_metric(
"humidity",
"The relative humidity as measured by the AirGradient SHT sensor",
"gauge", "percent");
add_metric_point("", String(_hum));
}
if (utils::isValidHumidity(ahumCompensated)) {
add_metric(
"humidity_compensated",
"The compensated relative humidity as measured by the AirGradient SHT / PMS sensor",
"gauge", "percent");
add_metric_point("", String(ahumCompensated));
}
response += "# EOF\n";
return response;
}

View File

@ -0,0 +1,28 @@
#ifndef _OPEN_METRICS_H_
#define _OPEN_METRICS_H_
#include "AgConfigure.h"
#include "AgValue.h"
#include "AgWiFiConnector.h"
#include "AirGradient.h"
#include "AgApiClient.h"
class OpenMetrics {
private:
AirGradient *ag;
Measurements &measure;
Configuration &config;
WifiConnector &wifiConnector;
AgApiClient &apiClient;
public:
OpenMetrics(Measurements &measure, Configuration &conig,
WifiConnector &wifiConnector, AgApiClient& apiClient);
~OpenMetrics();
void setAirGradient(AirGradient *ag);
const char *getApiContentType(void);
const char* getApi(void);
String getPayload(void);
};
#endif /** _OPEN_METRICS_H_ */

View File

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

View File

@ -1,974 +0,0 @@
/*
This is the code for the AirGradient Open Air open-source hardware outdoor Air
Quality Monitor with an ESP32-C3 Microcontroller.
It is an air quality monitor for PM2.5, CO2, TVOCs, NOx, Temperature and
Humidity and can send data over Wifi.
Open source air quality monitors and kits are available:
Indoor Monitor: https://www.airgradient.com/indoor/
Outdoor Monitor: https://www.airgradient.com/outdoor/
Build Instructions:
https://www.airgradient.com/documentation/open-air-pst-kit-1-3/
The codes needs the following libraries installed:
“WifiManager by tzapu, tablatronix” tested with version 2.0.16-rc.2
"Arduino_JSON" by Arduino Version 0.2.0
Please make sure you have esp32 board manager installed. Tested with
version 2.0.11.
Important flashing settings:
- Set board to "ESP32C3 Dev Module"
- Enable "USB CDC On Boot"
- Flash frequency "80Mhz"
- Flash mode "QIO"
- Flash size "4MB"
- Partition scheme "Default 4MB with spiffs (1.2MB APP/1,5MB SPIFFS)"
- JTAG adapter "Disabled"
If you have any questions please visit our forum at
https://forum.airgradient.com/
CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
*/
#include <AirGradient.h>
#include <Arduino_JSON.h>
#include <HTTPClient.h>
#include <HardwareSerial.h>
#include <WiFiManager.h>
#include <Wire.h>
/**
*
* @brief Application state machine state
*
*/
enum {
APP_SM_WIFI_MANAGER_MODE, /** In WiFi Manger Mode */
APP_SM_WIFI_MAMAGER_PORTAL_ACTIVE, /** WiFi Manager has connected to mobile
phone */
APP_SM_WIFI_MANAGER_STA_CONNECTING, /** After SSID and PW entered and OK
clicked, connection to WiFI network is
attempted*/
APP_SM_WIFI_MANAGER_STA_CONNECTED, /** Connecting to WiFi worked */
APP_SM_WIFI_OK_SERVER_CONNECTING, /** Once connected to WiFi an attempt to
reach the server is performed */
APP_SM_WIFI_OK_SERVER_CONNNECTED, /** Server is reachable, all fine */
/** Exceptions during WIFi Setup */
APP_SM_WIFI_MANAGER_CONNECT_FAILED, /** Cannot connect to WiFi (e.g. wrong
password, WPA Enterprise etc.) */
APP_SM_WIFI_OK_SERVER_CONNECT_FAILED, /** Connected to WiFi but server not
reachable, e.g. firewall block/
whitelisting needed etc. */
APP_SM_WIFI_OK_SERVER_OK_SENSOR_CONFIG_FAILED, /** Server reachable but sensor
not configured correctly*/
/** During Normal Operation */
APP_SM_WIFI_LOST, /** Connection to WiFi network failed credentials incorrect
encryption not supported etc. */
APP_SM_SERVER_LOST, /** Connected to WiFi network but the server cannot be
reached through the internet, e.g. blocked by firewall
*/
APP_SM_SENSOR_CONFIG_FAILED, /** Server is reachable but there is some
configuration issue to be fixed on the server
side */
APP_SM_NORMAL,
};
#define LED_FAST_BLINK_DELAY 250 /** ms */
#define LED_SLOW_BLINK_DELAY 1000 /** ms */
#define WIFI_CONNECT_COUNTDOWN_MAX 180 /** sec */
#define WIFI_CONNECT_RETRY_MS 10000 /** ms */
#define LED_BAR_COUNT_INIT_VALUE (-1) /** */
#define LED_BAR_ANIMATION_PERIOD 100 /** ms */
#define DISP_UPDATE_INTERVAL 5000 /** ms */
#define SERVER_CONFIG_UPDATE_INTERVAL 30000 /** ms */
#define SERVER_SYNC_INTERVAL 60000 /** ms */
#define SENSOR_CO2_CALIB_COUNTDOWN_MAX 5 /** sec */
#define SENSOR_TVOC_UPDATE_INTERVAL 1000 /** ms */
#define SENSOR_CO2_UPDATE_INTERVAL 5000 /** ms */
#define SENSOR_PM_UPDATE_INTERVAL 5000 /** ms */
#define SENSOR_TEMP_HUM_UPDATE_INTERVAL 5000 /** ms */
#define DISPLAY_DELAY_SHOW_CONTENT_MS 2000 /** ms */
#define WIFI_HOTSPOT_PASSWORD_DEFAULT \
"cleanair" /** default WiFi AP password \
*/
/**
* @brief Use use LED bar state
*/
typedef enum {
UseLedBarOff, /** Don't use LED bar */
UseLedBarPM, /** Use LED bar for PMS */
UseLedBarCO2, /** Use LED bar for CO2 */
} UseLedBar;
/**
* @brief Schedule handle with timing period
*
*/
class AgSchedule {
public:
AgSchedule(int period, void (*handler)(void))
: period(period), handler(handler) {}
void run(void) {
uint32_t ms = (uint32_t)(millis() - count);
if (ms >= period) {
/** Call handler */
handler();
Serial.printf("[AgSchedule] handle 0x%08x, period: %d(ms)\r\n",
(unsigned int)handler, period);
/** Update period time */
count = millis();
}
}
void setPeriod(int period) { this->period = period; }
private:
void (*handler)(void);
int period;
int count;
};
/**
* @brief AirGradient server configuration and sync data
*
*/
class AgServer {
public:
void begin(void) {
inF = false;
inUSAQI = false;
configFailed = false;
serverFailed = false;
memset(models, 0, sizeof(models));
memset(mqttBroker, 0, sizeof(mqttBroker));
}
/**
* @brief Get server configuration
*
* @param id Device ID
* @return true Success
* @return false Failure
*/
bool pollServerConfig(String id) {
String uri =
"http://hw.airgradient.com/sensors/airgradient:" + id + "/one/config";
/** Init http client */
HTTPClient client;
if (client.begin(uri) == false) {
configFailed = true;
return false;
}
/** Get */
int retCode = client.GET();
if (retCode != 200) {
client.end();
configFailed = true;
return false;
}
/** clear failed */
configFailed = false;
/** Get response string */
String respContent = client.getString();
client.end();
Serial.println("Get server config: " + respContent);
/** Parse JSON */
JSONVar root = JSON.parse(respContent);
if (JSON.typeof(root) == "undefined") {
/** JSON invalid */
return false;
}
/** Get "country" */
if (JSON.typeof_(root["country"]) == "string") {
String country = root["country"];
if (country == "US") {
inF = true;
} else {
inF = false;
}
}
/** Get "pmStandard" */
if (JSON.typeof_(root["pmStandard"]) == "string") {
String standard = root["pmStandard"];
if (standard == "ugm3") {
inUSAQI = false;
} else {
inUSAQI = true;
}
}
/** Get "co2CalibrationRequested" */
if (JSON.typeof_(root["co2CalibrationRequested"]) == "boolean") {
co2Calib = root["co2CalibrationRequested"];
}
/** Get "ledBarMode" */
if (JSON.typeof_(root["ledBarMode"]) == "string") {
String mode = root["ledBarMode"];
if (mode == "co2") {
ledBarMode = UseLedBarCO2;
} else if (mode == "pm") {
ledBarMode = UseLedBarPM;
} else if (mode == "off") {
ledBarMode = UseLedBarOff;
} else {
ledBarMode = UseLedBarOff;
}
}
/** Get model */
if (JSON.typeof_(root["model"]) == "string") {
String model = root["model"];
if (model.length()) {
int len =
model.length() < sizeof(models) ? model.length() : sizeof(models);
memset(models, 0, sizeof(models));
memcpy(models, model.c_str(), len);
}
}
/** Get "mqttBrokerUrl" */
if (JSON.typeof_(root["mqttBrokerUrl"]) == "string") {
String mqtt = root["mqttBrokerUrl"];
if (mqtt.length()) {
int len = mqtt.length() < sizeof(mqttBroker) ? mqtt.length()
: sizeof(mqttBroker);
memset(mqttBroker, 0, sizeof(mqttBroker));
memcpy(mqttBroker, mqtt.c_str(), len);
}
}
/** Get 'abcDays' */
if (JSON.typeof_(root["abcDays"]) == "number") {
co2AbcCalib = root["abcDays"];
} else {
co2AbcCalib = -1;
}
/** Show configuration */
showServerConfig();
return true;
}
bool postToServer(String id, String payload) {
/**
* @brief Only post data if WiFi is connected
*/
if (WiFi.isConnected() == false) {
return false;
}
Serial.printf("Post payload: %s\r\n", payload.c_str());
String uri =
"http://hw.airgradient.com/sensors/airgradient:" + id + "/measures";
WiFiClient wifiClient;
HTTPClient client;
if (client.begin(wifiClient, uri.c_str()) == false) {
return false;
}
client.addHeader("content-type", "application/json");
int retCode = client.POST(payload);
client.end();
if ((retCode == 200) || (retCode == 429)) {
serverFailed = false;
return true;
}
serverFailed = true;
return false;
}
/**
* @brief Get temperature configuration unit
*
* @return true F unit
* @return false C Unit
*/
bool isTemperatureUnitF(void) { return inF; }
/**
* @brief Get PMS standard unit
*
* @return true USAQI
* @return false ugm3
*/
bool isPMSinUSAQI(void) { return inUSAQI; }
/**
* @brief Get status of get server coniguration is failed
*
* @return true Failed
* @return false Success
*/
bool isConfigFailed(void) { return configFailed; }
/**
* @brief Get status of post server configuration is failed
*
* @return true Failed
* @return false Success
*/
bool isServerFailed(void) { return serverFailed; }
/**
* @brief Get request calibration CO2
*
* @return true Requested. If result = true, it's clear after function call
* @return false Not-requested
*/
bool isCo2Calib(void) {
bool ret = co2Calib;
if (ret) {
co2Calib = false;
}
return ret;
}
/**
* @brief Get the Co2 auto calib period
*
* @return int days, -1 if invalid.
*/
int getCo2Abccalib(void) { return co2AbcCalib; }
/**
* @brief Get device configuration model name
*
* @return String Model name, empty string if server failed
*/
String getModelName(void) { return String(models); }
/**
* @brief Get mqttBroker url
*
* @return String Broker url, empty if server failed
*/
String getMqttBroker(void) { return String(mqttBroker); }
/**
* @brief Show server configuration parameter
*/
void showServerConfig(void) {
Serial.println("Server configuration: ");
Serial.printf(" inF: %s\r\n", inF ? "true" : "false");
Serial.printf(" inUSAQI: %s\r\n", inUSAQI ? "true" : "false");
Serial.printf(" useRGBLedBar: %d\r\n", (int)ledBarMode);
Serial.printf(" Model: %s\r\n", models);
Serial.printf(" Mqtt Broker: %s\r\n", mqttBroker);
Serial.printf(" S8 calib period: %d\r\n", co2AbcCalib);
}
/**
* @brief Get server config led bar mode
*
* @return UseLedBar
*/
UseLedBar getLedBarMode(void) { return ledBarMode; }
private:
bool inF; /** Temperature unit, true: F, false: C */
bool inUSAQI; /** PMS unit, true: USAQI, false: ugm3 */
bool configFailed; /** Flag indicate get server configuration failed */
bool serverFailed; /** Flag indicate post data to server failed */
bool co2Calib; /** Is co2Ppmcalibration requset */
int co2AbcCalib = -1; /** update auto calibration number of day */
UseLedBar ledBarMode = UseLedBarCO2; /** */
char models[20]; /** */
char mqttBroker[256]; /** */
};
AgServer agServer;
/** Create airgradient instance for 'OPEN_AIR_OUTDOOR' board */
AirGradient ag(OPEN_AIR_OUTDOOR);
static int ledSmState = APP_SM_NORMAL;
int loopCount = 0;
WiFiManager wifiManager; /** wifi manager instance */
static bool wifiHasConfig = false;
static String wifiSSID = "";
int tvocIndex = -1;
int noxIndex = -1;
int co2Ppm = 0;
int pm25_1 = -1;
int pm01_1 = -1;
int pm10_1 = -1;
int pm03PCount_1 = -1;
float temp_1;
int hum_1;
int pm25_2 = -1;
int pm01_2 = -1;
int pm10_2 = -1;
int pm03PCount_2 = -1;
float temp_2;
int hum_2;
int pm1Value01;
int pm1Value25;
int pm1Value10;
int pm1PCount;
int pm1temp;
int pm1hum;
int pm2Value01;
int pm2Value25;
int pm2Value10;
int pm2PCount;
int pm2temp;
int pm2hum;
int countPosition;
const int targetCount = 20;
enum {
FW_MODE_PST, /** PMS5003T, S8 and SGP41 */
FW_MODE_PPT, /** PMS5003T_1, PMS5003T_2, SGP41 */
FW_MODE_PP /** PMS5003T_1, PMS5003T_2 */
};
int fw_mode = FW_MODE_PST;
void boardInit(void);
void failedHandler(String msg);
void co2Calibration(void);
static String getDevId(void);
static void updateWiFiConnect(void);
static void tvocPoll(void);
static void pmPoll(void);
static void sendDataToServer(void);
static void co2Poll(void);
static void serverConfigPoll(void);
static const char *getFwMode(int mode);
AgSchedule configSchedule(SERVER_CONFIG_UPDATE_INTERVAL, serverConfigPoll);
AgSchedule serverSchedule(SERVER_SYNC_INTERVAL, sendDataToServer);
AgSchedule co2Schedule(SENSOR_CO2_UPDATE_INTERVAL, co2Poll);
AgSchedule pmsSchedule(SENSOR_PM_UPDATE_INTERVAL, pmPoll);
AgSchedule tvocSchedule(SENSOR_TVOC_UPDATE_INTERVAL, tvocPoll);
void setup() {
Serial.begin(115200);
/** Board init */
boardInit();
/** Server init */
agServer.begin();
/** WiFi connect */
connectToWifi();
if (WiFi.isConnected()) {
wifiHasConfig = true;
sendPing();
agServer.pollServerConfig(getDevId());
if (agServer.isConfigFailed()) {
ledSmHandler(APP_SM_WIFI_OK_SERVER_OK_SENSOR_CONFIG_FAILED);
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
}
}
ledSmHandler(APP_SM_NORMAL);
}
void loop() {
configSchedule.run();
serverSchedule.run();
if (fw_mode == FW_MODE_PST) {
co2Schedule.run();
}
pmsSchedule.run();
if (fw_mode == FW_MODE_PST || fw_mode == FW_MODE_PPT) {
tvocSchedule.run();
}
updateWiFiConnect();
}
void sendPing() {
JSONVar root;
root["wifi"] = WiFi.RSSI();
root["boot"] = loopCount;
if (agServer.postToServer(getDevId(), JSON.stringify(root))) {
ledSmHandler(APP_SM_WIFI_OK_SERVER_CONNNECTED);
} else {
ledSmHandler(APP_SM_WIFI_OK_SERVER_CONNECT_FAILED);
}
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
}
static void sendDataToServer(void) {
JSONVar root;
root["wifi"] = WiFi.RSSI();
root["boot"] = loopCount;
if (fw_mode == FW_MODE_PST) {
if (co2Ppm >= 0) {
root["rco2"] = co2Ppm;
}
if (pm01_1 >= 0) {
root["pm01"] = pm01_1;
}
if (pm25_1 >= 0) {
root["pm02"] = pm25_1;
}
if (pm10_1 >= 0) {
root["pm10"] = pm10_1;
}
if (pm03PCount_1 >= 0) {
root["pm003_count"] = pm03PCount_1;
}
if (tvocIndex >= 0) {
root["tvoc_index"] = tvocIndex;
}
if (noxIndex >= 0) {
root["noxIndex"] = noxIndex;
}
if (temp_1 >= 0) {
root["atmp"] = temp_1;
}
if (hum_1 >= 0) {
root["rhum"] = hum_1;
}
} else if (fw_mode == FW_MODE_PPT) {
if (tvocIndex > 0) {
root["tvoc_index"] = loopCount;
}
if (noxIndex > 0) {
root["nox_index"] = loopCount;
}
}
if (fw_mode == FW_MODE_PP || FW_MODE_PPT) {
root["pm01"] = (int)((pm01_1 + pm01_2) / 2);
root["pm02"] = (int)((pm25_1 + pm25_2) / 2);
root["pm003_count"] = (int)((pm03PCount_1 + pm03PCount_2) / 2);
root["atmp"] = (int)((temp_1 + temp_2) / 2);
root["rhum"] = (int)((hum_1 + hum_2) / 2);
root["channels"]["1"]["pm01"] = pm01_1;
root["channels"]["1"]["pm02"] = pm25_1;
root["channels"]["1"]["pm10"] = pm10_1;
root["channels"]["1"]["pm003_count"] = pm03PCount_1;
root["channels"]["1"]["atmp"] = temp_1;
root["channels"]["1"]["rhum"] = hum_1;
root["channels"]["2"]["pm01"] = pm01_2;
root["channels"]["2"]["pm02"] = pm25_2;
root["channels"]["2"]["pm10"] = pm10_2;
root["channels"]["2"]["pm003_count"] = pm03PCount_2;
root["channels"]["2"]["atmp"] = temp_2;
root["channels"]["2"]["rhum"] = hum_2;
}
/** Send data to sensor */
if (agServer.postToServer(getDevId(), JSON.stringify(root))) {
resetWatchdog();
}
loopCount++;
}
void resetWatchdog() {
Serial.println("Watchdog reset");
ag.watchdog.reset();
}
bool wifiMangerClientConnected(void) {
return WiFi.softAPgetStationNum() ? true : false;
}
// Wifi Manager
void connectToWifi() {
wifiSSID = "airgradient-" + String(getNormalizedMac());
wifiManager.setConfigPortalBlocking(false);
wifiManager.setTimeout(WIFI_CONNECT_COUNTDOWN_MAX);
wifiManager.setAPCallback([](WiFiManager *obj) {
/** This callback if wifi connnected failed and try to start configuration
* portal */
ledSmState = APP_SM_WIFI_MANAGER_MODE;
});
wifiManager.setSaveConfigCallback([]() {
/** Wifi connected save the configuration */
ledSmHandler(APP_SM_WIFI_MANAGER_STA_CONNECTED);
});
wifiManager.setSaveParamsCallback([]() {
/** Wifi set connect: ssid, password */
ledSmHandler(APP_SM_WIFI_MANAGER_STA_CONNECTING);
});
wifiManager.autoConnect(wifiSSID.c_str(), WIFI_HOTSPOT_PASSWORD_DEFAULT);
xTaskCreate(
[](void *obj) {
while (wifiManager.getConfigPortalActive()) {
wifiManager.process();
}
vTaskDelete(NULL);
},
"wifi_cfg", 4096, NULL, 10, NULL);
uint32_t stimer = millis();
bool clientConnectChanged = false;
while (wifiManager.getConfigPortalActive()) {
if (WiFi.isConnected() == false) {
if (ledSmState == APP_SM_WIFI_MANAGER_MODE) {
uint32_t ms = (uint32_t)(millis() - stimer);
if (ms >= 100) {
stimer = millis();
ledSmHandler(ledSmState);
}
}
/** Check for client connect to change led color */
bool clientConnected = wifiMangerClientConnected();
if (clientConnected != clientConnectChanged) {
clientConnectChanged = clientConnected;
if (clientConnectChanged) {
ledSmHandler(APP_SM_WIFI_MAMAGER_PORTAL_ACTIVE);
} else {
ledSmHandler(APP_SM_WIFI_MANAGER_MODE);
}
}
}
}
/** Show display wifi connect result failed */
ag.statusLed.setOff();
delay(2000);
if (WiFi.isConnected() == false) {
ledSmHandler(APP_SM_WIFI_MANAGER_CONNECT_FAILED);
}
}
String getNormalizedMac() {
String mac = WiFi.macAddress();
mac.replace(":", "");
mac.toLowerCase();
return mac;
}
void boardInit(void) {
if (Wire.begin(ag.getI2cSdaPin(), ag.getI2cSclPin()) == false) {
failedHandler("Init I2C failed");
}
ag.watchdog.begin();
ag.button.begin();
ag.statusLed.begin();
/** detect sensor: PMS5003, PMS5003T, SGP41 and S8 */
if (ag.s8.begin(Serial1) == false) {
Serial.println("S8 not detect run mode 'PPT'");
fw_mode = FW_MODE_PPT;
/** De-initialize Serial1 */
Serial1.end();
}
if (ag.sgp41.begin(Wire) == false) {
if (fw_mode == FW_MODE_PST) {
failedHandler("Init SGP41 failed");
} else {
Serial.println("SGP41 not detect run mode 'PP'");
fw_mode = FW_MODE_PP;
}
}
if (ag.pms5003t_1.begin(Serial0) == false) {
failedHandler("Init PMS5003T_1 failed");
}
if (fw_mode != FW_MODE_PST) {
if (ag.pms5003t_2.begin(Serial1) == false) {
failedHandler("Init PMS5003T_2 failed");
}
}
if (fw_mode != FW_MODE_PST) {
pmsSchedule.setPeriod(2000);
}
Serial.printf("Firmware node: %s\r\n", getFwMode(fw_mode));
}
void failedHandler(String msg) {
while (true) {
Serial.println(msg);
vTaskDelay(1000);
}
}
void co2Calibration(void) {
/** Count down for co2CalibCountdown secs */
for (int i = 0; i < SENSOR_CO2_CALIB_COUNTDOWN_MAX; i++) {
Serial.printf("Start CO2 calib after %d sec\r\n",
SENSOR_CO2_CALIB_COUNTDOWN_MAX - i);
delay(1000);
}
if (ag.s8.setBaselineCalibration()) {
Serial.println("Calibration success");
delay(1000);
Serial.println("Wait for calib finish...");
int count = 0;
while (ag.s8.isBaseLineCalibrationDone() == false) {
delay(1000);
count++;
}
Serial.printf("Calib finish after %d sec\r\n", count);
delay(2000);
} else {
Serial.println("Calibration failure!!!");
delay(2000);
}
}
/**
* @brief WiFi reconnect handler
*/
static void updateWiFiConnect(void) {
static uint32_t lastRetry;
if (wifiHasConfig == false) {
return;
}
if (WiFi.isConnected()) {
lastRetry = millis();
return;
}
uint32_t ms = (uint32_t)(millis() - lastRetry);
if (ms >= WIFI_CONNECT_RETRY_MS) {
lastRetry = millis();
WiFi.reconnect();
Serial.printf("Re-Connect WiFi\r\n");
}
}
/**
* @brief Update tvocIndexindex
*
*/
static void tvocPoll(void) {
tvocIndex = ag.sgp41.getTvocIndex();
noxIndex = ag.sgp41.getNoxIndex();
Serial.printf("tvocIndexindex: %d\r\n", tvocIndex);
Serial.printf(" NOx index: %d\r\n", noxIndex);
}
/**
* @brief Update PMS data
*
*/
static void pmPoll(void) {
if (fw_mode == FW_MODE_PST) {
if (ag.pms5003t_1.readData()) {
pm01_1 = ag.pms5003t_1.getPm01Ae();
pm25_1 = ag.pms5003t_1.getPm25Ae();
pm25_1 = ag.pms5003t_1.getPm10Ae();
pm03PCount_1 = ag.pms5003t_1.getPm03ParticleCount();
temp_1 = ag.pms5003t_1.getTemperature();
hum_1 = ag.pms5003t_1.getRelativeHumidity();
}
} else {
if (ag.pms5003t_1.readData() && ag.pms5003t_2.readData()) {
pm1Value01 = pm1Value01 + ag.pms5003t_1.getPm01Ae();
pm1Value25 = pm1Value25 + ag.pms5003t_1.getPm25Ae();
pm1Value10 = pm1Value10 + ag.pms5003t_1.getPm10Ae();
pm1PCount = pm1PCount + ag.pms5003t_1.getPm03ParticleCount();
pm1temp = pm1temp + ag.pms5003t_1.getTemperature();
pm1hum = pm1hum + ag.pms5003t_1.getRelativeHumidity();
pm2Value01 = pm2Value01 + ag.pms5003t_2.getPm01Ae();
pm2Value25 = pm2Value25 + ag.pms5003t_2.getPm25Ae();
pm2Value10 = pm2Value10 + ag.pms5003t_2.getPm10Ae();
pm2PCount = pm2PCount + ag.pms5003t_2.getPm03ParticleCount();
pm2temp = pm2temp + ag.pms5003t_2.getTemperature();
pm2hum = pm2hum + ag.pms5003t_2.getRelativeHumidity();
countPosition++;
if (countPosition == targetCount) {
pm01_1 = pm1Value01 / targetCount;
pm25_1 = pm1Value25 / targetCount;
pm10_1 = pm1Value10 / targetCount;
pm03PCount_1 = pm1PCount / targetCount;
temp_1 = pm1temp / targetCount;
hum_1 = pm1hum / targetCount;
pm01_2 = pm2Value01 / targetCount;
pm25_2 = pm2Value25 / targetCount;
pm10_2 = pm2Value10 / targetCount;
pm03PCount_2 = pm2PCount / targetCount;
temp_2 = pm2temp / targetCount;
hum_2 = pm2hum / targetCount;
countPosition = 0;
pm1Value01 = 0;
pm1Value25 = 0;
pm1Value10 = 0;
pm1PCount = 0;
pm1temp = 0;
pm1hum = 0;
pm2Value01 = 0;
pm2Value25 = 0;
pm2Value10 = 0;
pm2PCount = 0;
pm2temp = 0;
pm2hum = 0;
}
}
}
}
static void co2Poll(void) {
co2Ppm = ag.s8.getCo2();
Serial.printf("CO2 index: %d\r\n", co2Ppm);
}
static void serverConfigPoll(void) {
if (agServer.pollServerConfig(getDevId())) {
/** Only support CO2 S8 sensor on FW_MODE_PST */
if (fw_mode == FW_MODE_PST) {
if (agServer.isCo2Calib()) {
co2Calibration();
}
if (agServer.getCo2Abccalib() > 0) {
if (ag.s8.setAutoCalib(agServer.getCo2Abccalib() * 24) == false) {
Serial.println("Set S8 auto calib failed");
}
}
}
}
}
static String getDevId(void) { return getNormalizedMac(); }
void ledBlinkDelay(uint32_t tdelay) {
ag.statusLed.setOn();
delay(tdelay);
ag.statusLed.setOff();
delay(tdelay);
}
void ledSmHandler(int sm) {
if (sm > APP_SM_NORMAL) {
return;
}
ledSmState = sm;
switch (sm) {
case APP_SM_WIFI_MANAGER_MODE: {
ag.statusLed.setToggle();
break;
}
case APP_SM_WIFI_MAMAGER_PORTAL_ACTIVE: {
ag.statusLed.setOn();
break;
}
case APP_SM_WIFI_MANAGER_STA_CONNECTING: {
ag.statusLed.setOff();
break;
}
case APP_SM_WIFI_MANAGER_STA_CONNECTED: {
ag.statusLed.setOff();
break;
}
case APP_SM_WIFI_OK_SERVER_CONNECTING: {
ag.statusLed.setOff();
break;
}
case APP_SM_WIFI_OK_SERVER_CONNNECTED: {
ag.statusLed.setOff();
/** two time slow blink, then off */
for (int i = 0; i < 2; i++) {
ledBlinkDelay(LED_SLOW_BLINK_DELAY);
}
ag.statusLed.setOff();
break;
}
case APP_SM_WIFI_MANAGER_CONNECT_FAILED: {
/** Three time fast blink then 2 sec pause. Repeat 3 times */
ag.statusLed.setOff();
for (int j = 0; j < 3; j++) {
for (int i = 0; i < 3; i++) {
ledBlinkDelay(LED_FAST_BLINK_DELAY);
}
delay(2000);
}
ag.statusLed.setOff();
break;
}
case APP_SM_WIFI_OK_SERVER_CONNECT_FAILED: {
ag.statusLed.setOff();
for (int j = 0; j < 3; j++) {
for (int i = 0; i < 4; i++) {
ledBlinkDelay(LED_FAST_BLINK_DELAY);
}
delay(2000);
}
ag.statusLed.setOff();
break;
}
case APP_SM_WIFI_OK_SERVER_OK_SENSOR_CONFIG_FAILED: {
ag.statusLed.setOff();
for (int j = 0; j < 3; j++) {
for (int i = 0; i < 5; i++) {
ledBlinkDelay(LED_FAST_BLINK_DELAY);
}
delay(2000);
}
ag.statusLed.setOff();
break;
}
case APP_SM_WIFI_LOST: {
ag.statusLed.setOff();
break;
}
case APP_SM_SERVER_LOST: {
ag.statusLed.setOff();
break;
}
case APP_SM_SENSOR_CONFIG_FAILED: {
ag.statusLed.setOff();
break;
}
case APP_SM_NORMAL: {
ag.statusLed.setOff();
break;
}
default:
break;
}
}
static const char *getFwMode(int mode) {
switch (mode) {
case FW_MODE_PST:
return "FW_MODE_PST";
case FW_MODE_PPT:
return "FW_MODE_PPT";
case FW_MODE_PP:
return "FW_MODE_PP";
default:
break;
}
return "FW_MODE_UNKNOW";
}

View File

@ -25,7 +25,7 @@ void setup()
if (ag.s8.begin(&Serial) == false)
{
#else
if (ag.s8.begin(Serial1) == false)
if (ag.s8.begin(Serial0) == false)
{
#endif
failedHandler("SenseAir S8 init failed");

View File

@ -10,8 +10,8 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
#ifdef ESP8266
AirGradient ag = AirGradient(DIY_BASIC);
#else
// AirGradient ag = AirGradient(ONE_INDOOR);
AirGradient ag = AirGradient(OPEN_AIR_OUTDOOR);
AirGradient ag = AirGradient(ONE_INDOOR);
// AirGradient ag = AirGradient(OPEN_AIR_OUTDOOR);
#endif
void failedHandler(String msg);
@ -35,42 +35,56 @@ void setup() {
#endif
}
uint32_t lastRead = 0;
void loop() {
int PM2;
bool readResul = false;
#ifdef ESP8266
if (ag.pms5003.readData()) {
PM2 = ag.pms5003.getPm25Ae();
Serial.printf("PM2.5 in ug/m3: %d\r\n", PM2);
Serial.printf("PM2.5 in US AQI: %d\r\n",
ag.pms5003.convertPm25ToUsAqi(PM2));
}
#else
if (ag.getBoardType() == OPEN_AIR_OUTDOOR) {
if (ag.pms5003t_1.readData()) {
PM2 = ag.pms5003t_1.getPm25Ae();
readResul = true;
}
} else {
if (ag.pms5003.readData()) {
PM2 = ag.pms5003.getPm25Ae();
readResul = true;
}
}
if (readResul) {
Serial.printf("PM2.5 in ug/m3: %d\r\n", PM2);
if (ag.getBoardType() == OPEN_AIR_OUTDOOR) {
Serial.printf("PM2.5 in US AQI: %d\r\n",
ag.pms5003t_1.convertPm25ToUsAqi(PM2));
} else {
uint32_t ms = (uint32_t)(millis() - lastRead);
if (ms >= 5000) {
lastRead = millis();
#ifdef ESP8266
if (ag.pms5003.isFailed() == false) {
PM2 = ag.pms5003.getPm25Ae();
Serial.printf("PM2.5 in ug/m3: %d\r\n", PM2);
Serial.printf("PM2.5 in US AQI: %d\r\n",
ag.pms5003.convertPm25ToUsAqi(PM2));
} else {
Serial.println("PMS sensor failed");
}
#else
if (ag.getBoardType() == OPEN_AIR_OUTDOOR) {
if (ag.pms5003t_1.isFailed() == false) {
PM2 = ag.pms5003t_1.getPm25Ae();
readResul = true;
}
} else {
if (ag.pms5003.isFailed() == false) {
PM2 = ag.pms5003.getPm25Ae();
readResul = true;
}
}
}
#endif
delay(5000);
if (readResul) {
Serial.printf("PM2.5 in ug/m3: %d\r\n", PM2);
if (ag.getBoardType() == OPEN_AIR_OUTDOOR) {
Serial.printf("PM2.5 in US AQI: %d\r\n",
ag.pms5003t_1.convertPm25ToUsAqi(PM2));
} else {
Serial.printf("PM2.5 in US AQI: %d\r\n",
ag.pms5003.convertPm25ToUsAqi(PM2));
}
} else {
Serial.println("PMS sensor failed");
}
#endif
}
if (ag.getBoardType() == OPEN_AIR_OUTDOOR) {
ag.pms5003t_1.handle();
} else {
ag.pms5003.handle();
}
}
void failedHandler(String msg) {

View File

@ -1,5 +1,5 @@
name=AirGradient Air Quality Sensor
version=3.0.3
version=3.1.5
author=AirGradient <support@airgradient.com>
maintainer=AirGradient <support@airgradient.com>
sentence=ESP32-C3 / ESP8266 library for air quality monitor measuring PM, CO2, Temperature, TVOC and Humidity with OLED display.

7
partitions.csv Normal file
View File

@ -0,0 +1,7 @@
# Name ,Type ,SubType ,Offset ,Size ,Flags
nvs ,data ,nvs ,0x9000 ,0x5000 ,
otadata ,data ,ota ,0xe000 ,0x2000 ,
app0 ,app ,ota_0 ,0x10000 ,0x1E0000 ,
app1 ,app ,ota_1 ,0x1F0000 ,0x1E0000 ,
spiffs ,data ,spiffs ,0x3D0000 ,0x20000 ,
coredump ,data ,coredump ,0x3F0000 ,0x10000 ,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x2000
4 app0 app ota_0 0x10000 0x1E0000
5 app1 app ota_1 0x1F0000 0x1E0000
6 spiffs data spiffs 0x3D0000 0x20000
7 coredump data coredump 0x3F0000 0x10000

51
platformio.ini Normal file
View File

@ -0,0 +1,51 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:esp32-c3]
platform = espressif32
board = esp32-c3-devkitm-1
framework = arduino
build_flags = !echo '-D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 -D GIT_VERSION=\\"'$(git describe --tags --always --dirty)'\\"'
board_build.partitions = partitions.csv
monitor_speed = 115200
lib_deps =
aglib=symlink://../arduino
EEPROM
WebServer
ESPmDNS
FS
SPIFFS
HTTPClient
WiFiClientSecure
Update
DNSServer
[env:esp8266]
platform = espressif8266
board = d1_mini
framework = arduino
monitor_speed = 115200
lib_deps =
aglib=symlink://../arduino
EEPROM
ESP8266HTTPClient
ESP8266WebServer
DNSServer
monitor_filters = time
[platformio]
src_dir = examples/OneOpenAir
; src_dir = examples/BASIC
; src_dir = examples/DiyProIndoorV4_2
; src_dir = examples/DiyProIndoorV3_3
; src_dir = examples/TestCO2
; src_dir = examples/TestPM
; src_dir = examples/TestSht

192
src/AgApiClient.cpp Normal file
View File

@ -0,0 +1,192 @@
#include "AgApiClient.h"
#include "AgConfigure.h"
#include "AirGradient.h"
#include "Libraries/Arduino_JSON/src/Arduino_JSON.h"
#ifdef ESP8266
#include <ESP8266HTTPClient.h>
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#else
#include <HTTPClient.h>
#endif
AgApiClient::AgApiClient(Stream &debug, Configuration &config)
: PrintLog(debug, "ApiClient"), config(config) {}
AgApiClient::~AgApiClient() {}
/**
* @brief Initialize the API client
*
*/
void AgApiClient::begin(void) {
getConfigFailed = false;
postToServerFailed = false;
logInfo("Init apiRoot: " + apiRoot);
logInfo("begin");
}
/**
* @brief Get configuration from AirGradient cloud
*
* @param deviceId Device ID
* @return true Success
* @return false Failure
*/
bool AgApiClient::fetchServerConfiguration(void) {
if (config.getConfigurationControl() ==
ConfigurationControl::ConfigurationControlLocal ||
config.isOfflineMode()) {
logWarning("Ignore fetch server configuration");
// Clear server configuration failed flag, cause it's ignore but not
// really failed
getConfigFailed = false;
return false;
}
String uri = apiRoot + "/sensors/airgradient:" +
ag->deviceId() + "/one/config";
/** Init http client */
#ifdef ESP8266
HTTPClient client;
WiFiClient wifiClient;
if (client.begin(wifiClient, uri) == false) {
getConfigFailed = true;
return false;
}
#else
HTTPClient client;
if (client.begin(uri) == false) {
getConfigFailed = true;
return false;
}
#endif
/** Get data */
int retCode = client.GET();
logInfo(String("GET: ") + uri);
logInfo(String("Return code: ") + String(retCode));
if (retCode != 200) {
client.end();
getConfigFailed = true;
/** Return code 400 mean device not setup on cloud. */
if (retCode == 400) {
notAvailableOnDashboard = true;
}
return false;
}
/** clear failed */
getConfigFailed = false;
notAvailableOnDashboard = false;
/** Get response string */
String respContent = client.getString();
client.end();
// logInfo("Get configuration: " + respContent);
/** Parse configuration and return result */
return config.parse(respContent, false);
}
/**
* @brief Post data to AirGradient cloud
*
* @param deviceId Device Id
* @param data String JSON
* @return true Success
* @return false Failure
*/
bool AgApiClient::postToServer(String data) {
if (config.isPostDataToAirGradient() == false) {
logWarning("Ignore post data to server");
return true;
}
if (WiFi.isConnected() == false) {
return false;
}
String uri =
"http://hw.airgradient.com/sensors/airgradient:" + ag->deviceId() +
"/measures";
// logInfo("Post uri: " + uri);
// logInfo("Post data: " + data);
WiFiClient wifiClient;
HTTPClient client;
if (client.begin(wifiClient, uri.c_str()) == false) {
logError("Init client failed");
return false;
}
client.addHeader("content-type", "application/json");
int retCode = client.POST(data);
client.end();
logInfo(String("POST: ") + uri);
logInfo(String("DATA: ") + data);
logInfo(String("Return code: ") + String(retCode));
if ((retCode == 200) || (retCode == 429)) {
postToServerFailed = false;
return true;
} else {
logError("Post response failed code: " + String(retCode));
}
postToServerFailed = true;
return false;
}
/**
* @brief Get failed status when get configuration from AirGradient cloud
*
* @return true Success
* @return false Failure
*/
bool AgApiClient::isFetchConfigureFailed(void) { return getConfigFailed; }
/**
* @brief Get failed status when post data to AirGradient cloud
*
* @return true Success
* @return false Failure
*/
bool AgApiClient::isPostToServerFailed(void) { return postToServerFailed; }
/**
* @brief Get status device has available on dashboard or not. should get after
* fetch configuration return failed
*
* @return true
* @return false
*/
bool AgApiClient::isNotAvailableOnDashboard(void) {
return notAvailableOnDashboard;
}
void AgApiClient::setAirGradient(AirGradient *ag) { this->ag = ag; }
/**
* @brief Send the package to check the connection with cloud
*
* @param rssi WiFi RSSI
* @param bootCount Boot count
* @return true Success
* @return false Failure
*/
bool AgApiClient::sendPing(int rssi, int bootCount) {
JSONVar root;
root["wifi"] = rssi;
root["boot"] = bootCount;
return postToServer(JSON.stringify(root));
}
String AgApiClient::getApiRoot() const { return apiRoot; }
void AgApiClient::setApiRoot(const String &apiRoot) { this->apiRoot = apiRoot; }

45
src/AgApiClient.h Normal file
View File

@ -0,0 +1,45 @@
/**
* @file AgApiClient.h
* @brief HTTP client connect post data to Aigradient cloud.
* @version 0.1
* @date 2024-Apr-02
*
* @copyright Copyright (c) 2024
*
*/
#ifndef _AG_API_CLIENT_H_
#define _AG_API_CLIENT_H_
#include "AgConfigure.h"
#include "AirGradient.h"
#include "Main/PrintLog.h"
#include <Arduino.h>
class AgApiClient : public PrintLog {
private:
Configuration &config;
AirGradient *ag;
String apiRoot = "http://hw.airgradient.com";
bool getConfigFailed;
bool postToServerFailed;
bool notAvailableOnDashboard = false; // Device not setup on Airgradient cloud dashboard.
public:
AgApiClient(Stream &stream, Configuration &config);
~AgApiClient();
void begin(void);
bool fetchServerConfiguration(void);
bool postToServer(String data);
bool isFetchConfigureFailed(void);
bool isPostToServerFailed(void);
bool isNotAvailableOnDashboard(void);
void setAirGradient(AirGradient *ag);
bool sendPing(int rssi, int bootCount);
String getApiRoot() const;
void setApiRoot(const String &apiRoot);
};
#endif /** _AG_API_CLIENT_H_ */

1186
src/AgConfigure.cpp Normal file

File diff suppressed because it is too large Load Diff

87
src/AgConfigure.h Normal file
View File

@ -0,0 +1,87 @@
#ifndef _AG_CONFIG_H_
#define _AG_CONFIG_H_
#include "App/AppDef.h"
#include "Main/PrintLog.h"
#include "AirGradient.h"
#include <Arduino.h>
class Configuration : public PrintLog {
private:
bool co2CalibrationRequested;
bool ledBarTestRequested;
bool udpated;
String failedMessage;
bool _noxLearnOffsetChanged;
bool _tvocLearningOffsetChanged;
bool ledBarBrightnessChanged = false;
bool displayBrightnessChanged = false;
String otaNewFirmwareVersion;
bool _offlineMode = false;
bool _ledBarModeChanged = false;
AirGradient* ag;
String getLedBarModeName(LedBarMode mode);
void saveConfig(void);
void loadConfig(void);
void defaultConfig(void);
void printConfig(void);
String jsonTypeInvalidMessage(String name, String type);
String jsonValueInvalidMessage(String name, String value);
void jsonInvalid(void);
void configLogInfo(String name, String fromValue, String toValue);
String getPMStandardString(bool usaqi);
String getAbcDayString(int value);
void toConfig(const char* buf);
public:
Configuration(Stream &debugLog);
~Configuration();
bool hasSensorS8 = true;
bool hasSensorPMS1 = true;
bool hasSensorPMS2 = true;
bool hasSensorSGP = true;
bool hasSensorSHT = true;
bool begin(void);
bool parse(String data, bool isLocal);
String toString(void);
String toString(AgFirmwareMode fwMode);
bool isTemperatureUnitInF(void);
String getCountry(void);
bool isPmStandardInUSAQI(void);
int getCO2CalibrationAbcDays(void);
LedBarMode getLedBarMode(void);
String getLedBarModeName(void);
bool getDisplayMode(void);
String getMqttBrokerUri(void);
bool isPostDataToAirGradient(void);
ConfigurationControl getConfigurationControl(void);
bool isCo2CalibrationRequested(void);
bool isLedBarTestRequested(void);
void reset(void);
String getModel(void);
bool isUpdated(void);
String getFailedMesage(void);
void setPostToAirGradient(bool enable);
bool noxLearnOffsetChanged(void);
bool tvocLearnOffsetChanged(void);
int getTvocLearningOffset(void);
int getNoxLearningOffset(void);
String wifiSSID(void);
String wifiPass(void);
void setAirGradient(AirGradient *ag);
bool isLedBarBrightnessChanged(void);
int getLedBarBrightness(void);
bool isDisplayBrightnessChanged(void);
int getDisplayBrightness(void);
String newFirmwareVersion(void);
bool isOfflineMode(void);
void setOfflineMode(bool offline);
void setOfflineModeWithoutSave(bool offline);
bool isLedBarModeChanged(void);
};
#endif /** _AG_CONFIG_H_ */

532
src/AgOledDisplay.cpp Normal file
View File

@ -0,0 +1,532 @@
#include "AgOledDisplay.h"
#include "Libraries/U8g2/src/U8g2lib.h"
#include "Main/utils.h"
/** Cast U8G2 */
#define DISP() ((U8G2_SH1106_128X64_NONAME_F_HW_I2C *)(this->u8g2))
/**
* @brief Show dashboard temperature and humdity
*
* @param hasStatus
*/
void OledDisplay::showTempHum(bool hasStatus) {
char buf[16];
if (utils::isValidTemperature(value.Temperature)) {
if (config.isTemperatureUnitInF()) {
float tempF = (value.Temperature * 9) / 5 + 32;
if (hasStatus) {
snprintf(buf, sizeof(buf), "%0.1f", tempF);
} else {
snprintf(buf, sizeof(buf), "%0.1f°F", tempF);
}
} else {
if (hasStatus) {
snprintf(buf, sizeof(buf), "%.1f", value.Temperature);
} else {
snprintf(buf, sizeof(buf), "%.1f°C", value.Temperature);
}
}
} else {
if (config.isTemperatureUnitInF()) {
snprintf(buf, sizeof(buf), "-°F");
} else {
snprintf(buf, sizeof(buf), "-°C");
}
}
DISP()->drawUTF8(1, 10, buf);
/** Show humidty */
if (utils::isValidHumidity(value.Humidity)) {
snprintf(buf, sizeof(buf), "%d%%", value.Humidity);
} else {
snprintf(buf, sizeof(buf), "-%%");
}
if (value.Humidity > 99) {
DISP()->drawStr(97, 10, buf);
} else {
DISP()->drawStr(105, 10, buf);
}
}
void OledDisplay::setCentralText(int y, String text) {
setCentralText(y, text.c_str());
}
void OledDisplay::setCentralText(int y, const char *text) {
int x = (DISP()->getWidth() - DISP()->getStrWidth(text)) / 2;
DISP()->drawStr(x, y, text);
}
/**
* @brief Construct a new Ag Oled Display:: Ag Oled Display object
*
* @param config AgConfiguration
* @param value Measurements
* @param log Serial Stream
*/
OledDisplay::OledDisplay(Configuration &config, Measurements &value,
Stream &log)
: PrintLog(log, "OledDisplay"), config(config), value(value) {}
/**
* @brief Set AirGradient instance
*
* @param ag Point to AirGradient instance
*/
void OledDisplay::setAirGradient(AirGradient *ag) { this->ag = ag; }
OledDisplay::~OledDisplay() {}
/**
* @brief Initialize display
*
* @return true Success
* @return false Failure
*/
bool OledDisplay::begin(void) {
if (isBegin) {
logWarning("Already begin, call 'end' and try again");
return true;
}
if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) {
/** Create u8g2 instance */
u8g2 = new U8G2_SH1106_128X64_NONAME_F_HW_I2C(U8G2_R0, U8X8_PIN_NONE);
if (u8g2 == NULL) {
logError("Create 'U8G2' failed");
return false;
}
/** Init u8g2 */
if (DISP()->begin() == false) {
logError("U8G2 'begin' failed");
return false;
}
} else if (ag->isBasic()) {
logInfo("DIY_BASIC init");
ag->display.begin(Wire);
ag->display.setTextColor(1);
ag->display.clear();
ag->display.show();
}
/** Show low brightness on startup. then it's completely turn off on main
* application */
int brightness = config.getDisplayBrightness();
if (brightness == 0) {
setBrightness(1);
}
isBegin = true;
logInfo("begin");
return true;
}
/**
* @brief De-Initialize display
*
*/
void OledDisplay::end(void) {
if (!isBegin) {
logWarning("Already end, call 'begin' and try again");
return;
}
if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) {
/** Free u8g2 */
delete DISP();
u8g2 = NULL;
} else if (ag->isBasic()) {
ag->display.end();
}
isBegin = false;
logInfo("end");
}
/**
* @brief Show text on 3 line of display
*
* @param line1
* @param line2
* @param line3
*/
void OledDisplay::setText(String &line1, String &line2, String &line3) {
setText(line1.c_str(), line2.c_str(), line3.c_str());
}
/**
* @brief Show text on 3 line of display
*
* @param line1
* @param line2
* @param line3
*/
void OledDisplay::setText(const char *line1, const char *line2,
const char *line3) {
if (isDisplayOff) {
return;
}
if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) {
DISP()->firstPage();
do {
DISP()->setFont(u8g2_font_t0_16_tf);
DISP()->drawStr(1, 10, line1);
DISP()->drawStr(1, 30, line2);
DISP()->drawStr(1, 50, line3);
} while (DISP()->nextPage());
} else if (ag->isBasic()) {
ag->display.clear();
ag->display.setCursor(1, 1);
ag->display.setText(line1);
ag->display.setCursor(1, 17);
ag->display.setText(line2);
ag->display.setCursor(1, 33);
ag->display.setText(line3);
ag->display.show();
}
}
/**
* @brief Set Text on 4 line
*
* @param line1
* @param line2
* @param line3
* @param line4
*/
void OledDisplay::setText(String &line1, String &line2, String &line3,
String &line4) {
setText(line1.c_str(), line2.c_str(), line3.c_str(), line4.c_str());
}
/**
* @brief Set Text on 4 line
*
* @param line1
* @param line2
* @param line3
* @param line4
*/
void OledDisplay::setText(const char *line1, const char *line2,
const char *line3, const char *line4) {
if (isDisplayOff) {
return;
}
if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) {
DISP()->firstPage();
do {
DISP()->setFont(u8g2_font_t0_16_tf);
DISP()->drawStr(1, 10, line1);
DISP()->drawStr(1, 25, line2);
DISP()->drawStr(1, 40, line3);
DISP()->drawStr(1, 55, line4);
} while (DISP()->nextPage());
} else if (ag->isBasic()) {
ag->display.clear();
ag->display.setCursor(0, 0);
ag->display.setText(line1);
ag->display.setCursor(0, 10);
ag->display.setText(line2);
ag->display.setCursor(0, 20);
ag->display.setText(line3);
ag->display.show();
}
}
/**
* @brief Update dashboard content
*
*/
void OledDisplay::showDashboard(void) { showDashboard(NULL); }
/**
* @brief Update dashboard content and error status
*
*/
void OledDisplay::showDashboard(const char *status) {
if (isDisplayOff) {
return;
}
char strBuf[16];
if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) {
DISP()->firstPage();
do {
DISP()->setFont(u8g2_font_t0_16_tf);
if ((status == NULL) || (strlen(status) == 0)) {
showTempHum(false);
} else {
String strStatus = "Show status: " + String(status);
logInfo(strStatus);
int strWidth = DISP()->getStrWidth(status);
DISP()->drawStr((DISP()->getWidth() - strWidth) / 2, 10, status);
/** Show WiFi NA*/
if (strcmp(status, "WiFi N/A") == 0) {
DISP()->setFont(u8g2_font_t0_12_tf);
showTempHum(true);
}
}
/** Draw horizonal line */
DISP()->drawLine(1, 13, 128, 13);
/** Show CO2 label */
DISP()->setFont(u8g2_font_t0_12_tf);
DISP()->drawUTF8(1, 27, "CO2");
DISP()->setFont(u8g2_font_t0_22b_tf);
if (utils::isValidCO2(value.CO2)) {
sprintf(strBuf, "%d", value.CO2);
} else {
sprintf(strBuf, "%s", "-");
}
DISP()->drawStr(1, 48, strBuf);
/** Show CO2 value index */
DISP()->setFont(u8g2_font_t0_12_tf);
DISP()->drawStr(1, 61, "ppm");
/** Draw vertical line */
DISP()->drawLine(52, 14, 52, 64);
DISP()->drawLine(97, 14, 97, 64);
/** Draw PM2.5 label */
DISP()->setFont(u8g2_font_t0_12_tf);
DISP()->drawStr(55, 27, "PM2.5");
/** Draw PM2.5 value */
int pm25 = value.pm25_1;
if (config.hasSensorSHT) {
pm25 = ag->pms5003.compensated(pm25, value.Humidity);
}
DISP()->setFont(u8g2_font_t0_22b_tf);
if (config.isPmStandardInUSAQI()) {
if (utils::isValidPMS(value.pm25_1)) {
sprintf(strBuf, "%d", ag->pms5003.convertPm25ToUsAqi(value.pm25_1));
} else {
sprintf(strBuf, "%s", "-");
}
DISP()->drawStr(55, 48, strBuf);
DISP()->setFont(u8g2_font_t0_12_tf);
DISP()->drawUTF8(55, 61, "AQI");
} else {
if (utils::isValidPMS(value.pm25_1)) {
sprintf(strBuf, "%d", value.pm25_1);
} else {
sprintf(strBuf, "%s", "-");
}
DISP()->drawStr(55, 48, strBuf);
DISP()->setFont(u8g2_font_t0_12_tf);
DISP()->drawUTF8(55, 61, "ug/m³");
}
/** Draw tvocIndexlabel */
DISP()->setFont(u8g2_font_t0_12_tf);
DISP()->drawStr(100, 27, "VOC:");
/** Draw tvocIndexvalue */
if (utils::isValidVOC(value.TVOC)) {
sprintf(strBuf, "%d", value.TVOC);
} else {
sprintf(strBuf, "%s", "-");
}
DISP()->drawStr(100, 39, strBuf);
/** Draw NOx label */
DISP()->drawStr(100, 53, "NOx:");
if (utils::isValidNOx(value.NOx)) {
sprintf(strBuf, "%d", value.NOx);
} else {
sprintf(strBuf, "%s", "-");
}
DISP()->drawStr(100, 63, strBuf);
} while (DISP()->nextPage());
} else if (ag->isBasic()) {
ag->display.clear();
/** Set CO2 */
if(utils::isValidCO2(value.CO2)) {
snprintf(strBuf, sizeof(strBuf), "CO2:%d", value.CO2);
} else {
snprintf(strBuf, sizeof(strBuf), "CO2:-");
}
ag->display.setCursor(0, 0);
ag->display.setText(strBuf);
/** Set PM */
int pm25 = value.pm25_1;
if(config.hasSensorSHT) {
pm25 = (int)ag->pms5003.compensated(pm25, value.Humidity);
}
ag->display.setCursor(0, 12);
if (utils::isValidPMS(value.pm25_1)) {
snprintf(strBuf, sizeof(strBuf), "PM2.5:%d", value.pm25_1);
} else {
snprintf(strBuf, sizeof(strBuf), "PM2.5:-");
}
ag->display.setText(strBuf);
/** Set temperature and humidity */
if (utils::isValidTemperature(value.Temperature)) {
if (config.isTemperatureUnitInF()) {
float tempF = (value.Temperature * 9) / 5 + 32;
snprintf(strBuf, sizeof(strBuf), "T:%0.1f F", tempF);
} else {
snprintf(strBuf, sizeof(strBuf), "T:%0.f1 C", value.Temperature);
}
} else {
if (config.isTemperatureUnitInF()) {
snprintf(strBuf, sizeof(strBuf), "T:-F");
} else {
snprintf(strBuf, sizeof(strBuf), "T:-C");
}
}
ag->display.setCursor(0, 24);
ag->display.setText(strBuf);
if (utils::isValidHumidity(value.Humidity)) {
snprintf(strBuf, sizeof(strBuf), "H:%d %%", (int)value.Humidity);
} else {
snprintf(strBuf, sizeof(strBuf), "H:- %%");
}
ag->display.setCursor(0, 36);
ag->display.setText(strBuf);
ag->display.show();
}
}
void OledDisplay::setBrightness(int percent) {
if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) {
if (percent == 0) {
isDisplayOff = true;
// Clear display.
DISP()->firstPage();
do {
} while (DISP()->nextPage());
} else {
isDisplayOff = false;
DISP()->setContrast((127 * percent) / 100);
}
} else if (ag->isBasic()) {
ag->display.setContrast((255 * percent) / 100);
}
}
#ifdef ESP32
void OledDisplay::showFirmwareUpdateVersion(String version) {
if (isDisplayOff) {
return;
}
DISP()->firstPage();
do {
DISP()->setFont(u8g2_font_t0_16_tf);
setCentralText(20, "Firmware Update");
setCentralText(40, "New version");
setCentralText(60, version.c_str());
} while (DISP()->nextPage());
}
void OledDisplay::showFirmwareUpdateProgress(int percent) {
if (isDisplayOff) {
return;
}
DISP()->firstPage();
do {
DISP()->setFont(u8g2_font_t0_16_tf);
setCentralText(20, "Firmware Update");
setCentralText(50, String("Updating... ") + String(percent) + String("%"));
} while (DISP()->nextPage());
}
void OledDisplay::showFirmwareUpdateSuccess(int count) {
if (isDisplayOff) {
return;
}
DISP()->firstPage();
do {
DISP()->setFont(u8g2_font_t0_16_tf);
setCentralText(20, "Firmware Update");
setCentralText(40, "Success");
setCentralText(60, String("Rebooting... ") + String(count));
} while (DISP()->nextPage());
}
void OledDisplay::showFirmwareUpdateFailed(void) {
if (isDisplayOff) {
return;
}
DISP()->firstPage();
do {
DISP()->setFont(u8g2_font_t0_16_tf);
setCentralText(20, "Firmware Update");
setCentralText(40, "fail, will retry");
// setCentralText(60, "will retry");
} while (DISP()->nextPage());
}
void OledDisplay::showFirmwareUpdateSkipped(void) {
if (isDisplayOff) {
return;
}
DISP()->firstPage();
do {
DISP()->setFont(u8g2_font_t0_16_tf);
setCentralText(20, "Firmware Update");
setCentralText(40, "skipped");
} while (DISP()->nextPage());
}
void OledDisplay::showFirmwareUpdateUpToDate(void) {
if (isDisplayOff) {
return;
}
DISP()->firstPage();
do {
DISP()->setFont(u8g2_font_t0_16_tf);
setCentralText(20, "Firmware Update");
setCentralText(40, "up to date");
} while (DISP()->nextPage());
}
#else
#endif
void OledDisplay::showRebooting(void) {
if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) {
DISP()->firstPage();
do {
DISP()->setFont(u8g2_font_t0_16_tf);
// setCentralText(20, "Firmware Update");
setCentralText(40, "Reboot...");
// setCentralText(60, String("Retry after 24h"));
} while (DISP()->nextPage());
} else if (ag->isBasic()) {
ag->display.clear();
ag->display.setCursor(0, 20);
ag->display.setText("Rebooting...");
ag->display.show();
}
}

52
src/AgOledDisplay.h Normal file
View File

@ -0,0 +1,52 @@
#ifndef _AG_OLED_DISPLAY_H_
#define _AG_OLED_DISPLAY_H_
#include "AgConfigure.h"
#include "AgValue.h"
#include "AirGradient.h"
#include "Main/PrintLog.h"
#include <Arduino.h>
class OledDisplay : public PrintLog {
private:
Configuration &config;
AirGradient *ag;
bool isBegin = false;
void *u8g2 = NULL;
Measurements &value;
bool isDisplayOff = false;
void showTempHum(bool hasStatus);
void setCentralText(int y, String text);
void setCentralText(int y, const char *text);
public:
OledDisplay(Configuration &config, Measurements &value,
Stream &log);
~OledDisplay();
void setAirGradient(AirGradient *ag);
bool begin(void);
void end(void);
void setText(String &line1, String &line2, String &line3);
void setText(const char *line1, const char *line2, const char *line3);
void setText(String &line1, String &line2, String &line3, String &line4);
void setText(const char *line1, const char *line2, const char *line3,
const char *line4);
void showDashboard(void);
void showDashboard(const char *status);
void setBrightness(int percent);
#ifdef ESP32
void showFirmwareUpdateVersion(String version);
void showFirmwareUpdateProgress(int percent);
void showFirmwareUpdateSuccess(int count);
void showFirmwareUpdateFailed(void);
void showFirmwareUpdateSkipped(void);
void showFirmwareUpdateUpToDate(void);
#else
#endif
void showRebooting(void);
};
#endif /** _AG_OLED_DISPLAY_H_ */

26
src/AgSchedule.cpp Normal file
View File

@ -0,0 +1,26 @@
#include "AgSchedule.h"
AgSchedule::AgSchedule(int period, void (*handler)(void))
: period(period), handler(handler) {}
AgSchedule::~AgSchedule() {}
void AgSchedule::run(void) {
uint32_t ms = (uint32_t)(millis() - count);
if (ms >= period) {
handler();
count = millis();
}
}
/**
* @brief Set schedule period
*
* @param period Period in ms
*/
void AgSchedule::setPeriod(int period) { this->period = period; }
/**
* @brief Update period
*/
void AgSchedule::update(void) { count = millis(); }

20
src/AgSchedule.h Normal file
View File

@ -0,0 +1,20 @@
#ifndef _AG_SCHEDULE_H_
#define _AG_SCHEDULE_H_
#include <Arduino.h>
class AgSchedule {
private:
int period;
void (*handler)(void);
uint32_t count;
public:
AgSchedule(int period, void (*handler)(void));
~AgSchedule();
void run(void);
void update(void);
void setPeriod(int period);
};
#endif /** _AG_SCHEDULE_H_ */

797
src/AgStateMachine.cpp Normal file
View File

@ -0,0 +1,797 @@
#include "AgStateMachine.h"
#define LED_FAST_BLINK_DELAY 250 /** ms */
#define LED_SLOW_BLINK_DELAY 1000 /** ms */
#define LED_SHORT_BLINK_DELAY 500 /** ms */
#define LED_LONG_BLINK_DELAY 2000 /** ms */
#define SENSOR_CO2_CALIB_COUNTDOWN_MAX 5 /** sec */
#define RGB_COLOR_R 255, 0, 0 /** Red */
#define RGB_COLOR_G 0, 255, 0 /** Green */
#define RGB_COLOR_Y 255, 150, 0 /** Yellow */
#define RGB_COLOR_O 255, 40, 0 /** Orange */
#define RGB_COLOR_P 180, 0, 255 /** Purple */
/**
* @brief Animation LED bar with color
*
* @param r
* @param g
* @param b
*/
void StateMachine::ledBarSingleLedAnimation(uint8_t r, uint8_t g, uint8_t b) {
if (ledBarAnimationCount < 0) {
ledBarAnimationCount = 0;
ag->ledBar.setColor(r, g, b, ledBarAnimationCount);
} else {
ledBarAnimationCount++;
if (ledBarAnimationCount >= ag->ledBar.getNumberOfLeds()) {
ledBarAnimationCount = 0;
}
ag->ledBar.setColor(r, g, b, ledBarAnimationCount);
}
}
/**
* @brief LED status blink with delay
*
* @param ms Miliseconds
*/
void StateMachine::ledStatusBlinkDelay(uint32_t ms) {
ag->statusLed.setOn();
delay(ms);
ag->statusLed.setOff();
delay(ms);
}
/**
* @brief Led bar show led color status
*
*/
void StateMachine::sensorhandleLeds(void) {
switch (config.getLedBarMode()) {
case LedBarMode::LedBarModeCO2:
co2handleLeds();
break;
case LedBarMode::LedBarModePm:
pm25handleLeds();
break;
default:
ag->ledBar.clear();
break;
}
}
/**
* @brief Show CO2 LED status
*
*/
void StateMachine::co2handleLeds(void) {
int co2Value = value.CO2;
if (co2Value <= 600) {
/** G; 1 */
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1);
} else if (co2Value <= 800) {
/** GG; 2 */
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 2);
} else if (co2Value <= 1000) {
/** YYY; 3 */
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 3);
} else if (co2Value <= 1250) {
/** OOOO; 4 */
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 4);
} else if (co2Value <= 1500) {
/** OOOOO; 5 */
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 5);
} else if (co2Value <= 1750) {
/** RRRRRR; 6 */
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 6);
} else if (co2Value <= 2000) {
/** RRRRRRR; 7 */
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 6);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 7);
} else if (co2Value <= 3000) {
/** PPPPPPPP; 8 */
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 6);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 7);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 8);
} else { /** > 3000 */
/* PRPRPRPRP; 9 */
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 6);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 7);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 8);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 9);
}
}
/**
* @brief Show PM2.5 LED status
*
*/
void StateMachine::pm25handleLeds(void) {
int pm25Value = value.pm25_1;
if (pm25Value < 5) {
/** G; 1 */
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1);
} else if (pm25Value < 10) {
/** GG; 2 */
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 2);
} else if (pm25Value < 20) {
/** YYY; 3 */
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 3);
} else if (pm25Value < 35) {
/** YYYY; 4 */
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 4);
} else if (pm25Value < 45) {
/** OOOOO; 5 */
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 5);
} else if (pm25Value < 55) {
/** OOOOOO; 6 */
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 6);
} else if (pm25Value < 100) {
/** RRRRRRR; 7 */
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 6);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 7);
} else if (pm25Value < 200) {
/** RRRRRRRR; 8 */
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 6);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 7);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 8);
} else if (pm25Value < 250) {
/** PPPPPPPPP; 9 */
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 6);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 7);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 8);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 9);
} else { /** > 250 */
/* PRPRPRPRP; 9 */
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 6);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 7);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 8);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 9);
}
}
void StateMachine::co2Calibration(void) {
if (config.isCo2CalibrationRequested() && config.hasSensorS8) {
logInfo("CO2 Calibration");
/** Count down to 0 then start */
for (int i = 0; i < SENSOR_CO2_CALIB_COUNTDOWN_MAX; i++) {
if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3()) {
String str =
"after " + String(SENSOR_CO2_CALIB_COUNTDOWN_MAX - i) + " sec";
disp.setText("Start CO2 calib", str.c_str(), "");
} else if (ag->isBasic()) {
String str = String(SENSOR_CO2_CALIB_COUNTDOWN_MAX - i) + " sec";
disp.setText("CO2 Calib", "after", str.c_str());
} else {
logInfo("Start CO2 calib after " +
String(SENSOR_CO2_CALIB_COUNTDOWN_MAX - i) + " sec");
}
delay(1000);
}
if (ag->s8.setBaselineCalibration()) {
if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3()) {
disp.setText("Calibration", "success", "");
} else if (ag->isBasic()) {
disp.setText("CO2 Calib", "success", "");
} else {
logInfo("CO2 Calibration: success");
}
delay(1000);
if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic()) {
disp.setText("Wait for", "calib done", "...");
} else {
logInfo("CO2 Calibration: Wait for calibration finish...");
}
/** Count down wait for finish */
int count = 0;
while (ag->s8.isBaseLineCalibrationDone() == false) {
delay(1000);
count++;
}
if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic()) {
String str = "after " + String(count);
disp.setText("Calib done", str.c_str(), "sec");
} else {
logInfo("CO2 Calibration: finish after " + String(count) + " sec");
}
delay(2000);
} else {
if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3()) {
disp.setText("Calibration", "failure!!!", "");
} else if (ag->isBasic()) {
disp.setText("CO2 calib", "failure!!!", "");
} else {
logInfo("CO2 Calibration: failure!!!");
}
delay(2000);
}
}
if (config.getCO2CalibrationAbcDays() >= 0 && config.hasSensorS8) {
int newHour = config.getCO2CalibrationAbcDays() * 24;
int curHour = ag->s8.getAbcPeriod();
if (curHour != newHour) {
String resultStr = "failure";
if (ag->s8.setAbcPeriod(config.getCO2CalibrationAbcDays() * 24)) {
resultStr = "successful";
}
String fromStr = String(curHour / 24) + " days";
if (curHour == 0) {
fromStr = "off";
}
String toStr = String(config.getCO2CalibrationAbcDays()) + " days";
if (config.getCO2CalibrationAbcDays() == 0) {
toStr = "off";
}
String msg =
"Setting S8 from " + fromStr + " to " + toStr + " " + resultStr;
logInfo(msg);
}
} else {
logWarning("CO2 S8 not available, set 'abcDays' ignored");
}
}
void StateMachine::ledBarTest(void) {
if (config.isLedBarTestRequested()) {
if (config.getCountry() == "TH") {
uint32_t tstart = millis();
logInfo("Start run LED test for 2 min");
while (1) {
ledBarRunTest();
uint32_t ms = (uint32_t)(millis() - tstart);
if (ms >= (60 * 1000 * 2)) {
logInfo("LED test after 2 min finish");
break;
}
}
} else {
ledBarRunTest();
}
}
}
void StateMachine::ledBarPowerUpTest(void) { ledBarRunTest(); }
void StateMachine::ledBarRunTest(void) {
disp.setText("LED Test", "running", ".....");
runLedTest('r');
ag->ledBar.show();
delay(1000);
runLedTest('g');
ag->ledBar.show();
delay(1000);
runLedTest('b');
ag->ledBar.show();
delay(1000);
runLedTest('w');
ag->ledBar.show();
delay(1000);
runLedTest('n');
ag->ledBar.show();
delay(1000);
}
void StateMachine::runLedTest(char color) {
int r = 0;
int g = 0;
int b = 0;
switch (color) {
case 'g':
g = 255;
break;
case 'y':
r = 255;
g = 255;
break;
case 'o':
r = 255;
g = 128;
break;
case 'r':
r = 255;
break;
case 'b':
b = 255;
break;
case 'w':
r = 255;
g = 255;
b = 255;
break;
case 'p':
r = 153;
b = 153;
break;
case 'z':
r = 102;
break;
case 'n':
default:
break;
}
ag->ledBar.setColor(r, g, b);
}
/**
* @brief Construct a new Ag State Machine:: Ag State Machine object
*
* @param disp OledDisplay
* @param log Serial Stream
* @param value Measurements
* @param config Configuration
*/
StateMachine::StateMachine(OledDisplay &disp, Stream &log, Measurements &value,
Configuration &config)
: PrintLog(log, "StateMachine"), disp(disp), value(value), config(config) {}
StateMachine::~StateMachine() {}
/**
* @brief OLED display show content from state value
*
* @param state
*/
void StateMachine::displayHandle(AgStateMachineState state) {
// Ignore handle if not support display
if (!(ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic())) {
if (state == AgStateMachineCo2Calibration) {
co2Calibration();
}
return;
}
if (state > AgStateMachineNormal) {
logError("displayHandle: State invalid");
return;
}
dispState = state;
switch (state) {
case AgStateMachineWiFiManagerMode:
case AgStateMachineWiFiManagerPortalActive: {
if (wifiConnectCountDown >= 0) {
if (ag->isBasic()) {
String ssid = "\"airgradient-" + ag->deviceId() + "\" " +
String(wifiConnectCountDown) + String("s");
disp.setText("Connect tohotspot:", ssid.c_str(), "");
} else {
String line1 = String(wifiConnectCountDown) + "s to connect";
String line2 = "to WiFi hotspot:";
String line3 = "\"airgradient-";
String line4 = ag->deviceId() + "\"";
disp.setText(line1, line2, line3, line4);
}
wifiConnectCountDown--;
}
break;
}
case AgStateMachineWiFiManagerStaConnecting: {
disp.setText("Trying to", "connect to WiFi", "...");
break;
}
case AgStateMachineWiFiManagerStaConnected: {
disp.setText("WiFi connection", "successful", "");
break;
}
case AgStateMachineWiFiOkServerConnecting: {
if (ag->isBasic()) {
disp.setText("Connecting", "to", "Server...");
} else {
disp.setText("Connecting to", "Server", "...");
}
break;
}
case AgStateMachineWiFiOkServerConnected: {
disp.setText("Server", "connection", "successful");
break;
}
case AgStateMachineWiFiManagerConnectFailed: {
disp.setText("WiFi not", "connected", "");
break;
}
case AgStateMachineWiFiOkServerConnectFailed: {
// displayShowText("Server not", "reachable", "");
break;
}
case AgStateMachineWiFiOkServerOkSensorConfigFailed: {
if (ag->isBasic()) {
disp.setText("Monitor", "not on", "dashboard");
} else {
disp.setText("Monitor not", "setup on", "dashboard");
}
break;
}
case AgStateMachineWiFiLost: {
disp.showDashboard("WiFi N/A");
break;
}
case AgStateMachineServerLost: {
disp.showDashboard("Server N/A");
break;
}
case AgStateMachineSensorConfigFailed: {
if (addToDashBoard) {
uint32_t ms = (uint32_t)(millis() - addToDashboardTime);
if (ms >= 5000) {
addToDashboardTime = millis();
if (addToDashBoardToggle) {
disp.showDashboard("Add to Dashboard");
} else {
disp.showDashboard(ag->deviceId().c_str());
}
addToDashBoardToggle = !addToDashBoardToggle;
}
} else {
disp.showDashboard("");
}
break;
}
case AgStateMachineNormal: {
disp.showDashboard();
break;
}
case AgStateMachineCo2Calibration:
co2Calibration();
break;
default:
break;
}
}
/**
* @brief OLED display show content as previous state updated
*
*/
void StateMachine::displayHandle(void) { displayHandle(dispState); }
/**
* @brief Update status add to dashboard
*
*/
void StateMachine::displaySetAddToDashBoard(void) {
if (addToDashBoard == false) {
addToDashboardTime = 0;
addToDashBoardToggle = true;
}
addToDashBoard = true;
}
void StateMachine::displayClearAddToDashBoard(void) { addToDashBoard = false; }
/**
* @brief Set WiFi connection coundown on dashboard
*
* @param count Seconds
*/
void StateMachine::displayWiFiConnectCountDown(int count) {
wifiConnectCountDown = count;
}
/**
* @brief Init before start LED bar animation
*
*/
void StateMachine::ledAnimationInit(void) { ledBarAnimationCount = -1; }
/**
* @brief Handle LED from state, only handle LED if board type is: One Indoor or
* Open Air
*
* @param state
*/
void StateMachine::handleLeds(AgStateMachineState state) {
/** Ignore if board type if not ONE_INDOOR or OPEN_AIR_OUTDOOR */
if ((ag->getBoardType() != BoardType::ONE_INDOOR) &&
(ag->getBoardType() != BoardType::OPEN_AIR_OUTDOOR)) {
return;
}
if (state > AgStateMachineNormal) {
logError("ledHandle: state invalid");
return;
}
ledState = state;
if (ag->isOne()) {
ag->ledBar.clear(); // Set all LED OFF
}
switch (state) {
case AgStateMachineWiFiManagerMode: {
/** In WiFi Manager Mode */
/** Turn LED OFF */
/** Turn middle LED Color */
if (ag->isOne()) {
ag->ledBar.setColor(0, 0, 255, ag->ledBar.getNumberOfLeds() / 2);
} else {
ag->statusLed.setToggle();
}
break;
}
case AgStateMachineWiFiManagerPortalActive: {
/** WiFi Manager has connected to mobile phone */
if (ag->isOne()) {
ag->ledBar.setColor(0, 0, 255);
} else {
ag->statusLed.setOn();
}
break;
}
case AgStateMachineWiFiManagerStaConnecting: {
/** after SSID and PW entered and OK clicked, connection to WiFI network is
* attempted */
if (ag->isOne()) {
ledBarSingleLedAnimation(255, 255, 255);
} else {
ag->statusLed.setOff();
}
break;
}
case AgStateMachineWiFiManagerStaConnected: {
/** Connecting to WiFi worked */
if (ag->isOne()) {
ag->ledBar.setColor(255, 255, 255);
} else {
ag->statusLed.setOff();
}
break;
}
case AgStateMachineWiFiOkServerConnecting: {
/** once connected to WiFi an attempt to reach the server is performed */
if (ag->isOne()) {
ledBarSingleLedAnimation(0, 255, 0);
} else {
ag->statusLed.setOff();
}
break;
}
case AgStateMachineWiFiOkServerConnected: {
/** Server is reachable, all fine */
if (ag->isOne()) {
ag->ledBar.setColor(0, 255, 0);
} else {
ag->statusLed.setOff();
/** two time slow blink, then off */
for (int i = 0; i < 2; i++) {
ledStatusBlinkDelay(LED_SLOW_BLINK_DELAY);
}
ag->statusLed.setOff();
}
break;
}
case AgStateMachineWiFiManagerConnectFailed: {
/** Cannot connect to WiFi (e.g. wrong password, WPA Enterprise etc.) */
if (ag->isOne()) {
ag->ledBar.setColor(255, 0, 0);
} else {
ag->statusLed.setOff();
for (int j = 0; j < 3; j++) {
for (int i = 0; i < 3; i++) {
ledStatusBlinkDelay(LED_FAST_BLINK_DELAY);
}
delay(2000);
}
ag->statusLed.setOff();
}
break;
}
case AgStateMachineWiFiOkServerConnectFailed: {
/** Connected to WiFi but server not reachable, e.g. firewall block/
* whitelisting needed etc. */
if (ag->isOne()) {
ag->ledBar.setColor(233, 183, 54); /** orange */
} else {
ag->statusLed.setOff();
for (int j = 0; j < 3; j++) {
for (int i = 0; i < 4; i++) {
ledStatusBlinkDelay(LED_FAST_BLINK_DELAY);
}
delay(2000);
}
ag->statusLed.setOff();
}
break;
}
case AgStateMachineWiFiOkServerOkSensorConfigFailed: {
/** Server reachable but sensor not configured correctly */
if (ag->isOne()) {
ag->ledBar.setColor(139, 24, 248); /** violet */
} else {
ag->statusLed.setOff();
for (int j = 0; j < 3; j++) {
for (int i = 0; i < 5; i++) {
ledStatusBlinkDelay(LED_FAST_BLINK_DELAY);
}
delay(2000);
}
ag->statusLed.setOff();
}
break;
}
case AgStateMachineWiFiLost: {
/** Connection to WiFi network failed credentials incorrect encryption not
* supported etc. */
if (ag->isOne()) {
/** WIFI failed status LED color */
ag->ledBar.setColor(255, 0, 0, 0);
/** Show CO2 or PM color status */
// sensorLedColorHandler();
sensorhandleLeds();
} else {
ag->statusLed.setOff();
}
break;
}
case AgStateMachineServerLost: {
/** Connected to WiFi network but the server cannot be reached through the
* internet, e.g. blocked by firewall */
if (ag->isOne()) {
ag->ledBar.setColor(233, 183, 54, 0);
/** Show CO2 or PM color status */
sensorhandleLeds();
// sensorLedColorHandler();
} else {
ag->statusLed.setOff();
}
break;
}
case AgStateMachineSensorConfigFailed: {
/** Server is reachable but there is some configuration issue to be fixed on
* the server side */
if (ag->isOne()) {
ag->ledBar.setColor(139, 24, 248, 0);
/** Show CO2 or PM color status */
sensorhandleLeds();
} else {
ag->statusLed.setOff();
}
break;
}
case AgStateMachineNormal: {
if (ag->isOne()) {
sensorhandleLeds();
} else {
ag->statusLed.setOff();
}
break;
}
case AgStateMachineLedBarTest:
ledBarTest();
break;
case AgStateMachineLedBarPowerUpTest:
ledBarPowerUpTest();
default:
break;
}
// Show LED bar color
if (ag->isOne()) {
ag->ledBar.show();
}
}
/**
* @brief Handle LED as previous state updated
*
*/
void StateMachine::handleLeds(void) { handleLeds(ledState); }
/**
* @brief Set display state
*
* @param state
*/
void StateMachine::setDisplayState(AgStateMachineState state) {
dispState = state;
}
/**
* @brief Get current display state
*
* @return AgStateMachineState
*/
AgStateMachineState StateMachine::getDisplayState(void) { return dispState; }
/**
* @brief Set AirGradient instance
*
* @param ag Point to AirGradient instance
*/
void StateMachine::setAirGradient(AirGradient *ag) { this->ag = ag; }
/**
* @brief Get current LED state
*
* @return AgStateMachineState
*/
AgStateMachineState StateMachine::getLedState(void) { return ledState; }
void StateMachine::executeCo2Calibration(void) {
displayHandle(AgStateMachineCo2Calibration);
}
void StateMachine::executeLedBarTest(void) {
handleLeds(AgStateMachineLedBarTest);
}
void StateMachine::executeLedBarPowerUpTest(void) {
handleLeds(AgStateMachineLedBarPowerUpTest);
}

57
src/AgStateMachine.h Normal file
View File

@ -0,0 +1,57 @@
#ifndef _AG_STATE_MACHINE_H_
#define _AG_STATE_MACHINE_H_
#include "AgOledDisplay.h"
#include "AgValue.h"
#include "AgConfigure.h"
#include "Main/PrintLog.h"
#include "App/AppDef.h"
class StateMachine : public PrintLog {
private:
// AgStateMachineState state;
AgStateMachineState ledState;
AgStateMachineState dispState;
AirGradient *ag;
OledDisplay &disp;
Measurements &value;
Configuration &config;
bool addToDashBoard = false;
bool addToDashBoardToggle = false;
uint32_t addToDashboardTime;
int wifiConnectCountDown;
int ledBarAnimationCount;
void ledBarSingleLedAnimation(uint8_t r, uint8_t g, uint8_t b);
void ledStatusBlinkDelay(uint32_t delay);
void sensorhandleLeds(void);
void co2handleLeds(void);
void pm25handleLeds(void);
void co2Calibration(void);
void ledBarTest(void);
void ledBarPowerUpTest(void);
void ledBarRunTest(void);
void runLedTest(char color);
public:
StateMachine(OledDisplay &disp, Stream &log,
Measurements &value, Configuration& config);
~StateMachine();
void setAirGradient(AirGradient* ag);
void displayHandle(AgStateMachineState state);
void displayHandle(void);
void displaySetAddToDashBoard(void);
void displayClearAddToDashBoard(void);
void displayWiFiConnectCountDown(int count);
void ledAnimationInit(void);
void handleLeds(AgStateMachineState state);
void handleLeds(void);
void setDisplayState(AgStateMachineState state);
AgStateMachineState getDisplayState(void);
AgStateMachineState getLedState(void);
void executeCo2Calibration(void);
void executeLedBarTest(void);
void executeLedBarPowerUpTest(void);
};
#endif /** _AG_STATE_MACHINE_H_ */

349
src/AgValue.cpp Normal file
View File

@ -0,0 +1,349 @@
#include "AgValue.h"
#include "AgConfigure.h"
#include "AirGradient.h"
#include "Main/utils.h"
#include "Libraries/Arduino_JSON/src/Arduino_JSON.h"
String Measurements::toString(bool localServer, AgFirmwareMode fwMode, int rssi,
void *_ag, void *_config) {
AirGradient *ag = (AirGradient *)_ag;
Configuration *config = (Configuration *)_config;
JSONVar root;
root["wifi"] = rssi;
if (localServer) {
root["serialno"] = ag->deviceId();
}
if (config->hasSensorS8 && utils::isValidCO2(this->CO2)) {
root["rco2"] = this->CO2;
}
if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic()) {
if (config->hasSensorPMS1) {
if (utils::isValidPMS(this->pm01_1)) {
root["pm01"] = this->pm01_1;
}
if (utils::isValidPMS(this->pm25_1)) {
root["pm02"] = this->pm25_1;
}
if (utils::isValidPMS(this->pm10_1)) {
root["pm10"] = this->pm10_1;
}
if (utils::isValidPMS03Count(this->pm03PCount_1)) {
root["pm003Count"] = this->pm03PCount_1;
}
}
if (config->hasSensorSHT) {
if (utils::isValidTemperature(this->Temperature)) {
root["atmp"] = ag->round2(this->Temperature);
if (localServer) {
root["atmpCompensated"] = ag->round2(this->Temperature);
}
}
if (utils::isValidHumidity(this->Humidity)) {
root["rhum"] = this->Humidity;
if (localServer) {
root["rhumCompensated"] = this->Humidity;
}
}
}
if (config->hasSensorSHT && config->hasSensorPMS1) {
int pm25 = ag->pms5003.compensated(this->pm25_1, this->Humidity);
if (pm25 >= 0) {
root["pm02Compensated"] = pm25;
}
}
} else {
if (config->hasSensorPMS1 && config->hasSensorPMS2) {
if (utils::isValidPMS(this->pm01_1) && utils::isValidPMS(this->pm01_2)) {
root["pm01"] = ag->round2((this->pm01_1 + this->pm01_2) / 2.0f);
}
if (utils::isValidPMS(this->pm25_1) && utils::isValidPMS(this->pm25_2)) {
root["pm02"] = ag->round2((this->pm25_1 + this->pm25_2) / 2.0f);
}
if (utils::isValidPMS(this->pm10_1) && utils::isValidPMS(this->pm10_2)) {
root["pm10"] = ag->round2((this->pm10_1 + this->pm10_2) / 2.0f);
}
if (utils::isValidPMS(this->pm03PCount_1) && utils::isValidPMS(this->pm03PCount_2)) {
root["pm003Count"] = ag->round2((this->pm03PCount_1 + this->pm03PCount_2) / 2.0f);
}
float val;
if (utils::isValidTemperature(this->temp_1) && utils::isValidTemperature(this->temp_1)) {
root["atmp"] = ag->round2((this->temp_1 + this->temp_2) / 2.0f);
if (localServer) {
val = ag->pms5003t_2.temperatureCompensated((this->temp_1 + this->temp_2) / 2.0f);
if (utils::isValidTemperature(val)) {
root["atmpCompensated"] = ag->round2(val);
}
}
}
if (utils::isValidHumidity(this->hum_1) && utils::isValidHumidity(this->hum_1)) {
root["rhum"] = ag->round2((this->hum_1 + this->hum_2) / 2.0f);
if (localServer) {
val = ag->pms5003t_2.humidityCompensated((this->hum_1 + this->hum_2) / 2.0f);
if (utils::isValidHumidity(val)) {
root["rhumCompensated"] = (int)val;
}
}
}
int pm25 = (ag->pms5003t_1.compensated(this->pm25_1, this->temp_1) +
ag->pms5003t_2.compensated(this->pm25_2, this->temp_2)) /
2;
root["pm02Compensated"] = pm25;
}
if (fwMode == FW_MODE_O_1PS || fwMode == FW_MODE_O_1PST) {
float val;
if (config->hasSensorPMS1) {
if (utils::isValidPMS(this->pm01_1)) {
root["pm01"] = this->pm01_1;
}
if (utils::isValidPMS(this->pm25_1)) {
root["pm02"] = this->pm25_1;
}
if (utils::isValidPMS(this->pm10_1)) {
root["pm10"] = this->pm10_1;
}
if (utils::isValidPMS03Count(this->pm03PCount_1)) {
root["pm003Count"] = this->pm03PCount_1;
}
if (utils::isValidTemperature(this->temp_1)) {
root["atmp"] = ag->round2(this->temp_1);
if (localServer) {
val = ag->pms5003t_1.temperatureCompensated(this->temp_1);
if (utils::isValidTemperature(val)) {
root["atmpCompensated"] = ag->round2(val);
}
}
}
if (utils::isValidHumidity(this->hum_1)) {
root["rhum"] = this->hum_1;
if (localServer) {
val = ag->pms5003t_1.humidityCompensated(this->hum_1);
if (utils::isValidHumidity(val)) {
root["rhumCompensated"] = (int)val;
}
}
}
root["pm02Compensated"] = ag->pms5003t_1.compensated(this->pm25_1, this->temp_1);
}
if (config->hasSensorPMS2) {
if(utils::isValidPMS(this->pm01_2)) {
root["pm01"] = this->pm01_2;
}
if(utils::isValidPMS(this->pm25_2)) {
root["pm02"] = this->pm25_2;
}
if(utils::isValidPMS(this->pm10_2)) {
root["pm10"] = this->pm10_2;
}
if(utils::isValidPMS03Count(this->pm03PCount_2)) {
root["pm003Count"] = this->pm03PCount_2;
}
float val;
if (utils::isValidTemperature(this->temp_2)) {
root["atmp"] = ag->round2(this->temp_2);
if (localServer) {
val = ag->pms5003t_2.temperatureCompensated(this->temp_2);
if (utils::isValidTemperature(val)) {
root["atmpCompensated"] = ag->round2(val);
}
}
}
if(utils::isValidHumidity(this->hum_2)) {
root["rhum"] = this->hum_2;
if (localServer) {
val = ag->pms5003t_2.humidityCompensated(this->hum_2);
if (utils::isValidHumidity(val)) {
root["rhumCompensated"] = (int)val;
}
}
}
root["pm02Compensated"] = ag->pms5003t_2.compensated(this->pm25_2, this->temp_2);
}
} else {
if (fwMode == FW_MODE_O_1P) {
float val;
if (config->hasSensorPMS1) {
if (utils::isValidPMS(this->pm01_1)) {
root["pm01"] = this->pm01_1;
}
if (utils::isValidPMS(this->pm25_1)) {
root["pm02"] = this->pm25_1;
}
if (utils::isValidPMS(this->pm10_1)) {
root["pm10"] = this->pm10_1;
}
if (utils::isValidPMS03Count(this->pm03PCount_1)) {
root["pm003Count"] = this->pm03PCount_1;
}
if (utils::isValidTemperature(this->temp_1)) {
root["atmp"] = ag->round2(this->temp_1);
if (localServer) {
val = ag->pms5003t_1.temperatureCompensated(this->temp_1);
if (utils::isValidTemperature(val)) {
root["atmpCompensated"] = ag->round2(val);
}
}
}
if (utils::isValidHumidity(this->hum_1)) {
root["rhum"] = this->hum_1;
if(localServer) {
val = ag->pms5003t_1.humidityCompensated(this->hum_1);
if(utils::isValidHumidity(val)) {
root["rhumCompensated"] = (int)val;
}
}
}
root["pm02Compensated"] = ag->pms5003t_1.compensated(this->pm25_1, this->temp_1);
} else if (config->hasSensorPMS2) {
if(utils::isValidPMS(this->pm01_2)) {
root["pm01"] = this->pm01_2;
}
if(utils::isValidPMS(this->pm25_2)) {
root["pm02"] = this->pm25_2;
}
if(utils::isValidPMS(this->pm10_2)) {
root["pm10"] = this->pm10_2;
}
if(utils::isValidPMS03Count(this->pm03PCount_2)) {
root["pm003Count"] = this->pm03PCount_2;
}
if (utils::isValidTemperature(this->temp_2)) {
root["atmp"] = ag->round2(this->temp_2);
if (localServer) {
val = ag->pms5003t_1.temperatureCompensated(this->temp_2);
if (utils::isValidTemperature(val)) {
root["atmpCompensated"] = ag->round2(val);
}
}
}
if (utils::isValidHumidity(this->hum_2)) {
root["rhum"] = this->hum_2;
if(localServer) {
val = ag->pms5003t_1.humidityCompensated(this->hum_2);
if(utils::isValidHumidity(val)) {
root["rhumCompensated"] = (int)val;
}
}
}
root["pm02Compensated"] = ag->pms5003t_1.compensated(this->pm25_1, this->temp_1);
}
} else {
float val;
if (config->hasSensorPMS1) {
if(utils::isValidPMS(this->pm01_1)) {
root["channels"]["1"]["pm01"] = this->pm01_1;
}
if(utils::isValidPMS(this->pm25_1)) {
root["channels"]["1"]["pm02"] = this->pm25_1;
}
if(utils::isValidPMS(this->pm10_1)) {
root["channels"]["1"]["pm10"] = this->pm10_1;
}
if (utils::isValidPMS03Count(this->pm03PCount_1)) {
root["channels"]["1"]["pm003Count"] = this->pm03PCount_1;
}
if(utils::isValidTemperature(this->temp_1)) {
root["channels"]["1"]["atmp"] = ag->round2(this->temp_1);
if (localServer) {
val = ag->pms5003t_1.temperatureCompensated(this->temp_1);
if (utils::isValidTemperature(val)) {
root["channels"]["1"]["atmpCompensated"] = ag->round2(val);
}
}
}
if (utils::isValidHumidity(this->hum_1)) {
root["channels"]["1"]["rhum"] = this->hum_1;
if (localServer) {
val = ag->pms5003t_1.humidityCompensated(this->hum_1);
if (utils::isValidHumidity(val)) {
root["channels"]["1"]["rhumCompensated"] = (int)val;
}
}
}
root["channels"]["1"]["pm02Compensated"] = ag->pms5003t_1.compensated(this->pm25_1, this->temp_1);
}
if (config->hasSensorPMS2) {
float val;
if (utils::isValidPMS(this->pm01_2)) {
root["channels"]["2"]["pm01"] = this->pm01_2;
}
if (utils::isValidPMS(this->pm25_2)) {
root["channels"]["2"]["pm02"] = this->pm25_2;
}
if (utils::isValidPMS(this->pm10_2)) {
root["channels"]["2"]["pm10"] = this->pm10_2;
}
if (utils::isValidPMS03Count(this->pm03PCount_2)) {
root["channels"]["2"]["pm003Count"] = this->pm03PCount_2;
}
if (utils::isValidTemperature(this->temp_2)) {
root["channels"]["2"]["atmp"] = ag->round2(this->temp_2);
if (localServer) {
val = ag->pms5003t_1.temperatureCompensated(this->temp_2);
if (utils::isValidTemperature(val)) {
root["channels"]["2"]["atmpCompensated"] = ag->round2(val);
}
}
}
if (utils::isValidHumidity(this->hum_2)) {
root["channels"]["2"]["rhum"] = this->hum_2;
if (localServer) {
val = ag->pms5003t_1.humidityCompensated(this->hum_2);
if (utils::isValidHumidity(val)) {
root["channels"]["2"]["rhumCompensated"] = (int)val;
}
}
}
root["channels"]["2"]["pm02Compensated"] = ag->pms5003t_2.compensated(this->pm25_2, this->temp_2);
}
}
}
}
if (config->hasSensorSGP) {
if (utils::isValidVOC(this->TVOC)) {
root["tvocIndex"] = this->TVOC;
}
if (utils::isValidVOC(this->TVOCRaw)) {
root["tvocRaw"] = this->TVOCRaw;
}
if (utils::isValidNOx(this->NOx)) {
root["noxIndex"] = this->NOx;
}
if (utils::isValidNOx(this->NOxRaw)) {
root["noxRaw"] = this->NOxRaw;
}
}
root["boot"] = bootCount;
root["bootCount"] = bootCount;
if (localServer) {
if (ag->isOne()) {
root["ledMode"] = config->getLedBarModeName();
}
root["firmware"] = ag->getVersion();
root["model"] = AgFirmwareModeName(fwMode);
}
return JSON.stringify(root);
}

76
src/AgValue.h Normal file
View File

@ -0,0 +1,76 @@
#ifndef _AG_VALUE_H_
#define _AG_VALUE_H_
#include <Arduino.h>
#include "App/AppDef.h"
class Measurements {
private:
public:
Measurements() {
pm25_1 = -1;
pm01_1 = -1;
pm10_1 = -1;
pm03PCount_1 = -1;
temp_1 = -1001;
hum_1 = -1;
pm25_2 = -1;
pm01_2 = -1;
pm10_2 = -1;
pm03PCount_2 = -1;
temp_2 = -1001;
hum_2 = -1;
Temperature = -1001;
Humidity = -1;
CO2 = -1;
TVOC = -1;
TVOCRaw = -1;
NOx = -1;
NOxRaw = -1;
}
~Measurements() {}
float Temperature;
int Humidity;
int CO2;
int TVOC;
int TVOCRaw;
int NOx;
int NOxRaw;
int pm25_1;
int pm01_1;
int pm10_1;
int pm03PCount_1;
float temp_1;
int hum_1;
int pm25_2;
int pm01_2;
int pm10_2;
int pm03PCount_2;
float temp_2;
int hum_2;
int pm1Value01;
int pm1Value25;
int pm1Value10;
int pm1PCount;
int pm1temp;
int pm1hum;
int pm2Value01;
int pm2Value25;
int pm2Value10;
int pm2PCount;
int pm2temp;
int pm2hum;
int countPosition;
const int targetCount = 20;
int bootCount;
String toString(bool isLocal, AgFirmwareMode fwMode, int rssi, void* _ag, void* _config);
};
#endif /** _AG_VALUE_H_ */

385
src/AgWiFiConnector.cpp Normal file
View File

@ -0,0 +1,385 @@
#include "AgWiFiConnector.h"
#include "Libraries/WiFiManager/WiFiManager.h"
#define WIFI_CONNECT_COUNTDOWN_MAX 180
#define WIFI_HOTSPOT_PASSWORD_DEFAULT "cleanair"
#define WIFI() ((WiFiManager *)(this->wifi))
/**
* @brief Set reference AirGradient instance
*
* @param ag Point to AirGradient instance
*/
void WifiConnector::setAirGradient(AirGradient *ag) { this->ag = ag; }
/**
* @brief Construct a new Ag Wi Fi Connector:: Ag Wi Fi Connector object
*
* @param disp OledDisplay
* @param log Stream
* @param sm StateMachine
*/
WifiConnector::WifiConnector(OledDisplay &disp, Stream &log, StateMachine &sm,
Configuration &config)
: PrintLog(log, "WifiConnector"), disp(disp), sm(sm), config(config) {}
WifiConnector::~WifiConnector() {}
/**
* @brief Connection to WIFI AP process. Just call one times
*
* @return true Success
* @return false Failure
*/
bool WifiConnector::connect(void) {
if (wifi == NULL) {
wifi = new WiFiManager();
if (wifi == NULL) {
logError("Create 'WiFiManger' failed");
return false;
}
}
WIFI()->setConfigPortalBlocking(false);
WIFI()->setConnectTimeout(15);
WIFI()->setTimeout(WIFI_CONNECT_COUNTDOWN_MAX);
WIFI()->setAPCallback([this](WiFiManager *obj) { _wifiApCallback(); });
WIFI()->setSaveConfigCallback([this]() { _wifiSaveConfig(); });
WIFI()->setSaveParamsCallback([this]() { _wifiSaveParamCallback(); });
WIFI()->setConfigPortalTimeoutCallback([this]() {_wifiTimeoutCallback();});
if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic()) {
disp.setText("Connect to", "WiFi", "...");
} else {
logInfo("Connecting to WiFi...");
}
ssid = "airgradient-" + ag->deviceId();
// ssid = "AG-" + String(ESP.getChipId(), HEX);
WIFI()->setConfigPortalTimeout(WIFI_CONNECT_COUNTDOWN_MAX);
WiFiManagerParameter postToAg("chbPostToAg",
"Prevent Connection to AirGradient Server", "T",
2, "type=\"checkbox\" ", WFM_LABEL_AFTER);
WIFI()->addParameter(&postToAg);
WiFiManagerParameter postToAgInfo(
"<p>Prevent connection to the AirGradient Server. Important: Only enable "
"it if you are sure you don't want to use any AirGradient cloud "
"features. As a result you will not receive automatic firmware updates "
"and your data will not reach the AirGradient dashboard.</p>");
WIFI()->addParameter(&postToAgInfo);
WIFI()->autoConnect(ssid.c_str(), WIFI_HOTSPOT_PASSWORD_DEFAULT);
logInfo("Wait for configure portal");
#ifdef ESP32
// Task handle WiFi connection.
xTaskCreate(
[](void *obj) {
WifiConnector *connector = (WifiConnector *)obj;
while (connector->_wifiConfigPortalActive()) {
connector->_wifiProcess();
vTaskDelay(1);
}
vTaskDelete(NULL);
},
"wifi_cfg", 4096, this, 10, NULL);
/** Wait for WiFi connect and show LED, display status */
uint32_t dispPeriod = millis();
uint32_t ledPeriod = millis();
bool clientConnectChanged = false;
AgStateMachineState stateOld = sm.getDisplayState();
while (WIFI()->getConfigPortalActive()) {
/** LED animatoin and display update content */
if (WiFi.isConnected() == false) {
/** Display countdown */
uint32_t ms;
if (ag->isOne()) {
ms = (uint32_t)(millis() - dispPeriod);
if (ms >= 1000) {
dispPeriod = millis();
sm.displayHandle();
} else {
if (stateOld != sm.getDisplayState()) {
stateOld = sm.getDisplayState();
sm.displayHandle();
}
}
}
/** LED animations */
ms = (uint32_t)(millis() - ledPeriod);
if (ms >= 100) {
ledPeriod = millis();
sm.handleLeds();
}
/** Check for client connect to change led color */
bool clientConnected = wifiClientConnected();
if (clientConnected != clientConnectChanged) {
clientConnectChanged = clientConnected;
if (clientConnectChanged) {
sm.handleLeds(AgStateMachineWiFiManagerPortalActive);
} else {
sm.ledAnimationInit();
sm.handleLeds(AgStateMachineWiFiManagerMode);
if (ag->isOne()) {
sm.displayHandle(AgStateMachineWiFiManagerMode);
}
}
}
}
delay(1); // avoid watchdog timer reset.
}
#else
_wifiProcess();
#endif
/** Show display wifi connect result failed */
if (WiFi.isConnected() == false) {
sm.handleLeds(AgStateMachineWiFiManagerConnectFailed);
if (ag->isOne() || ag->isPro4_2() || ag->isPro3_3() || ag->isBasic()) {
sm.displayHandle(AgStateMachineWiFiManagerConnectFailed);
}
delay(6000);
} else {
hasConfig = true;
logInfo("WiFi Connected: " + WiFi.SSID() + " IP: " + localIpStr());
if (hasPortalConfig) {
String result = String(postToAg.getValue());
logInfo("Setting postToAirGradient set from " +
String(config.isPostDataToAirGradient() ? "True" : "False") +
String(" to ") + String(result != "T" ? "True" : "False") +
String(" successful"));
config.setPostToAirGradient(result != "T");
}
hasPortalConfig = false;
}
return true;
}
/**
* @brief Disconnect to current connected WiFi AP
*
*/
void WifiConnector::disconnect(void) {
if (WiFi.isConnected()) {
logInfo("Disconnect");
WiFi.disconnect();
}
}
/**
* @brief Has wifi STA connected to WIFI softAP (this device)
*
* @return true Connected
* @return false Not connected
*/
bool WifiConnector::wifiClientConnected(void) {
return WiFi.softAPgetStationNum() ? true : false;
}
/**
* @brief Handle WiFiManage softAP setup completed callback
*
*/
void WifiConnector::_wifiApCallback(void) {
sm.displayWiFiConnectCountDown(WIFI_CONNECT_COUNTDOWN_MAX);
sm.setDisplayState(AgStateMachineWiFiManagerMode);
sm.ledAnimationInit();
sm.handleLeds(AgStateMachineWiFiManagerMode);
}
/**
* @brief Handle WiFiManager save configuration callback
*
*/
void WifiConnector::_wifiSaveConfig(void) {
sm.setDisplayState(AgStateMachineWiFiManagerStaConnected);
sm.handleLeds(AgStateMachineWiFiManagerStaConnected);
}
/**
* @brief Handle WiFiManager save parameter callback
*
*/
void WifiConnector::_wifiSaveParamCallback(void) {
sm.ledAnimationInit();
sm.handleLeds(AgStateMachineWiFiManagerStaConnecting);
sm.setDisplayState(AgStateMachineWiFiManagerStaConnecting);
hasPortalConfig = true;
}
/**
* @brief Check that WiFiManager Configure portal active
*
* @return true Active
* @return false Not-Active
*/
bool WifiConnector::_wifiConfigPortalActive(void) {
return WIFI()->getConfigPortalActive();
}
void WifiConnector::_wifiTimeoutCallback(void) { connectorTimeout = true; }
/**
* @brief Process WiFiManager connection
*
*/
void WifiConnector::_wifiProcess() {
#ifdef ESP32
WIFI()->process();
#else
/** Wait for WiFi connect and show LED, display status */
uint32_t dispPeriod = millis();
uint32_t ledPeriod = millis();
bool clientConnectChanged = false;
AgStateMachineState stateOld = sm.getDisplayState();
while (WIFI()->getConfigPortalActive()) {
WIFI()->process();
if (WiFi.isConnected() == false) {
/** Display countdown */
uint32_t ms;
if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic()) {
ms = (uint32_t)(millis() - dispPeriod);
if (ms >= 1000) {
dispPeriod = millis();
sm.displayHandle();
logInfo("displayHandle state: " + String(sm.getDisplayState()));
} else {
if (stateOld != sm.getDisplayState()) {
stateOld = sm.getDisplayState();
sm.displayHandle();
}
}
}
/** LED animations */
ms = (uint32_t)(millis() - ledPeriod);
if (ms >= 100) {
ledPeriod = millis();
sm.handleLeds();
}
/** Check for client connect to change led color */
bool clientConnected = wifiClientConnected();
if (clientConnected != clientConnectChanged) {
clientConnectChanged = clientConnected;
if (clientConnectChanged) {
sm.handleLeds(AgStateMachineWiFiManagerPortalActive);
} else {
sm.ledAnimationInit();
sm.handleLeds(AgStateMachineWiFiManagerMode);
if (ag->isOne()) {
sm.displayHandle(AgStateMachineWiFiManagerMode);
}
}
}
}
delay(1);
}
// TODO This is for basic
if (ag->getBoardType() == DIY_BASIC) {
if (!WiFi.isConnected()) {
// disp.setText("Booting", "offline", "mode");
Serial.println("failed to connect and hit timeout");
delay(2500);
} else {
hasConfig = true;
}
}
#endif
}
/**
* @brief Handle and reconnect WiFi
*
*/
void WifiConnector::handle(void) {
// Ignore if WiFi is not configured
if (hasConfig == false) {
return;
}
if (WiFi.isConnected()) {
lastRetry = millis();
return;
}
/** Retry connect WiFi each 10sec */
uint32_t ms = (uint32_t)(millis() - lastRetry);
if (ms >= 10000) {
lastRetry = millis();
WiFi.reconnect();
logInfo("Re-Connect WiFi");
}
}
/**
* @brief Is WiFi connected
*
* @return true Connected
* @return false Disconnected
*/
bool WifiConnector::isConnected(void) { return WiFi.isConnected(); }
/**
* @brief Reset WiFi configuretion and connection, disconnect wifi before call
* this method
*
*/
void WifiConnector::reset(void) {
if(this->wifi == NULL) {
this->wifi = new WiFiManager();
if(this->wifi == NULL){
logInfo("reset failed");
return;
}
}
WIFI()->resetSettings();
}
/**
* @brief Get wifi RSSI
*
* @return int
*/
int WifiConnector::RSSI(void) { return WiFi.RSSI(); }
/**
* @brief Get WIFI IP as string format ex: 192.168.1.1
*
* @return String
*/
String WifiConnector::localIpStr(void) { return WiFi.localIP().toString(); }
/**
* @brief Get status that wifi has configurated
*
* @return true Configurated
* @return false Not Configurated
*/
bool WifiConnector::hasConfigurated(void) {
if (WiFi.SSID().isEmpty()) {
return false;
}
return true;
}
/**
* @brief Get WiFi connection porttal timeout.
*
* @return true
* @return false
*/
bool WifiConnector::isConfigurePorttalTimeout(void) { return connectorTimeout; }

51
src/AgWiFiConnector.h Normal file
View File

@ -0,0 +1,51 @@
#ifndef _AG_WIFI_CONNECTOR_H_
#define _AG_WIFI_CONNECTOR_H_
#include "AgOledDisplay.h"
#include "AgStateMachine.h"
#include "AirGradient.h"
#include "AgConfigure.h"
#include "Main/PrintLog.h"
#include <Arduino.h>
class WifiConnector : public PrintLog {
private:
AirGradient *ag;
OledDisplay &disp;
StateMachine &sm;
Configuration &config;
String ssid;
void *wifi = NULL;
bool hasConfig;
uint32_t lastRetry;
bool hasPortalConfig = false;
bool connectorTimeout = false;
bool wifiClientConnected(void);
public:
void setAirGradient(AirGradient *ag);
WifiConnector(OledDisplay &disp, Stream &log, StateMachine &sm, Configuration& config);
~WifiConnector();
bool connect(void);
void disconnect(void);
void handle(void);
void _wifiApCallback(void);
void _wifiSaveConfig(void);
void _wifiSaveParamCallback(void);
bool _wifiConfigPortalActive(void);
void _wifiTimeoutCallback(void);
void _wifiProcess();
bool isConnected(void);
void reset(void);
int RSSI(void);
String localIpStr(void);
bool hasConfigurated(void);
bool isConfigurePorttalTimeout(void);
};
#endif /** _AG_WIFI_CONNECTOR_H_ */

View File

@ -1,6 +1,9 @@
#include "AirGradient.h"
#define AG_LIB_VER "3.0.3"
#ifdef ESP8266
#include <ESP8266WiFi.h>
#else
#include "WiFi.h"
#endif
AirGradient::AirGradient(BoardType type)
: pms5003(type), pms5003t_1(type), pms5003t_2(type), s8(type), sgp41(type),
@ -33,6 +36,48 @@ int AirGradient::getI2cSclPin(void) {
return bsp->I2C.scl_pin;
}
String AirGradient::getVersion(void) { return AG_LIB_VER; }
String AirGradient::getVersion(void) { return GIT_VERSION; }
BoardType AirGradient::getBoardType(void) { return boardType; }
double AirGradient::round2(double value) {
double ret;
if (value >= 0) {
ret = (int)(value * 100 + 0.5f);
} else {
ret = (int)(value * 100 - 0.5f);
}
return ret / 100;
}
String AirGradient::getBoardName(void) {
return String(getBoardDefName(boardType));
}
/**
* @brief Board Type is ONE_INDOOR
*
* @return true ONE_INDOOR
* @return false Other
*/
bool AirGradient::isOne(void) {
return boardType == BoardType::ONE_INDOOR;
}
bool AirGradient::isPro4_2(void) {
return boardType == BoardType::DIY_PRO_INDOOR_V4_2;
}
bool AirGradient::isPro3_3(void) {
return boardType == BoardType::DIY_PRO_INDOOR_V3_3;
}
bool AirGradient::isBasic(void) { return boardType == BoardType::DIY_BASIC; }
String AirGradient::deviceId(void) {
String mac = WiFi.macAddress();
mac.replace(":", "");
mac.toLowerCase();
return mac;
}

View File

@ -1,17 +1,22 @@
#ifndef _AIR_GRADIENT_H_
#define _AIR_GRADIENT_H_
#include "display/oled.h"
#include "main/BoardDef.h"
#include "main/HardwareWatchdog.h"
#include "main/LedBar.h"
#include "main/PushButton.h"
#include "main/StatusLed.h"
#include "pms/pms5003.h"
#include "pms/pms5003t.h"
#include "s8/s8.h"
#include "sgp41/sgp41.h"
#include "sht/sht.h"
#include "Display/Display.h"
#include "Main/BoardDef.h"
#include "Main/HardwareWatchdog.h"
#include "Main/LedBar.h"
#include "Main/PushButton.h"
#include "Main/StatusLed.h"
#include "PMS/PMS5003.h"
#include "PMS/PMS5003T.h"
#include "S8/S8.h"
#include "Sgp41/Sgp41.h"
#include "Sht/Sht.h"
#include "Main/utils.h"
#ifndef GIT_VERSION
#define GIT_VERSION "3.1.5-snap"
#endif
/**
* @brief Class with define all the sensor has supported by Airgradient. Each
@ -107,6 +112,59 @@ public:
*/
String getVersion(void);
/**
* @brief Get the Board Name object
*
* @return String
*/
String getBoardName(void);
/**
* @brief Round double value with for 2 decimal
*
* @param valuem Round value
* @return double
*/
double round2(double value);
/**
* @brief Check that Airgradient object is ONE_INDOOR
*
* @return true Yes
* @return false No
*/
bool isOne(void);
/**
* @brief Check that Airgradient object is DIY_PRO 4.2 indoor
*
* @return true Yes
* @return false No
*/
bool isPro4_2(void);
/**
* @brief Check that Airgradient object is DIY_PRO 3.7 indoor
*
* @return true Yes
* @return false No
*/
bool isPro3_3(void);
/**
* @brief Check that Airgradient object is DIY_BASIC
*
* @return true Yes
* @return false No
*/
bool isBasic(void);
/**
* @brief Get device Id
*
* @return String
*/
String deviceId(void);
private:
BoardType boardType;
};

27
src/App/AppDef.cpp Normal file
View File

@ -0,0 +1,27 @@
#include "AppDef.h"
const char *AgFirmwareModeName(AgFirmwareMode mode) {
switch (mode) {
case FW_MODE_I_9PSL:
return "I-9PSL";
case FW_MODE_O_1PP:
return "O-1PP";
case FW_MODE_O_1PPT:
return "O-1PPT";
case FW_MODE_O_1PST:
return "O-1PST";
case FW_MODE_O_1PS:
return "0-1PS";
case FW_MODE_O_1P:
return "O-1P";
case FW_MODE_I_42PS:
return "DIY-PRO-I-4.2PS";
case FW_MODE_I_33PS:
return "DIY-PRO-I-3.3PS";
case FW_MODE_I_BASIC_40PS:
return "DIY-BASIC-I-4.0PS";
default:
break;
}
return "UNKNOWN";
}

110
src/App/AppDef.h Normal file
View File

@ -0,0 +1,110 @@
#ifndef _APP_DEF_H_
#define _APP_DEF_H_
/**
* @brief Application state machine state
*
*/
enum AgStateMachineState {
/** In WiFi Manger Mode */
AgStateMachineWiFiManagerMode,
/** WiFi Manager has connected to mobile phone */
AgStateMachineWiFiManagerPortalActive,
/** After SSID and PW entered and OK clicked, connection to WiFI network is
attempted*/
AgStateMachineWiFiManagerStaConnecting,
/** Connecting to WiFi worked */
AgStateMachineWiFiManagerStaConnected,
/** Once connected to WiFi an attempt to reach the server is performed */
AgStateMachineWiFiOkServerConnecting,
/** Server is reachable, all fine */
AgStateMachineWiFiOkServerConnected,
/** =================================== *
* Exceptions during WIFi Setup *
* =================================== **/
/** Cannot connect to WiFi (e.g. wrong password, WPA Enterprise etc.) */
AgStateMachineWiFiManagerConnectFailed,
/** Connected to WiFi but server not reachable, e.g. firewall
block/whitelisting needed etc. */
AgStateMachineWiFiOkServerConnectFailed,
/** Server reachable but sensor not configured correctly*/
AgStateMachineWiFiOkServerOkSensorConfigFailed,
/** =================================== *
* During Normal Operation *
* =================================== **/
/** Connection to WiFi network failed credentials incorrect encryption not
supported etc. */
AgStateMachineWiFiLost,
/** Connected to WiFi network but the server cannot be reached through the
internet, e.g. blocked by firewall */
AgStateMachineServerLost,
/** Server is reachable but there is some configuration issue to be fixed on
the server side */
AgStateMachineSensorConfigFailed,
/** CO2 calibration */
AgStateMachineCo2Calibration,
/* LED bar testing */
AgStateMachineLedBarTest,
AgStateMachineLedBarPowerUpTest,
/** OTA perform, show display status */
AgStateMachineOtaPerform,
/** LED: Show working state.
* Display: Show dashboard */
AgStateMachineNormal,
};
/**
* @brief RGB LED bar mode for ONE_INDOOR board
*/
enum LedBarMode {
/** Don't use LED bar */
LedBarModeOff,
/** Use LED bar for show PM2.5 value level */
LedBarModePm,
/** Use LED bar for show CO2 value level */
LedBarModeCO2,
};
enum ConfigurationControl {
/** Allow set configuration from local over device HTTP server */
ConfigurationControlLocal,
/** Allow set configuration from Airgradient cloud */
ConfigurationControlCloud,
/** Allow set configuration from Local and Cloud */
ConfigurationControlBoth
};
enum AgFirmwareMode {
FW_MODE_I_9PSL, /** ONE_INDOOR */
FW_MODE_O_1PST, /** PMS5003T, S8 and SGP41 */
FW_MODE_O_1PPT, /** PMS5003T_1, PMS5003T_2, SGP41 */
FW_MODE_O_1PP, /** PMS5003T_1, PMS5003T_2 */
FW_MODE_O_1PS, /** PMS5003T, S8 */
FW_MODE_O_1P, /** PMS5003T */
FW_MODE_I_42PS, /** DIY_PRO 4.2 */
FW_MODE_I_33PS, /** DIY_PRO 3.3 */
FW_MODE_I_BASIC_40PS, /** DIY_BASIC 4.0 */
};
const char *AgFirmwareModeName(AgFirmwareMode mode);
#endif /** _APP_DEF_H_ */

View File

@ -1,6 +1,6 @@
#include "oled.h"
#include "../library/Adafruit_SH110x/Adafruit_SH110X.h"
#include "../library/Adafruit_SSD1306_Wemos_OLED/Adafruit_SSD1306.h"
#include "Display.h"
#include "../Libraries/Adafruit_SH110x/Adafruit_SH110X.h"
#include "../Libraries/Adafruit_SSD1306_Wemos_OLED/Adafruit_SSD1306.h"
#define disp(func) \
if (this->_boardType == DIY_BASIC) { \

View File

@ -1,7 +1,7 @@
#ifndef _AIR_GRADIENT_OLED_H_
#define _AIR_GRADIENT_OLED_H_
#include "../main/BoardDef.h"
#include "../Main/BoardDef.h"
#include <Arduino.h>
#include <Wire.h>

Some files were not shown because too many files have changed in this diff Show More