Compare commits

..

228 Commits
3.0.0 ... 3.1.0

Author SHA1 Message Date
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
5667279cf1 Merge pull request #53 from airgradienthq/feature/update-sht-for-all-examples
Feature/update sht for all examples
2024-02-16 21:57:52 +07:00
2941bb2d5d next version 3.0.3 2024-02-16 21:56:43 +07:00
a0044ad0ac Update example to use sht support for sht3x and sht4x 2024-02-16 13:39:33 +07:00
fc5c0a1d6e Merge branch 'hotfix/sht30' into feature/update-sht-for-all-examples 2024-02-16 13:29:03 +07:00
b28719b7a5 Improved LED test on startup. Set for version 3.0.2 2024-02-16 12:11:17 +07:00
87a3b6e409 Merge commit 'f17afd932ebef7d0d2e49c130e076dfc5462c086' 2024-02-15 20:07:42 +07:00
dd62a10ed5 Update arduino library version 3.0.1 2024-02-15 20:01:02 +07:00
225d079d48 Merge pull request #52 from airgradienthq/feature/led-test-with-button-on-power-up
Feature/led test with button on power up
2024-02-15 19:58:40 +07:00
7ea43fdc7d Update lib version string: 3.0.1 2024-02-15 19:56:46 +07:00
67ad912a71 update DISPLAY_DELAY_SHOW_CONTENT_MS from 3000 to 6000 2024-02-15 19:55:17 +07:00
5602a456a7 fix: wrong config key for PM Standard 2024-02-15 19:48:58 +07:00
28e5aa4e69 LED test update: support country TH and Show display Press now for LED test 2024-02-15 13:40:42 +07:00
e55f3b6e74 LED Test on Button 2024-02-15 11:21:04 +07:00
f17afd932e Auto detect 1PST, 1PPT and 1PP 2024-02-15 10:56:53 +07:00
7a6cc8caef use "arduino-sht" library for sht3x and sht4x 2024-02-10 21:14:27 +07:00
94ead3751b Merge pull request #48 from airgradienthq/feature/Basic_V4-show-full-device-id-on-display
Feature/basic v4 show full device id on display
2024-02-07 21:01:22 +07:00
ab600e014a Update content and line space of display show serial number value 2024-02-07 21:00:13 +07:00
60d02d88b5 Update show device id on 4 line of display 2024-02-07 09:43:52 +07:00
05594441b8 Update sht3x 2024-02-07 09:18:02 +07:00
9e461a9036 Merge pull request #47 from airgradienthq/hotfix/ledbar-test
updated `ledBarTestRequested`
2024-02-06 13:25:14 +07:00
ce6bee19af updated ledBarTestRequested 2024-02-06 13:24:10 +07:00
0354c6e634 Merge pull request #46 from airgradienthq/optimize-and-clean-code
Optimize firmware and bug fix
2024-02-06 10:56:39 +07:00
ac9efccd94 Fix: missing handle serverConfig ledBarTestRequested test 2024-02-06 10:52:01 +07:00
4df0fc5d5c Set S8 Automatic Baseline Period 2024-02-06 10:41:10 +07:00
4c180fedbd Support SHT3x 2024-02-06 09:38:37 +07:00
8b73ac77f9 Update TVOC missing call to polling data 2024-02-04 16:30:50 +07:00
cbb444f1bc Change WiFi connection screen to four lines 2024-02-04 16:20:37 +07:00
b2762a3b6c [Lib] Change LedBar function name getNumberOfLed to getNumberOfLeds 2024-02-04 15:48:06 +07:00
512420f5a2 Update: Explain difference between PMS5003 and PMS5003T 2024-02-04 15:22:06 +07:00
e94a625072 Remove .DS_Store 2024-02-04 15:19:39 +07:00
7bbe81ad1d Fix: TestPM (before PM Simple) only shows nulls 2024-02-04 15:10:46 +07:00
335fad3f0d Clean code and add comments 2024-02-04 15:04:38 +07:00
565 changed files with 529772 additions and 5392 deletions

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

@ -0,0 +1,55 @@
on: [push, pull_request]
jobs:
compile:
strategy:
fail-fast: false
matrix:
example:
- "BASIC"
- "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=default,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: "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 Normal file
View File

@ -0,0 +1,5 @@
*.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)
@ -34,8 +38,9 @@ If you have any questions or problems, check out [our forum](https://forum.airgr
- [Sensirion Gas Index Algorithm](https://github.com/Sensirion/arduino-gas-index-algorithm)
- [Sensirion Core](https://github.com/Sensirion/arduino-core/)
- [Sensirion I2C SGP41](https://github.com/Sensirion/arduino-i2c-sgp41)
- [Sensirion I2C SHT4x](https://github.com/Sensirion/arduino-i2c-sht4x)
- [PMS](https://github.com/fu-hsi/pms)
- [Sensirion I2C SHT](https://github.com/Sensirion/arduino-sht)
- [WiFiManager](https://github.com/tzapu/WiFiManager)
- [Arduino_JSON](https://github.com/arduino-libraries/Arduino_JSON)
## License
CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License

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

@ -0,0 +1,105 @@
## 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:
~~~
{"wifi":-46,
"serialno":"ecda3b1eaaaf",
"rco2":447,
"pm01":3,
"pm02":7,
"pm10":8,
"pm003Count":442,
"atmp":25.87,
"rhum":43,
"tvocIndex":100,
"tvoc_raw":33051,
"noxIndex":1,
"nox_raw":16307,
"boot":6,
"ledMode":"pm",
"firmwareVersion":"3.0.10beta",
"fwMode":"I-9PSL"}
~~~
|Properties|Type|Explanation|
|-|-|-|
|serialno|String| Serial Number of the monitor|
|wifi|Number| WiFi signal strength|
|pm01, pm02, pm10|Number| PM1, PM2.5 and PM10 in ug/m3|
|rco2|Number| CO2 in ppm|
|pm003Count|Number| Particle count per dL|
|atmp|Number| Temperature in Degrees Celcius|
|rhum|Number| Relative Humidity|
|tvocIndex|Number| Senisiron VOC Index|
|tvoc_raw|Number| VOC raw value|
|noxIndex|Number| Senisirion NOx Index|
|nox_raw|Number| NOx raw value|
|boot|Number| Counts every measurement cycle. Low boot counts indicate restarts.|
|ledMode|String| Current configuration of the LED mode|
|firmwareVersion|String| Current firmware version|
|fwMode|String| Current model name|
#### Get Configuration Parameters (GET)
With the path "/config" you can get the current configuration.
~~~
{"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 Celcius
```curl -X PUT -H "Content-Type: application/json" -d '{"temperatureUnit":"c"}' http://airgradient_84fce612eff4.local/config ```
#### Avoiding Conflicts with Configuration on AirGradient Server
If the monitor is setup 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|Type|Accepted Values|Example|
|-|-|-|-|
|country|String| Country code as [ALPHA-2 notation](https://www.iban.com/country-codes) | {"country": "TH"}|
|pmStandard|String|ugm3 : ug/m3 <br> usaqi: USAQI | {"pmStandard": "ugm3"}|
|ledBarMode|String|co2: LED bar displays CO2 <br> pm: LED bar displays PM <br> off: Turn off LED bar | {"ledBarMode": "off"}|
|abcDays|Number|Number of days for CO2 automatic baseline balibration. Maximum 200 days. Default 8 days. | {"abcDays": 8}|
|mqttBrokerUrl|String|MQTT broker URL. | {"mqttBrokerUrl":"mqtt://192.168.0.18:1883"} |
|temperatureUnit|String|c or C: Degree Celsius °C <br>f or F: Degree Fahrenheit °F | {"temperatureUnit": "c"}|
|configurationControl|String|both : Accept local and cloud configuration <br>local : Accept only local configuration <br>cloud : Accept only cloud configuration | {"configurationControl": "both"}|
|postDataToAirGradient|Boolean|Send data to AirGradient cloud: <br>true : Enabled <br>false: Disabled | {"postDataToAirGradient": true}|
|co2CalibrationRequested|Boolean|Trigger CO2 calibration (400ppm) on monitor:<br>true : Calibration will be triggered | {"co2CalibrationRequested": true}|
|ledBarTestRequested|Boolean|Test LED bar:<br> true : LEDs will run test sequence | {"ledBarTestRequested": true}|

BIN
examples/.DS_Store vendored

Binary file not shown.

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

@ -0,0 +1,401 @@
/*
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 <AirGradient.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266WiFi.h>
#include <WiFiClient.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 2000 /** ms */
#define DISPLAY_DELAY_SHOW_CONTENT_MS 2000 /** ms */
#define WIFI_HOTSPOT_PASSWORD_DEFAULT \
"cleanair" /** default WiFi AP password \
*/
/** Create airgradient instance for 'DIY_BASIC' board */
static AirGradient ag = AirGradient(DIY_BASIC);
static Configuration configuration(Serial);
static AgApiClient apiClient(Serial, configuration);
static WifiConnector wifiConnector(Serial);
static int co2Ppm = -1;
static int pm25 = -1;
static float temp = -1001;
static int hum = -1;
static long val;
static void boardInit(void);
static void failedHandler(String msg);
static void executeCo2Calibration(void);
static void updateServerConfiguration(void);
static void co2Update(void);
static void pmUpdate(void);
static void tempHumUpdate(void);
static void sendDataToServer(void);
static void dispHandler(void);
static String getDevId(void);
static void showNr(void);
bool hasSensorS8 = true;
bool hasSensorPMS = true;
bool hasSensorSHT = true;
int pmFailCount = 0;
int getCO2FailCount = 0;
AgSchedule configSchedule(SERVER_CONFIG_UPDATE_INTERVAL,
updateServerConfiguration);
AgSchedule serverSchedule(SERVER_SYNC_INTERVAL, sendDataToServer);
AgSchedule dispSchedule(DISP_UPDATE_INTERVAL, dispHandler);
AgSchedule co2Schedule(SENSOR_CO2_UPDATE_INTERVAL, co2Update);
AgSchedule pmsSchedule(SENSOR_PM_UPDATE_INTERVAL, pmUpdate);
AgSchedule tempHumSchedule(SENSOR_TEMP_HUM_UPDATE_INTERVAL, tempHumUpdate);
void setup() {
Serial.begin(115200);
showNr();
/** Init I2C */
Wire.begin(ag.getI2cSdaPin(), ag.getI2cSclPin());
delay(1000);
/** Board init */
boardInit();
/** Init AirGradient server */
apiClient.begin();
apiClient.setAirGradient(&ag);
configuration.setAirGradient(&ag);
wifiConnector.setAirGradient(&ag);
/** Show boot display */
displayShowText("DIY basic", "Lib:" + ag.getVersion(), "");
delay(2000);
/** WiFi connect */
// connectToWifi();
if (wifiConnector.connect()) {
if (WiFi.status() == WL_CONNECTED) {
sendDataToAg();
apiClient.fetchServerConfiguration();
if (configuration.isCo2CalibrationRequested()) {
executeCo2Calibration();
}
}
}
/** 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();
if (hasSensorS8) {
co2Schedule.run();
}
if (hasSensorPMS) {
pmsSchedule.run();
}
if (hasSensorSHT) {
tempHumSchedule.run();
}
wifiConnector.handle();
/** Read PMS on loop */
ag.pms5003.handle();
}
static void sendDataToAg() {
// delay(1500);
if (apiClient.sendPing(wifiConnector.RSSI(), 0)) {
// 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();
delay(100);
}
static void boardInit(void) {
/** Init SHT sensor */
if (ag.sht.begin(Wire) == false) {
hasSensorSHT = false;
Serial.println("SHT sensor not found");
}
/** CO2 init */
if (ag.s8.begin(&Serial) == false) {
Serial.println("CO2 S8 snsor not found");
hasSensorS8 = false;
}
/** PMS init */
if (ag.pms5003.begin(&Serial) == false) {
Serial.println("PMS sensor not found");
hasSensorPMS = false;
}
/** Display init */
ag.display.begin(Wire);
ag.display.setTextColor(1);
ag.display.clear();
ag.display.show();
delay(100);
}
static void failedHandler(String msg) {
while (true) {
Serial.println(msg);
delay(1000);
}
}
static void executeCo2Calibration(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 updateServerConfiguration(void) {
if (apiClient.fetchServerConfiguration()) {
if (configuration.isCo2CalibrationRequested()) {
if (hasSensorS8) {
executeCo2Calibration();
} else {
Serial.println("CO2 S8 not available, calib ignored");
}
}
if (configuration.getCO2CalibrationAbcDays() > 0) {
if (hasSensorS8) {
int newHour = configuration.getCO2CalibrationAbcDays() * 24;
Serial.printf("abcDays config: %d days(%d hours)\r\n",
configuration.getCO2CalibrationAbcDays(), newHour);
int curHour = ag.s8.getAbcPeriod();
Serial.printf("Current config: %d (hours)\r\n", curHour);
if (curHour == newHour) {
Serial.println("set 'abcDays' ignored");
} else {
if (ag.s8.setAbcPeriod(configuration.getCO2CalibrationAbcDays() *
24) == false) {
Serial.println("Set S8 abcDays period calib failed");
} else {
Serial.println("Set S8 abcDays period calib success");
}
}
} else {
Serial.println("CO2 S8 not available, set 'abcDays' ignored");
}
}
}
}
static void co2Update() {
int value = ag.s8.getCo2();
if (value >= 0) {
co2Ppm = value;
getCO2FailCount = 0;
Serial.printf("CO2 index: %d\r\n", co2Ppm);
} else {
getCO2FailCount++;
Serial.printf("Get CO2 failed: %d\r\n", getCO2FailCount);
if (getCO2FailCount >= 3) {
co2Ppm = -1;
}
}
}
void pmUpdate() {
if (ag.pms5003.isFailed() == false) {
pm25 = ag.pms5003.getPm25Ae();
Serial.printf("PMS2.5: %d\r\n", pm25);
pmFailCount = 0;
} else {
Serial.printf("PM read failed, %d", pmFailCount);
pmFailCount++;
if (pmFailCount >= 3) {
pm25 = -1;
}
}
}
static void tempHumUpdate() {
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() {
String wifi = "\"wifi\":" + String(WiFi.RSSI());
String rco2 = "";
if(co2Ppm >= 0){
rco2 = ",\"rco2\":" + String(co2Ppm);
}
String pm02 = "";
if(pm25) {
pm02 = ",\"pm02\":" + String(pm25);
}
String rhum = "";
if(hum >= 0){
rhum = ",\"rhum\":" + String(rhum);
}
String payload = "{" + wifi + rco2 + pm02 + rhum + "}";
if (apiClient.postToServer(payload) == false) {
Serial.println("Post to server failed");
}
}
static void dispHandler() {
String ln1 = "";
String ln2 = "";
String ln3 = "";
if (configuration.isPmStandardInUSAQI()) {
if (pm25 < 0) {
ln1 = "AQI: -";
} else {
ln1 = "AQI:" + String(ag.pms5003.convertPm25ToUsAqi(pm25));
}
} else {
if (pm25 < 0) {
ln1 = "PM :- ug";
} else {
ln1 = "PM :" + String(pm25) + " ug";
}
}
if (co2Ppm > -1001) {
ln2 = "CO2:" + String(co2Ppm);
} else {
ln2 = "CO2: -";
}
String _hum = "-";
if (hum > 0) {
_hum = String(hum);
}
String _temp = "-";
if (configuration.isTemperatureUnitInF()) {
if (temp > -1001) {
_temp = String((temp * 9 / 5) + 32).substring(0, 4);
}
ln3 = _temp + " " + _hum + "%";
} else {
if (temp > -1001) {
_temp = String(temp).substring(0, 4);
}
ln3 = _temp + " " + _hum + "%";
}
displayShowText(ln1, ln2, ln3);
}
static String getDevId(void) { return getNormalizedMac(); }
static void showNr(void) {
Serial.println();
Serial.println("Serial nr: " + getDevId());
}
String getNormalizedMac() {
String mac = WiFi.macAddress();
mac.replace(":", "");
mac.toLowerCase();
return mac;
}

View File

@ -1,460 +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 <U8g2lib.h>
#include <WiFiClient.h>
#include <WiFiManager.h>
typedef struct {
bool inF; /** Temperature unit */
bool inUSAQI; /** PMS standard */
uint8_t ledBarMode; /** @ref UseLedBar*/
char model[16]; /** Model string value, Just define, don't know how much
memory usage */
char mqttBroker[128]; /** Mqtt broker link */
uint32_t _check; /** Checksum configuration data */
} ServerConfig_t;
static ServerConfig_t serverConfig;
AirGradient ag = AirGradient(BOARD_DIY_BASIC_KIT);
// CONFIGURATION START
// set to the endpoint you would like to use
String APIROOT = "http://hw.airgradient.com/";
String wifiApPass = "cleanair";
// set to true if you want to connect to wifi. You have 60 seconds to connect.
// Then it will go into an offline mode.
boolean connectWIFI = true;
// CONFIGURATION END
unsigned long currentMillis = 0;
const int oledInterval = 5000;
unsigned long previousOled = 0;
bool co2CalibrationRequest = false;
uint32_t serverConfigLoadTime = 0;
String HOSTPOT = "";
const int sendToServerInterval = 60000;
const int pollServerConfigInterval = 30000;
const int co2CalibCountdown = 5; /** Seconds */
unsigned long previoussendToServer = 0;
const int co2Interval = 5000;
unsigned long previousCo2 = 0;
int Co2 = 0;
const int pm25Interval = 5000;
unsigned long previousPm25 = 0;
int pm25 = 0;
const int tempHumInterval = 2500;
unsigned long previousTempHum = 0;
float temp = 0;
int hum = 0;
long val;
void failedHandler(String msg);
void boardInit(void);
void getServerConfig(void);
void co2Calibration(void);
void setup() {
Serial.begin(115200);
/** Init I2C */
Wire.begin(ag.getI2cSdaPin(), ag.getI2cSclPin());
/** Board init */
boardInit();
/** Show boot display */
displayShowText("Basic v4", "Lib:" + ag.getVersion(), "");
delay(2000);
if (connectWIFI) {
connectToWifi();
}
/** Show display */
displayShowText("Warm Up", "Serial#", String(ESP.getChipId(), HEX));
delay(10000);
getServerConfig();
}
void loop() {
currentMillis = millis();
updateOLED();
updateCo2();
updatePm25();
updateTempHum();
sendToServer();
getServerConfig();
}
void updateCo2() {
if (currentMillis - previousCo2 >= co2Interval) {
previousCo2 += co2Interval;
Co2 = ag.s8.getCo2();
Serial.println(String(Co2));
}
}
void updatePm25() {
if (currentMillis - previousPm25 >= pm25Interval) {
previousPm25 += pm25Interval;
if (ag.pms5003.readData()) {
pm25 = ag.pms5003.getPm25Ae();
Serial.printf("PM25: %d\r\n", pm25);
}
}
}
void updateTempHum() {
if (currentMillis - previousTempHum >= tempHumInterval) {
previousTempHum += tempHumInterval;
/** Get temperature and humidity */
temp = ag.sht.getTemperature();
hum = ag.sht.getRelativeHumidity();
/** Print debug message */
Serial.printf("SHT Humidity: %d%, Temperature: %0.2f\r\n", hum, temp);
}
}
void updateOLED() {
if (currentMillis - previousOled >= oledInterval) {
previousOled += oledInterval;
String ln1;
String ln2;
String ln3;
if (serverConfig.inUSAQI) {
ln1 = "AQI:" + String(ag.pms5003.convertPm25ToUsAqi(pm25));
} else {
ln1 = "PM :" + String(pm25) + " ug";
}
ln2 = "CO2:" + String(Co2);
if (serverConfig.inF) {
ln3 =
String((temp * 9 / 5) + 32).substring(0, 4) + " " + String(hum) + "%";
} else {
ln3 = String(temp).substring(0, 4) + " " + String(hum) + "%";
}
displayShowText(ln1, ln2, ln3);
}
}
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();
}
void sendToServer() {
if (currentMillis - previoussendToServer >= sendToServerInterval) {
previoussendToServer += sendToServerInterval;
String payload = "{\"wifi\":" + String(WiFi.RSSI()) +
(Co2 < 0 ? "" : ", \"rco2\":" + String(Co2)) +
(pm25 < 0 ? "" : ", \"pm02\":" + String(pm25)) +
", \"atmp\":" + String(temp) +
(hum < 0 ? "" : ", \"rhum\":" + String(hum)) + "}";
if (WiFi.status() == WL_CONNECTED) {
Serial.println(payload);
String POSTURL = APIROOT +
"sensors/airgradient:" + String(ESP.getChipId(), HEX) +
"/measures";
Serial.println(POSTURL);
WiFiClient client;
HTTPClient http;
http.begin(client, POSTURL);
http.addHeader("content-type", "application/json");
int httpCode = http.POST(payload);
String response = http.getString();
Serial.println(httpCode);
Serial.println(response);
http.end();
} else {
Serial.println("WiFi Disconnected");
}
}
}
// Wifi Manager
void connectToWifi() {
WiFiManager wifiManager;
// WiFi.disconnect(); //to delete previous saved hotspot
String HOTSPOT = "AG-" + String(ESP.getChipId(), HEX);
// displayShowText("Connect", "AG-", String(ESP.getChipId(), HEX));
delay(2000);
// wifiManager.setTimeout(90);
wifiManager.setConfigPortalBlocking(false);
wifiManager.setConfigPortalTimeout(180);
wifiManager.autoConnect(HOTSPOT.c_str(), wifiApPass.c_str());
uint32_t lastTime = millis();
int count = 179;
displayShowText("180 sec", "SSID:",HOTSPOT);
while (wifiManager.getConfigPortalActive()) {
wifiManager.process();
uint32_t ms = (uint32_t)(millis() - lastTime);
if (ms >= 1000) {
lastTime = millis();
displayShowText(String(count) + " sec", "SSID:",HOTSPOT);
count--;
// Timeout
if (count == 0) {
break;
}
}
}
if (!WiFi.isConnected()) {
displayShowText("Booting", "offline", "mode");
Serial.println("failed to connect and hit timeout");
delay(6000);
}
}
void failedHandler(String msg) {
while (true) {
Serial.println(msg);
delay(1000);
}
}
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);
}
void showConfig(void) {
Serial.println("Server configuration: ");
Serial.printf(" inF: %s\r\n", serverConfig.inF ? "true" : "false");
Serial.printf(" inUSAQI: %s\r\n",
serverConfig.inUSAQI ? "true" : "false");
Serial.printf("useRGBLedBar: %d\r\n", (int)serverConfig.ledBarMode);
Serial.printf(" Model: %.*s\r\n", sizeof(serverConfig.model),
serverConfig.model);
Serial.printf(" Mqtt Broker: %.*s\r\n", sizeof(serverConfig.mqttBroker),
serverConfig.mqttBroker);
}
void updateServerConfigLoadTime(void) {
serverConfigLoadTime = millis();
if (serverConfigLoadTime == 0) {
serverConfigLoadTime = 1;
}
}
void getServerConfig(void) {
/** Only trigger load configuration again after pollServerConfigInterval sec
*/
if (serverConfigLoadTime) {
uint32_t ms = (uint32_t)(millis() - serverConfigLoadTime);
if (ms < pollServerConfigInterval) {
return;
}
}
updateServerConfigLoadTime();
Serial.println("Trigger load server configuration");
if (WiFi.status() != WL_CONNECTED) {
Serial.println(
"Ignore get server configuration because WIFI not connected");
return;
}
// WiFiClient wifiClient;
HTTPClient httpClient;
String getUrl = "http://hw.airgradient.com/sensors/airgradient:" +
String(ESP.getChipId(), HEX) + "/one/config";
Serial.println("HttpClient get: " + getUrl);
WiFiClient client;
if (httpClient.begin(client, getUrl) == false) {
Serial.println("HttpClient init failed");
updateServerConfigLoadTime();
return;
}
int respCode = httpClient.GET();
/** get failure */
if (respCode != 200) {
Serial.printf("HttpClient get failed: %d\r\n", respCode);
updateServerConfigLoadTime();
return;
}
String respContent = httpClient.getString();
Serial.println("Server config: " + respContent);
/** Parse JSON */
JSONVar root = JSON.parse(respContent);
if (JSON.typeof_(root) == "undefined") {
Serial.println("Server configura JSON invalid");
updateServerConfigLoadTime();
return;
}
/** Get "country" */
bool inF = serverConfig.inF;
if (JSON.typeof_(root["country"]) == "string") {
String country = root["country"];
if (country == "US") {
inF = true;
} else {
inF = false;
}
}
/** Get "pmStandard" */
bool inUSAQI = serverConfig.inUSAQI;
if (JSON.typeof_(root["pmStandard"]) == "string") {
String standard = root["pmStandard"];
if (standard == "ugm3") {
inUSAQI = false;
} else {
inUSAQI = true;
}
}
/** Get CO2 "co2CalibrationRequested" */
co2CalibrationRequest = false;
if (JSON.typeof_(root["co2CalibrationRequested"]) == "boolean") {
co2CalibrationRequest = root["co2CalibrationRequested"];
}
/** get "model" */
String model = "";
if (JSON.typeof_(root["model"]) == "string") {
String _model = root["model"];
model = _model;
}
/** get "mqttBrokerUrl" */
String mqtt = "";
if (JSON.typeof_(root["mqttBrokerUrl"]) == "string") {
String _mqtt = root["mqttBrokerUrl"];
mqtt = _mqtt;
}
if (inF != serverConfig.inF) {
serverConfig.inF = inF;
}
if (inUSAQI != serverConfig.inUSAQI) {
serverConfig.inUSAQI = inUSAQI;
}
if (model.length()) {
if (model != String(serverConfig.model)) {
memset(serverConfig.model, 0, sizeof(serverConfig.model));
memcpy(serverConfig.model, model.c_str(), model.length());
}
}
if (mqtt.length()) {
if (mqtt != String(serverConfig.mqttBroker)) {
memset(serverConfig.mqttBroker, 0, sizeof(serverConfig.mqttBroker));
memcpy(serverConfig.mqttBroker, mqtt.c_str(), mqtt.length());
}
}
/** Show server configuration */
showConfig();
/** Calibration */
if (co2CalibrationRequest) {
co2Calibration();
}
}
void co2Calibration(void) {
/** Count down for co2CalibCountdown secs */
for (int i = 0; i < co2CalibCountdown; i++) {
displayShowText("CO2 calib", "after",
String(co2CalibCountdown - 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(2000);
} else {
displayShowText("Calib", "failure!!!", "");
delay(2000);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,68 @@
#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) {
server.send(200, "application/json", config.toString());
}
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_ */

View File

@ -0,0 +1,996 @@
/*
This is the combined firmware code for AirGradient ONE and AirGradient Open Air
open-source hardware Air Quality Monitor with ESP32-C3 Microcontroller.
It is an air quality monitor for PM2.5, CO2, TVOCs, NOx, Temperature and
Humidity with a small display, an RGB led bar 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: AirGradient ONE:
https://www.airgradient.com/documentation/one-v9/ Build Instructions:
AirGradient Open Air:
https://www.airgradient.com/documentation/open-air-pst-kit-1-3/
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 "Minimal SPIFFS (1.9MB APP with OTA/190KB SPIFFS)"
- JTAG adapter "Disabled"
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
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 <HardwareSerial.h>
#include "AirGradient.h"
#include "OtaHandler.h"
#include "AgApiClient.h"
#include "AgConfigure.h"
#include "AgSchedule.h"
#include "AgStateMachine.h"
#include "AgWiFiConnector.h"
#include "EEPROM.h"
#include "ESPmDNS.h"
#include "LocalServer.h"
#include "MqttClient.h"
#include "OpenMetrics.h"
#include "WebServer.h"
#include <WebServer.h>
#define LED_BAR_ANIMATION_PERIOD 100 /** ms */
#define DISP_UPDATE_INTERVAL 2500 /** ms */
#define SERVER_CONFIG_UPDATE_INTERVAL 15000 /** 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 */
/** I2C define */
#define I2C_SDA_PIN 7
#define I2C_SCL_PIN 6
#define OLED_I2C_ADDR 0x3C
static MqttClient mqttClient(Serial);
static TaskHandle_t mqttTask = NULL;
static Configuration configuration(Serial);
static AgApiClient apiClient(Serial, configuration);
static Measurements measurements;
static AirGradient *ag;
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 OtaHandler otaHandler;
static LocalServer localServer(Serial, openMetrics, measurements, configuration,
wifiConnector);
static int pmFailCount = 0;
static uint32_t factoryBtnPressTime = 0;
static int getCO2FailCount = 0;
static bool offlineMode = false;
static AgFirmwareMode fwMode = FW_MODE_I_9PSL;
static bool ledBarButtonTest = false;
static void boardInit(void);
static void failedHandler(String msg);
static void configurationUpdateSchedule(void);
static void appLedHandler(void);
static void appDispHandler(void);
static void oledDisplayLedBarSchedule(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 createMqttTask(void);
static void initMqtt(void);
static void factoryConfigReset(void);
static void wdgFeedUpdate(void);
static void ledBarEnabledUpdate(void);
static bool sgp41Init(void);
AgSchedule dispLedSchedule(DISP_UPDATE_INTERVAL, oledDisplayLedBarSchedule);
AgSchedule configSchedule(SERVER_CONFIG_UPDATE_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);
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(I2C_SDA_PIN, I2C_SCL_PIN);
delay(1000);
/** Detect board type: ONE_INDOOR has OLED display, Scan the I2C address to
* identify board type */
Wire.beginTransmission(OLED_I2C_ADDR);
if (Wire.endTransmission() == 0x00) {
ag = new AirGradient(BoardType::ONE_INDOOR);
} else {
ag = new AirGradient(BoardType::OPEN_AIR_OUTDOOR);
}
Serial.println("Detected " + ag->getBoardName());
/** Init sensor */
boardInit();
configuration.setAirGradient(ag);
oledDisplay.setAirGradient(ag);
stateMachine.setAirGradient(ag);
wifiConnector.setAirGradient(ag);
apiClient.setAirGradient(ag);
openMetrics.setAirGradient(ag);
localServer.setAirGraident(ag);
/** Connecting wifi */
bool connectToWifi = false;
if (ag->isOne()) {
if (ledBarButtonTest) {
stateMachine.executeLedBarTest();
} else {
ledBarEnabledUpdate();
connectToWifi = true;
}
} else {
connectToWifi = true;
}
if (connectToWifi) {
apiClient.begin();
if (wifiConnector.connect()) {
if (wifiConnector.isConnected()) {
mdnsInit();
localServer.begin();
initMqtt();
sendDataToAg();
#ifdef ESP8266
// ota not supported
#else
otaHandler.updateFirmwareIfOutdated(ag->deviceId());
#endif
apiClient.fetchServerConfiguration();
configSchedule.update();
if (apiClient.isFetchConfigureFailed()) {
if (ag->isOne()) {
stateMachine.displayHandle(
AgStateMachineWiFiOkServerOkSensorConfigFailed);
}
stateMachine.handleLeds(
AgStateMachineWiFiOkServerOkSensorConfigFailed);
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
} else {
ledBarEnabledUpdate();
}
} else {
offlineMode = true;
}
}
}
/** Show display Warning up */
if (ag->isOne()) {
oledDisplay.setText("Warming Up", "Serial Number:", ag->deviceId().c_str());
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
}
appLedHandler();
appDispHandler();
}
void loop() {
/** Handle schedule */
dispLedSchedule.run();
configSchedule.run();
agApiPostSchedule.run();
if (configuration.hasSensorS8) {
co2Schedule.run();
}
if (configuration.hasSensorPMS1 || configuration.hasSensorPMS2) {
pmsSchedule.run();
}
if (ag->isOne()) {
if (configuration.hasSensorSHT) {
tempHumSchedule.run();
}
}
if (configuration.hasSensorSGP) {
tvocSchedule.run();
}
if (ag->isOne()) {
if (configuration.hasSensorPMS1) {
ag->pms5003.handle();
}
} else {
if (configuration.hasSensorPMS1) {
ag->pms5003t_1.handle();
}
if (configuration.hasSensorPMS2) {
ag->pms5003t_2.handle();
}
}
/** Auto reset external watchdog timer on offline mode and
* postDataToAirGradient disabled. */
if (offlineMode || (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();
}
static void co2Update(void) {
int value = ag->s8.getCo2();
if (value >= 0) {
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 = -1;
}
}
}
static void mdnsInit(void) {
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");
}
static void createMqttTask(void) {
if (mqttTask) {
vTaskDelete(mqttTask);
mqttTask = NULL;
Serial.println("Delete old MQTT task");
}
Serial.println("Create new MQTT task");
xTaskCreate(
[](void *param) {
for (;;) {
delay(MQTT_SYNC_INTERVAL);
/** Send data */
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");
}
}
}
},
"mqtt-task", 1024 * 4, NULL, 6, &mqttTask);
if (mqttTask == NULL) {
Serial.println("Creat mqttTask failed");
}
}
static void initMqtt(void) {
if (mqttClient.begin(configuration.getMqttBrokerUri())) {
Serial.println("Connect to MQTT broker successful");
createMqttTask();
} else {
Serial.println("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()) {
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);
if (ag->isOne()) {
String str = "for " + String(count) + " sec";
oledDisplay.setText("Factory reset", "keep pressed", str.c_str());
} else {
Serial.printf("Factory reset, keep pressed for %d sec\r\n", count);
}
count--;
if (count == 0) {
/** Stop MQTT task first */
if (mqttTask) {
vTaskDelete(mqttTask);
mqttTask = NULL;
}
/** Disconnect WIFI */
wifiConnector.disconnect();
wifiConnector.reset();
/** Reset local config */
configuration.reset();
if (ag->isOne()) {
oledDisplay.setText("Factory reset", "successful", "");
} else {
Serial.println("Factory reset successful");
}
delay(3000);
ESP.restart();
}
}
/** Show current content cause reset ignore */
factoryBtnPressTime = 0;
if (ag->isOne()) {
appDispHandler();
}
}
}
} else {
if (factoryBtnPressTime != 0) {
if (ag->isOne()) {
/** Restore last display content */
appDispHandler();
}
}
factoryBtnPressTime = 0;
}
}
static void wdgFeedUpdate(void) {
ag->watchdog.reset();
Serial.println();
Serial.println("External watchdog feed");
Serial.println();
}
static void ledBarEnabledUpdate(void) {
if (ag->isOne()) {
ag->ledBar.setEnable(configuration.getLedBarMode() != LedBarModeOff);
}
}
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 sendDataToAg() {
/** Change oledDisplay and led state */
if (ag->isOne()) {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting);
}
stateMachine.handleLeds(AgStateMachineWiFiOkServerConnecting);
/** Task handle led connecting animation */
xTaskCreate(
[](void *obj) {
for (;;) {
// ledSmHandler();
stateMachine.handleLeds();
if (stateMachine.getLedState() !=
AgStateMachineWiFiOkServerConnecting) {
break;
}
delay(LED_BAR_ANIMATION_PERIOD);
}
vTaskDelete(NULL);
},
"task_led", 2048, NULL, 5, NULL);
delay(1500);
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount)) {
if (ag->isOne()) {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected);
}
stateMachine.handleLeds(AgStateMachineWiFiOkServerConnected);
} else {
if (ag->isOne()) {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed);
}
stateMachine.handleLeds(AgStateMachineWiFiOkServerConnectFailed);
}
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
stateMachine.handleLeds(AgStateMachineNormal);
}
/**
* @brief Must reset each 5min to avoid ESP32 reset
*/
static void resetWatchdog() { ag->watchdog.reset(); }
void dispSensorNotFound(String ss) {
ss = ss + " not found";
oledDisplay.setText("Sensor init", "Error:", ss.c_str());
delay(2000);
}
static void oneIndoorInit(void) {
configuration.hasSensorPMS2 = false;
/** 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->ledBar.begin();
ag->button.begin();
ag->watchdog.begin();
/** 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(Serial1) == false) {
Serial.println("CO2 S8 sensor not found");
configuration.hasSensorS8 = false;
dispSensorNotFound("S8");
}
/** Init PMS5003 */
if (ag->pms5003.begin(Serial0) == false) {
Serial.println("PMS sensor not found");
configuration.hasSensorPMS1 = false;
dispSensorNotFound("PMS");
}
/** Run LED test on start up */
oledDisplay.setText("Press now for", "LED test &", "offline mode");
ledBarButtonTest = false;
uint32_t stime = millis();
while (true) {
if (ag->button.getState() == ag->button.BUTTON_PRESSED) {
ledBarButtonTest = true;
break;
}
delay(1);
uint32_t ms = (uint32_t)(millis() - stime);
if (ms >= 3000) {
break;
}
}
}
static void openAirInit(void) {
configuration.hasSensorSHT = false;
fwMode = FW_MODE_O_1PST;
Serial.println("Firmware Version: " + ag->getVersion());
ag->watchdog.begin();
ag->button.begin();
ag->statusLed.begin();
/** detect sensor: PMS5003, PMS5003T, SGP41 and S8 */
/**
* Serial1 and Serial0 is use for connect S8 and PM sensor or both PM
*/
bool serial1Available = true;
bool serial0Available = true;
if (ag->s8.begin(Serial1) == false) {
Serial1.end();
delay(200);
Serial.println("Can not detect S8 on Serial1, try on Serial0");
/** Check on other port */
if (ag->s8.begin(Serial0) == false) {
configuration.hasSensorS8 = false;
Serial.println("CO2 S8 sensor not found");
Serial.println("Can not detect S8 run mode 'PPT'");
fwMode = FW_MODE_O_1PPT;
Serial0.end();
delay(200);
} else {
Serial.println("Found S8 on Serial0");
serial0Available = false;
}
} else {
Serial.println("Found S8 on Serial1");
serial1Available = false;
}
if (sgp41Init() == false) {
Serial.println("SGP sensor not found");
if (configuration.hasSensorS8 == false) {
Serial.println("Can not detect SGP run mode 'O-1PP'");
fwMode = FW_MODE_O_1PP;
} else {
Serial.println("Can not detect SGP run mode 'O-1PS'");
fwMode = FW_MODE_O_1PS;
}
}
/** Try to find the PMS on other difference port with S8 */
if (fwMode == FW_MODE_O_1PST) {
bool pmInitSuccess = false;
if (serial0Available) {
if (ag->pms5003t_1.begin(Serial0) == false) {
configuration.hasSensorPMS1 = false;
Serial.println("PMS1 sensor not found");
} else {
serial0Available = false;
pmInitSuccess = true;
Serial.println("Found PMS 1 on Serial0");
}
}
if (pmInitSuccess == false) {
if (serial1Available) {
if (ag->pms5003t_1.begin(Serial1) == false) {
configuration.hasSensorPMS1 = false;
Serial.println("PMS1 sensor not found");
} else {
serial1Available = false;
Serial.println("Found PMS 1 on Serial1");
}
}
}
configuration.hasSensorPMS2 = false; // Disable PM2
} else {
if (ag->pms5003t_1.begin(Serial0) == false) {
configuration.hasSensorPMS1 = false;
Serial.println("PMS1 sensor not found");
} else {
Serial.println("Found PMS 1 on Serial0");
}
if (ag->pms5003t_2.begin(Serial1) == false) {
configuration.hasSensorPMS2 = false;
Serial.println("PMS2 sensor not found");
} else {
Serial.println("Found PMS 2 on Serial1");
}
if (fwMode == FW_MODE_O_1PP) {
int count = (configuration.hasSensorPMS1 ? 1 : 0) +
(configuration.hasSensorPMS2 ? 1 : 0);
if (count == 1) {
fwMode = FW_MODE_O_1P;
}
}
}
/** update the PMS poll period base on fw mode and sensor available */
if (fwMode != FW_MODE_O_1PST) {
if (configuration.hasSensorPMS1 && configuration.hasSensorPMS2) {
pmsSchedule.setPeriod(2000);
}
}
Serial.printf("Firmware Mode: %s\r\n", AgFirmwareModeName(fwMode));
}
static void boardInit(void) {
if (ag->isOne()) {
oneIndoorInit();
} else {
openAirInit();
}
/** 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);
vTaskDelay(1000);
}
}
static void configurationUpdateSchedule(void) {
if (apiClient.fetchServerConfiguration()) {
configUpdateHandle();
}
}
static void configUpdateHandle() {
if (configuration.isUpdated() == false) {
return;
}
ledBarEnabledUpdate();
stateMachine.executeCo2Calibration();
stateMachine.executeLedBarTest();
String mqttUri = configuration.getMqttBrokerUri();
if (mqttClient.isCurrentUri(mqttUri) == false) {
mqttClient.end();
initMqtt();
}
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);
}
}
appDispHandler();
appLedHandler();
}
static void appLedHandler(void) {
AgStateMachineState state = AgStateMachineNormal;
if (wifiConnector.isConnected() == false) {
state = AgStateMachineWiFiLost;
} else if (apiClient.isFetchConfigureFailed()) {
stateMachine.displaySetAddToDashBoard();
state = AgStateMachineSensorConfigFailed;
} else if (apiClient.isPostToServerFailed()) {
state = AgStateMachineServerLost;
}
stateMachine.handleLeds(state);
}
static void appDispHandler(void) {
if (ag->isOne()) {
AgStateMachineState state = AgStateMachineNormal;
if (wifiConnector.isConnected() == false) {
state = AgStateMachineWiFiLost;
} else if (apiClient.isFetchConfigureFailed()) {
state = AgStateMachineSensorConfigFailed;
} else if (apiClient.isPostToServerFailed()) {
state = AgStateMachineServerLost;
}
stateMachine.displayHandle(state);
}
}
static void oledDisplayLedBarSchedule(void) {
if (ag->isOne()) {
if (factoryBtnPressTime == 0) {
appDispHandler();
}
}
appLedHandler();
}
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->isOne()) {
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 = -1;
measurements.pm25_1 = -1;
measurements.pm10_1 = -1;
measurements.pm03PCount_1 = -1;
}
}
} else {
bool pmsResult_1 = false;
bool pmsResult_2 = false;
if (configuration.hasSensorPMS1 && (ag->pms5003t_1.isFailed() == false)) {
measurements.pm01_1 = ag->pms5003t_1.getPm01Ae();
measurements.pm25_1 = ag->pms5003t_1.getPm25Ae();
measurements.pm10_1 = ag->pms5003t_1.getPm10Ae();
measurements.pm03PCount_1 = ag->pms5003t_1.getPm03ParticleCount();
measurements.temp_1 = ag->pms5003t_1.getTemperature();
measurements.hum_1 = ag->pms5003t_1.getRelativeHumidity();
pmsResult_1 = true;
Serial.println();
Serial.printf("[1] PM1 ug/m3: %d\r\n", measurements.pm01_1);
Serial.printf("[1] PM2.5 ug/m3: %d\r\n", measurements.pm25_1);
Serial.printf("[1] PM10 ug/m3: %d\r\n", measurements.pm10_1);
Serial.printf("[1] PM3.0 Count: %d\r\n", measurements.pm03PCount_1);
Serial.printf("[1] Temperature in C: %0.2f\r\n", measurements.temp_1);
Serial.printf("[1] Relative Humidity: %d\r\n", measurements.hum_1);
Serial.printf("[1] Temperature compensated in C: %0.2f\r\n",
ag->pms5003t_1.temperatureCompensated(measurements.temp_1));
Serial.printf("[1] Relative Humidity compensated: %d\r\n",
ag->pms5003t_1.humidityCompensated(measurements.hum_1));
} else {
measurements.pm01_1 = -1;
measurements.pm25_1 = -1;
measurements.pm10_1 = -1;
measurements.pm03PCount_1 = -1;
measurements.temp_1 = -1001;
measurements.hum_1 = -1;
}
if (configuration.hasSensorPMS2 && (ag->pms5003t_2.isFailed() == false)) {
measurements.pm01_2 = ag->pms5003t_2.getPm01Ae();
measurements.pm25_2 = ag->pms5003t_2.getPm25Ae();
measurements.pm10_2 = ag->pms5003t_2.getPm10Ae();
measurements.pm03PCount_2 = ag->pms5003t_2.getPm03ParticleCount();
measurements.temp_2 = ag->pms5003t_2.getTemperature();
measurements.hum_2 = ag->pms5003t_2.getRelativeHumidity();
pmsResult_2 = true;
Serial.println();
Serial.printf("[2] PM1 ug/m3: %d\r\n", measurements.pm01_2);
Serial.printf("[2] PM2.5 ug/m3: %d\r\n", measurements.pm25_2);
Serial.printf("[2] PM10 ug/m3: %d\r\n", measurements.pm10_2);
Serial.printf("[2] PM3.0 Count: %d\r\n", measurements.pm03PCount_2);
Serial.printf("[2] Temperature in C: %0.2f\r\n", measurements.temp_2);
Serial.printf("[2] Relative Humidity: %d\r\n", measurements.hum_2);
Serial.printf("[2] Temperature compensated in C: %0.2f\r\n",
ag->pms5003t_1.temperatureCompensated(measurements.temp_2));
Serial.printf("[2] Relative Humidity compensated: %d\r\n",
ag->pms5003t_1.humidityCompensated(measurements.hum_2));
} else {
measurements.pm01_2 = -1;
measurements.pm25_2 = -1;
measurements.pm10_2 = -1;
measurements.pm03PCount_2 = -1;
measurements.temp_2 = -1001;
measurements.hum_2 = -1;
}
if (configuration.hasSensorPMS1 && configuration.hasSensorPMS2 &&
pmsResult_1 && pmsResult_2) {
/** Get total of PMS1*/
measurements.pm1Value01 = measurements.pm1Value01 + measurements.pm01_1;
measurements.pm1Value25 = measurements.pm1Value25 + measurements.pm25_1;
measurements.pm1Value10 = measurements.pm1Value10 + measurements.pm10_1;
measurements.pm1PCount =
measurements.pm1PCount + measurements.pm03PCount_1;
measurements.pm1temp = measurements.pm1temp + measurements.temp_1;
measurements.pm1hum = measurements.pm1hum + measurements.hum_1;
/** Get total of PMS2 */
measurements.pm2Value01 = measurements.pm2Value01 + measurements.pm01_2;
measurements.pm2Value25 = measurements.pm2Value25 + measurements.pm25_2;
measurements.pm2Value10 = measurements.pm2Value10 + measurements.pm10_2;
measurements.pm2PCount =
measurements.pm2PCount + measurements.pm03PCount_2;
measurements.pm2temp = measurements.pm2temp + measurements.temp_2;
measurements.pm2hum = measurements.pm2hum + measurements.hum_2;
measurements.countPosition++;
/** Get average */
if (measurements.countPosition == measurements.targetCount) {
measurements.pm01_1 =
measurements.pm1Value01 / measurements.targetCount;
measurements.pm25_1 =
measurements.pm1Value25 / measurements.targetCount;
measurements.pm10_1 =
measurements.pm1Value10 / measurements.targetCount;
measurements.pm03PCount_1 =
measurements.pm1PCount / measurements.targetCount;
measurements.temp_1 = measurements.pm1temp / measurements.targetCount;
measurements.hum_1 = measurements.pm1hum / measurements.targetCount;
measurements.pm01_2 =
measurements.pm2Value01 / measurements.targetCount;
measurements.pm25_2 =
measurements.pm2Value25 / measurements.targetCount;
measurements.pm10_2 =
measurements.pm2Value10 / measurements.targetCount;
measurements.pm03PCount_2 =
measurements.pm2PCount / measurements.targetCount;
measurements.temp_2 = measurements.pm2temp / measurements.targetCount;
measurements.hum_2 = measurements.pm2hum / measurements.targetCount;
measurements.countPosition = 0;
measurements.pm1Value01 = 0;
measurements.pm1Value25 = 0;
measurements.pm1Value10 = 0;
measurements.pm1PCount = 0;
measurements.pm1temp = 0;
measurements.pm1hum = 0;
measurements.pm2Value01 = 0;
measurements.pm2Value25 = 0;
measurements.pm2Value10 = 0;
measurements.pm2PCount = 0;
measurements.pm2temp = 0;
measurements.pm2hum = 0;
}
}
if (pmsResult_1 && pmsResult_2) {
measurements.Temperature =
(measurements.temp_1 + measurements.temp_2) / 2;
measurements.Humidity = (measurements.hum_1 + measurements.hum_2) / 2;
} else {
if (pmsResult_1) {
measurements.Temperature = measurements.temp_1;
measurements.Humidity = measurements.hum_1;
}
if (pmsResult_2) {
measurements.Temperature = measurements.temp_2;
measurements.Humidity = measurements.hum_2;
}
}
if (configuration.hasSensorSGP) {
float temp;
float hum;
if (pmsResult_1 && pmsResult_2) {
temp = (measurements.temp_1 + measurements.temp_2) / 2.0f;
hum = (measurements.hum_1 + measurements.hum_2) / 2.0f;
} else {
if (pmsResult_1) {
temp = measurements.temp_1;
hum = measurements.hum_1;
}
if (pmsResult_2) {
temp = measurements.temp_2;
hum = measurements.hum_2;
}
}
ag->sgp41.setCompensationTemperatureHumidity(temp, hum);
}
}
}
static void sendDataToServer(void) {
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI(),
ag, &configuration);
if (apiClient.postToServer(syncData)) {
resetWatchdog();
}
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");
}
}

View File

@ -0,0 +1,212 @@
#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 = -1001;
float _hum = -1;
int pm01 = -1;
int pm25 = -1;
int pm10 = -1;
int pm03PCount = -1;
int atmpCompensated = -1;
int ahumCompensated = -1;
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;
}
} 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 (pm01 >= 0) {
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 (pm25 >= 0) {
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 (pm10 >= 0) {
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 (pm03PCount >= 0) {
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 (measure.TVOC >= 0) {
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 (measure.TVOCRaw >= 0) {
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 (measure.NOx >= 0) {
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 (measure.NOxRaw >= 0) {
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 (_temp > -1001) {
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 (atmpCompensated > -1001) {
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 (_hum >= 0) {
add_metric(
"humidity",
"The relative humidity as measured by the AirGradient SHT sensor"
"gauge", "percent");
add_metric_point("", String(_hum));
}
if (ahumCompensated >= 0) {
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,128 @@
#ifndef _OTA_HANDLER_H_
#define _OTA_HANDLER_H_
#include <esp_ota_ops.h>
#include <esp_http_client.h>
#include <esp_err.h>
#include <Arduino.h>
#define OTA_BUF_SIZE 512
#define URL_BUF_SIZE 256
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 @ %s\n", urlAsChar);
esp_http_client_config_t config = {};
config.url = urlAsChar;
esp_err_t ret = attemptToPerformOta(&config);
Serial.println(ret);
if (ret == 0) {
Serial.println("OTA completed");
esp_restart();
} else {
Serial.println("OTA failed, maybe already up to date");
}
}
private:
int 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 -1;
}
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 -1;
}
esp_http_client_fetch_headers(client);
esp_ota_handle_t update_handle = 0;
const esp_partition_t *update_partition = NULL;
Serial.println("Starting OTA ...");
update_partition = esp_ota_get_next_update_partition(NULL);
if (update_partition == NULL) {
Serial.println("Passive OTA partition not found");
cleanupHttp(client);
return ESP_FAIL;
}
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 err;
}
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 ESP_ERR_NO_MEM;
}
int binary_file_len = 0;
while (1) {
int data_read = esp_http_client_read(client, upgrade_data_buf, OTA_BUF_SIZE);
if (data_read == 0) {
Serial.println("Connection closed, all data received");
break;
}
if (data_read < 0) {
Serial.println("Data read error");
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) {
break;
}
binary_file_len += data_read;
// Serial.printf("Written image length %d\n", binary_file_len);
}
}
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 ota_write_err;
} else if (ota_end_err != ESP_OK) {
Serial.printf("Error: esp_ota_end failed! err=0x%d. Image is invalid", ota_end_err);
return ota_end_err;
}
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 err;
}
return 0;
}
void cleanupHttp(esp_http_client_handle_t client) {
esp_http_client_close(client);
esp_http_client_cleanup(client);
}
};
#endif

View File

@ -1,687 +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 DEBUG true
#define WIFI_CONNECT_COUNTDOWN_MAX 180 /** sec */
#define WIFI_HOTSPOT_PASSWORD_DEFAULT "cleanair"
AirGradient ag(BOARD_OUTDOOR_MONITOR_V1_3);
// time in seconds needed for NOx conditioning
uint16_t conditioning_s = 10;
String APIROOT = "http://hw.airgradient.com/";
typedef struct {
bool inF; /** Temperature unit */
bool inUSAQI; /** PMS standard */
uint8_t ledBarMode; /** @ref UseLedBar*/
char model[16]; /** Model string value, Just define, don't know how much
memory usage */
char mqttBroker[128]; /** Mqtt broker link */
uint32_t _check; /** Checksum configuration data */
} ServerConfig_t;
static ServerConfig_t serverConfig;
// set to true if you want to connect to wifi. You have 60 seconds to connect.
// Then it will go into an offline mode.
boolean connectWIFI = true;
static int ledSmState = APP_SM_NORMAL;
static bool serverFailed = false;
static bool configFailed = false;
static bool wifiHasConfig = false;
int loopCount = 0;
WiFiManager wifiManager; /** wifi manager instance */
unsigned long currentMillis = 0;
const int oledInterval = 5000;
unsigned long previousOled = 0;
const int sendToServerInterval = 60000;
const int pollServerConfigInterval = 30000;
const int co2CalibCountdown = 5; /** Seconds */
unsigned long previoussendToServer = 0;
const int tvocInterval = 1000;
unsigned long previousTVOC = 0;
int TVOC = -1;
int NOX = -1;
const int co2Interval = 5000;
unsigned long previousCo2 = 0;
int Co2 = 0;
const int pmInterval = 5000;
unsigned long previousPm = 0;
int pm25 = -1;
int pm01 = -1;
int pm10 = -1;
int pm03PCount = -1;
float temp;
int hum;
bool co2CalibrationRequest = false;
uint32_t serverConfigLoadTime = 0;
String HOTSPOT = "";
// const int tempHumInterval = 2500;
// unsigned long previousTempHum = 0;
void boardInit(void);
void failedHandler(String msg);
void getServerConfig(void);
void co2Calibration(void);
void setup() {
if (DEBUG) {
Serial.begin(115200);
}
/** Board init */
boardInit();
delay(500);
countdown(3);
if (connectWIFI) {
connectToWifi();
}
if (WiFi.status() == WL_CONNECTED) {
sendPing();
Serial.println(F("WiFi connected!"));
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
}
getServerConfig();
if (configFailed) {
ledSmHandler(APP_SM_SENSOR_CONFIG_FAILED);
delay(5000);
}
ledSmHandler(APP_SM_NORMAL);
}
void loop() {
currentMillis = millis();
updateTVOC();
updateCo2();
updatePm();
sendToServer();
getServerConfig();
}
void updateTVOC() {
delay(1000);
if (currentMillis - previousTVOC >= tvocInterval) {
previousTVOC += tvocInterval;
TVOC = ag.sgp41.getTvocIndex();
NOX = ag.sgp41.getNoxIndex();
}
}
void updateCo2() {
if (currentMillis - previousCo2 >= co2Interval) {
previousCo2 += co2Interval;
Co2 = ag.s8.getCo2();
Serial.printf("CO2: %d\r\n", Co2);
}
}
void updatePm() {
if (currentMillis - previousPm >= pmInterval) {
previousPm += pmInterval;
if (ag.pms5003t_1.readData()) {
pm01 = ag.pms5003t_1.getPm01Ae();
pm25 = ag.pms5003t_1.getPm25Ae();
pm10 = ag.pms5003t_1.getPm10Ae();
pm03PCount = ag.pms5003t_1.getPm03ParticleCount();
temp = ag.pms5003t_1.getTemperature();
hum = ag.pms5003t_1.getRelativeHumidity();
}
}
}
void sendPing() {
String payload =
"{\"wifi\":" + String(WiFi.RSSI()) + ", \"boot\":" + loopCount + "}";
if (postToServer(payload)) {
ledSmHandler(APP_SM_WIFI_OK_SERVER_CONNNECTED);
} else {
ledSmHandler(APP_SM_WIFI_OK_SERVER_CONNECT_FAILED);
}
delay(5000);
}
bool postToServer(String &payload) {
String POSTURL = APIROOT +
"sensors/airgradient:" + String(getNormalizedMac()) +
"/measures";
WiFiClient client;
HTTPClient http;
ag.statusLed.setOn();
http.begin(client, POSTURL);
http.addHeader("content-type", "application/json");
int httpCode = http.POST(payload);
Serial.printf("Post to %s, %d\r\n", POSTURL.c_str(), httpCode);
http.end();
ag.statusLed.setOff();
return (httpCode == 200);
}
void sendToServer() {
if (currentMillis - previoussendToServer >= sendToServerInterval) {
previoussendToServer += sendToServerInterval;
String payload =
"{\"wifi\":" + String(WiFi.RSSI()) +
(Co2 < 0 ? "" : ", \"rco2\":" + String(Co2)) +
(pm01 < 0 ? "" : ", \"pm01\":" + String(pm01)) +
(pm25 < 0 ? "" : ", \"pm02\":" + String(pm25)) +
(pm10 < 0 ? "" : ", \"pm10\":" + String(pm10)) +
(pm03PCount < 0 ? "" : ", \"pm003_count\":" + String(pm03PCount)) +
(TVOC < 0 ? "" : ", \"tvoc_index\":" + String(TVOC)) +
(NOX < 0 ? "" : ", \"nox_index\":" + String(NOX)) +
", \"atmp\":" + String(temp) +
(hum < 0 ? "" : ", \"rhum\":" + String(hum)) +
", \"boot\":" + loopCount + "}";
if (WiFi.status() == WL_CONNECTED) {
postToServer(payload);
resetWatchdog();
loopCount++;
} else {
Serial.println("WiFi Disconnected");
}
}
}
void countdown(int from) {
debug("\n");
while (from > 0) {
debug(String(from--));
debug(" ");
delay(1000);
}
debug("\n");
}
void resetWatchdog() {
Serial.println("Watchdog reset");
ag.watchdog.reset();
}
bool wifiMangerClientConnected(void) {
return WiFi.softAPgetStationNum() ? true : false;
}
// Wifi Manager
void connectToWifi() {
HOTSPOT = "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(HOTSPOT.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 */
if (WiFi.isConnected() == false) {
ledSmHandler(APP_SM_WIFI_MANAGER_CONNECT_FAILED);
} else {
wifiHasConfig = true;
}
}
void debug(String msg) {
if (DEBUG)
Serial.print(msg);
}
void debug(int msg) {
if (DEBUG)
Serial.print(msg);
}
void debugln(String msg) {
if (DEBUG)
Serial.println(msg);
}
void debugln(int msg) {
if (DEBUG)
Serial.println(msg);
}
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();
if (ag.pms5003t_1.begin(Serial0) == false) {
failedHandler("Init PMS5003T failed");
}
if (ag.s8.begin(Serial1) == false) {
failedHandler("Init SenseAirS8 failed");
}
if (ag.sgp41.begin(Wire) == false) {
failedHandler("Init SGP41 failed");
}
}
void failedHandler(String msg) {
while (true) {
Serial.println(msg);
vTaskDelay(1000);
}
}
void updateServerConfigLoadTime(void) {
serverConfigLoadTime = millis();
if (serverConfigLoadTime == 0) {
serverConfigLoadTime = 1;
}
}
void showConfig(void) {
Serial.println("Server configuration: ");
Serial.printf(" inF: %s\r\n", serverConfig.inF ? "true" : "false");
Serial.printf(" inUSAQI: %s\r\n",
serverConfig.inUSAQI ? "true" : "false");
Serial.printf("useRGBLedBar: %d\r\n", (int)serverConfig.ledBarMode);
Serial.printf(" Model: %.*s\r\n", sizeof(serverConfig.model),
serverConfig.model);
Serial.printf(" Mqtt Broker: %.*s\r\n", sizeof(serverConfig.mqttBroker),
serverConfig.mqttBroker);
}
void getServerConfig(void) {
/** Only trigger load configuration again after pollServerConfigInterval sec
*/
if (serverConfigLoadTime) {
uint32_t ms = (uint32_t)(millis() - serverConfigLoadTime);
if (ms < pollServerConfigInterval) {
return;
}
}
updateServerConfigLoadTime();
Serial.println("Trigger load server configuration");
if (WiFi.status() != WL_CONNECTED) {
Serial.println(
"Ignore get server configuration because WIFI not connected");
return;
}
// WiFiClient wifiClient;
HTTPClient httpClient;
String getUrl = "http://hw.airgradient.com/sensors/airgradient:" +
String(getNormalizedMac()) + "/one/config";
Serial.println("HttpClient get: " + getUrl);
if (httpClient.begin(getUrl) == false) {
Serial.println("HttpClient init failed");
updateServerConfigLoadTime();
return;
}
int respCode = httpClient.GET();
/** get failure */
if (respCode != 200) {
Serial.printf("HttpClient get failed: %d\r\n", respCode);
updateServerConfigLoadTime();
httpClient.end();
configFailed = true;
return;
}
String respContent = httpClient.getString();
Serial.println("Server config: " + respContent);
httpClient.end();
/** Parse JSON */
JSONVar root = JSON.parse(respContent);
if (JSON.typeof_(root) == "undefined") {
Serial.println("Server configura JSON invalid");
updateServerConfigLoadTime();
configFailed = true;
return;
}
configFailed = false;
/** Get "country" */
bool inF = serverConfig.inF;
if (JSON.typeof_(root["country"]) == "string") {
String country = root["country"];
if (country == "US") {
inF = true;
} else {
inF = false;
}
}
/** Get "pmStandard" */
bool inUSAQI = serverConfig.inUSAQI;
if (JSON.typeof_(root["pmStandard"]) == "string") {
String standard = root["pmStandard"];
if (standard == "ugm3") {
inUSAQI = false;
} else {
inUSAQI = true;
}
}
/** Get CO2 "co2CalibrationRequested" */
co2CalibrationRequest = false;
if (JSON.typeof_(root["co2CalibrationRequested"]) == "boolean") {
co2CalibrationRequest = root["co2CalibrationRequested"];
}
/** get "model" */
String model = "";
if (JSON.typeof_(root["model"]) == "string") {
String _model = root["model"];
model = _model;
}
/** get "mqttBrokerUrl" */
String mqtt = "";
if (JSON.typeof_(root["mqttBrokerUrl"]) == "string") {
String _mqtt = root["mqttBrokerUrl"];
mqtt = _mqtt;
}
if (inF != serverConfig.inF) {
serverConfig.inF = inF;
}
if (inUSAQI != serverConfig.inUSAQI) {
serverConfig.inUSAQI = inUSAQI;
}
if (model.length()) {
if (model != String(serverConfig.model)) {
memset(serverConfig.model, 0, sizeof(serverConfig.model));
memcpy(serverConfig.model, model.c_str(), model.length());
}
}
if (mqtt.length()) {
if (mqtt != String(serverConfig.mqttBroker)) {
memset(serverConfig.mqttBroker, 0, sizeof(serverConfig.mqttBroker));
memcpy(serverConfig.mqttBroker, mqtt.c_str(), mqtt.length());
}
}
/** Show server configuration */
showConfig();
/** Calibration */
if (co2CalibrationRequest) {
co2Calibration();
}
}
void co2Calibration(void) {
/** Count down for co2CalibCountdown secs */
for (int i = 0; i < co2CalibCountdown; i++) {
Serial.printf("Start CO2 calib after %d sec\r\n", co2CalibCountdown - 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);
}
}
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();
ag.statusLed.setOn();
delay(50);
ag.statusLed.setOff();
delay(950);
ag.statusLed.setOn();
delay(50);
ag.statusLed.setOff();
break;
}
case APP_SM_WIFI_MANAGER_CONNECT_FAILED: {
ag.statusLed.setOff();
for (int j = 0; j < 3; j++) {
for (int i = 0; i < 3 * 2; i++) {
ag.statusLed.setToggle();
delay(100);
}
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 * 2; i++) {
ag.statusLed.setToggle();
delay(100);
}
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 * 2; i++) {
ag.statusLed.setToggle();
delay(100);
}
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;
}
}

View File

@ -2,41 +2,47 @@
This is sample code for the AirGradient library with a minimal implementation to read CO2 values from the SenseAir S8 sensor.
CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
*/
#include <AirGradient.h>
#ifdef ESP8266
AirGradient ag = AirGradient(BOARD_DIY_PRO_INDOOR_V4_2);
// AirGradient ag = AirGradient(BOARD_DIY_BASIC_KIT);
AirGradient ag = AirGradient(DIY_BASIC);
#else
// AirGradient ag = AirGradient(BOARD_ONE_INDOOR_MONITOR_V9_0);
AirGradient ag = AirGradient(BOARD_OUTDOOR_MONITOR_V1_3);
/** Create airgradient instance for 'OPEN_AIR_OUTDOOR' board */
AirGradient ag = AirGradient(OPEN_AIR_OUTDOOR);
#endif
void failedHandler(String msg);
void setup() {
void setup()
{
Serial.begin(115200);
/** Init CO2 sensor */
#ifdef ESP8266
if (ag.s8.begin(&Serial) == false) {
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");
}
}
void loop() {
int CO2 = ag.s8.getCo2();
Serial.printf("CO2: %d\r\n", CO2);
void loop()
{
int co2Ppm = ag.s8.getCo2();
Serial.printf("CO2: %d\r\n", co2Ppm);
delay(5000);
}
void failedHandler(String msg) {
while (true) {
void failedHandler(String msg)
{
while (true)
{
Serial.println(msg);
delay(1000);
}

View File

@ -1,369 +0,0 @@
#include <AirGradient.h>
#include <HardwareSerial.h>
#include <Wire.h>
/**
* AirGradient use ESP32C3 has default Serial0 use for PMS5003, to print log
* should use esp-hal-log instead.
*/
#include <esp32-hal-log.h>
/**
* @brief Define test board
*/
#define TEST_BOARD_OUTDOOR_MONITOR_V1_3 0
#define TEST_BOARD_ONE_INDOOR_MONITOR_V9_0 1
/**
* @brief Define test sensor
*/
#define TEST_SENSOR_SenseAirS8 0
#define TEST_SENSOR_SHT4x 0
#define TEST_SENSOR_SGP4x 0
#define TEST_SWITCH 0
#define TEST_OLED 0
#if TEST_BOARD_OUTDOOR_MONITOR_V1_3
#define TEST_STATUS_LED 0
#define TEST_PMS5003T 1
#endif
#define TEST_WATCHDOG 1
#if TEST_BOARD_ONE_INDOOR_MONITOR_V9_0
#define TEST_LED_BAR 1
#define TEST_SENSOR_PMS5003 0
#endif
#if TEST_BOARD_OUTDOOR_MONITOR_V1_3
AirGradient ag(BOARD_OUTDOOR_MONITOR_V1_3);
#elif TEST_BOARD_ONE_INDOOR_MONITOR_V9_0
AirGradient ag(BOARD_ONE_INDOOR_MONITOR_V9_0);
#else
#error "Must enable board test
#endif
void setup() {
/** Print All AirGradient board define */
printBoardDef(NULL);
#if TEST_SENSOR_SenseAirS8
/** Cause Serial is use default for PMS, CO2S8 should be use Serial 1
* Serial 1 will be init by SenseAirS8 don't need to init any more on user
* code
*/
if (ag.s8.begin(Serial1)) {
log_i("CO2S8 sensor init success");
} else {
log_i("CO2S8 sensor init failure");
}
log_i("Start baseline calib");
if (ag.s8.setBaselineCalibration()) {
log_i("Calib success");
} else {
log_e("Calib failure");
}
delay(5000); // Wait for calib done
#endif
#if TEST_SENSOR_PMS5003
if (ag.pms5003.begin(Serial0)) {
log_i("PMS5003 sensor init success");
} else {
log_i("PMS5003 sensor init failure");
}
#endif
#if TEST_PMS5003T
/**
* @brief PMS5003T_1 alway connect to Serial (esp32c3 RXD0, RXD0)
*/
if (ag.pms5003t_1.begin(Serial)) {
log_i("PMS5003T_1 sensor init success");
} else {
log_i("PMS5003T_1 sensor init failure");
}
// TODO Only test without senseair s8 because it's share the UART bus
#if TEST_SENSOR_SenseAirS8 == 0
if (ag.pms5003t_2.begin(Serial1)) {
log_i("PMS5003T_2 sensor init success");
} else {
log_i("PMS5003T_2 sensor init failure");
}
#endif
#endif
#if TEST_SENSOR_SHT4x || TEST_SENSOR_SGP4x || TEST_OLED
Wire.begin(ag.getI2cSdaPin(), ag.getI2cSclPin());
#endif
#if TEST_SENSOR_SHT4x
if (ag.sht.begin(Wire)) {
log_i("SHT init success");
} else {
log_i("SHT init failed");
}
#endif
#if TEST_SENSOR_SGP4x
if (ag.sgp41.begin(Wire)) {
log_i("SGP init success");
} else {
log_e("SGP init failure");
}
#endif
#if TEST_LED
led.begin();
#endif
#if TEST_SWITCH
ag.button.begin();
#endif
#if TEST_OLED
ag.display.begin(Wire);
ag.display.setTextSize(1);
ag.display.setCursor(0, 0);
ag.display.setTextColor(1);
ag.display.setText("180s to connect to wifi hostpost AC-xxxxx");
ag.display.show();
#endif
#if TEST_STATUS_LED
ag.statusLed.begin();
#endif
#if TEST_LED_BAR
ag.ledBar.begin();
#endif
#if TEST_WATCHDOG
ag.watchdog.begin();
#endif
}
void loop() {
uint32_t ms;
#if TEST_SENSOR_SenseAirS8
static uint32_t lastTime = 0;
/** Wait for sensor ready */
ms = (uint32_t)(millis() - lastTime);
if (ms >= 1000) {
lastTime = millis();
log_i("CO2: %d (PPM)", ag.s8.getCo2());
}
#endif
#if TEST_SENSOR_PMS5003
static uint32_t pmsTime = 0;
ms = (uint32_t)(millis() - pmsTime);
if (ms >= 1000) {
pmsTime = millis();
if (ag.pms5003.readData()) {
log_i("Passive mode PM 1.0 (ug/m3): %d", ag.pms5003.getPm10Ae());
log_i("Passive mode PM 2.5 (ug/m3): %d", ag.pms5003.getPm25Ae());
log_i("Passive mode PM 10.0 (ug/m3): %d", ag.pms5003.getPm10Ae());
} else {
log_i("PMS sensor read failure");
}
}
#endif
#if TEST_PMS5003T
static uint32_t pmsTime = 0;
ms = (uint32_t)(millis() - pmsTime);
if (ms >= 1000) {
pmsTime = millis();
if (ag.pms5003t_1.readData()) {
log_i("PMS5003_1 PM 1.0 (ug/m3): %d", ag.pms5003t_1.getPm10Ae());
log_i("PMS5003_1 PM 2.5 (ug/m3): %d", ag.pms5003t_1.getPm25Ae());
log_i("PMS5003_1 PM 10.0 (ug/m3): %d", ag.pms5003t_1.getPm10Ae());
log_i("PMS5003_1 PM 3.0 (ug/m3): %d",
ag.pms5003t_1.getPm03ParticleCount());
log_i("Temperature : %02f °C",
ag.pms5003t_1.getTemperature());
log_i("Humidity : %02f %%",
ag.pms5003t_1.getRelativeHumidity());
} else {
log_i("PMS5003_1 sensor read failure");
}
if (ag.pms5003t_2.readData()) {
log_i("PMS5003_2 PM 1.0 (ug/m3): %d", ag.pms5003t_2.getPm10Ae());
log_i("PMS5003_2 PM 2.5 (ug/m3): %d", ag.pms5003t_2.getPm25Ae());
log_i("PMS5003_2 PM 10.0 (ug/m3): %d", ag.pms5003t_2.getPm10Ae());
log_i("PMS5003_2 PM 3.0 (ug/m3): %d",
ag.pms5003t_2.getPm03ParticleCount());
// log_i("Temperature : %02f °C",
// ag.pms5003t_1.getTemperature());
// log_i("Humidity : %02f %%",
// ag.pms5003t_1.getRelativeHumidity());
} else {
log_i("PMS5003_2 sensor read failure");
}
}
#endif
#if TEST_SENSOR_SHT4x
/**
* @brief Get SHT sensor data each 1sec
*
*/
static uint32_t shtTime = 0;
ms = (uint32_t)(millis() - shtTime);
if (ms >= 1000) {
shtTime = millis();
log_i("Get sht temperature: %0.2f (degree celsius)",
ag.sht.getTemperature());
log_i("Get sht temperature: %0.2f (%%)", ag.sht.getRelativeHumidity());
}
#endif
#if TEST_SENSOR_SGP4x
static uint32_t sgpTime;
ms = (uint32_t)(millis() - sgpTime);
if (ms >= 1000) {
sgpTime = millis();
uint16_t rawVOC;
log_i("Get TVOC: %d", ag.sgp41.getTvocIndex());
log_i("Get NOx: %d", ag.sgp41.getNoxIndex());
}
#endif
#if TEST_LED
static uint32_t ledTime;
#if TEST_BOARD_OUTDOOR_MONITOR_V1_3
// ms = (uint32_t)(millis() - ledTime);
// if(ms >= 500)
// {
// ledTime = millis();
// led.ledToggle();
// }
#elif TEST_BOARD_ONE_INDOOR_MONITOR_V9_0
static int ledIndex;
static int ledIndexOld;
ms = (uint32_t)(millis() - ledTime);
if (ms >= 50) {
ledTime = millis();
if (ledIndex == ledIndexOld) {
led.ledOff();
} else {
// Turn last LED off
led.ledSetColor(0, 0, 0, ledIndexOld);
}
// Turn new led ON
led.ledSetColor(255, 0, 0, ledIndex);
ledIndexOld = ledIndex;
ledIndex++;
if (ledIndex >= led.getNumberOfLed()) {
ledIndex = 0;
}
}
#else
#endif
#endif
#if TEST_SWITCH
static PushButton::State stateOld = PushButton::State::BUTTON_RELEASED;
PushButton::State state = ag.button.getState();
if (state != stateOld) {
stateOld = state;
log_i("Button state changed: %s", ag.button.toString(state).c_str());
if (state == PushButton::State::BUTTON_PRESSED) {
ag.statusLed.setOn();
} else {
ag.statusLed.setOff();
}
}
#endif
#if TEST_LED_BAR
static uint32_t ledTime;
static uint8_t ledNum = 0;
static uint8_t ledIndex = 0;
static uint8_t ledStep = 0;
static bool ledOn = false;
if (ledNum == 0) {
ledNum = ag.ledBar.getNumberOfLed();
log_i("Get number of led: %d", ledNum);
if (ledNum) {
ag.ledBar.setBrighness(0xff);
for (int i = 0; i < ledNum; i++) {
// ag.ledBar.setColor(0xff, 0xff, 0xff, i);
// ag.ledBar.setColor(204, 136, 153, i);
// ag.ledBar.setColor(204, 0, 0, i);
// ag.ledBar.setColor(204, 100, 153, i);
ag.ledBar.setColor(0, 136, 255, i);
}
ag.ledBar.show();
}
} else {
ms = (uint32_t)(millis() - ledTime);
if (ms >= 500) {
ledTime = millis();
switch (ledStep) {
case 0: {
ag.ledBar.setColor(255, 0, 0, ledIndex);
ledIndex++;
if (ledIndex >= ledNum) {
ag.ledBar.setColor(0, 0, 0);
ledIndex = 0;
ledStep = 1;
}
ag.ledBar.show();
break;
}
case 1: {
ledIndex++;
if (ledIndex >= ledNum) {
ag.ledBar.setColor(255, 0, 0);
ag.ledBar.show();
ledIndex = ledNum - 1;
ledStep = 2;
}
break;
}
case 2: {
if (ledOn) {
ag.ledBar.setColor(255, 0, 0);
} else {
ag.ledBar.setColor(0, 0, 0);
}
ledOn = !ledOn;
ag.ledBar.show();
ledIndex--;
if (ledIndex == 0) {
ag.ledBar.setColor(0, 0, 0);
ag.ledBar.show();
ledStep = 0;
ledIndex = 0;
}
break;
}
default:
break;
}
}
}
#endif
#if TEST_WATCHDOG
static uint32_t wdgTime;
ms = (uint32_t)(millis() - wdgTime);
if (ms >= (1000 * 60)) {
wdgTime = millis();
/** Reset watchdog reach 1 minutes */
ag.watchdog.reset();
}
#endif
}

View File

@ -1,164 +0,0 @@
#include <AirGradient.h>
#include <Wire.h>
/**
* @brief Define test board
*/
#define TEST_BOARD_DIY_BASIC_KIT 0
#define TEST_BOARD_DIY_PRO_INDOOR_V4_2 1
/**
* @brief Define test sensor
*/
#define TEST_SENSOR_SenseAirS8 0
#define TEST_SENSOR_PMS5003 0
#define TEST_SENSOR_SHT4x 0
#define TEST_SENSOR_SGP4x 1
#define TEST_SWITCH 0
#define TEST_OLED 0
#if TEST_BOARD_DIY_BASIC_KIT
AirGradient ag(BOARD_DIY_BASIC_KIT);
#elif TEST_BOARD_DIY_PRO_INDOOR_V4_2
AirGradient ag(BOARD_DIY_PRO_INDOOR_V4_2);
#else
#error "Board test not defined"
#endif
void setup() {
Serial.begin(115200);
/** Print All AirGradient board define */
printBoardDef(&Serial);
#if TEST_SENSOR_SenseAirS8
if (ag.s8.begin(&Serial) == true) {
Serial.println("CO2S8 sensor init success");
} else {
Serial.println("CO2S8 sensor init failure");
}
if (ag.s8.setBaselineCalibration()) {
Serial.println("Manual calib success");
} else {
Serial.println("Manual calib failure");
}
delay(5000);
#endif
#if TEST_SENSOR_PMS5003
if (ag.pms5003.begin(&Serial) == true) {
Serial.println("PMS5003 sensor init success");
} else {
Serial.println("PMS5003 sensor init failure");
}
#endif
#if TEST_SENSOR_SHT4x || TEST_SENSOR_SGP4x || TEST_OLED
Wire.begin(ag.getI2cSdaPin(), ag.getI2cSclPin());
#endif
#if TEST_SENSOR_SHT4x
if (ag.sht.begin(Wire, Serial)) {
Serial.println("SHT init success");
} else {
Serial.println("SHT init failed");
}
#endif
#if TEST_SENSOR_SGP4x
if (ag.sgp41.begin(Wire, Serial)) {
Serial.println("SGP init succses");
} else {
Serial.println("SGP init failure");
}
#endif
#if TEST_SWITCH
ag.button.begin(Serial);
#endif
#if TEST_OLED
ag.display.begin(Wire, Serial);
ag.display.setTextSize(1);
ag.display.setCursor(0, 0);
ag.display.setTextColor(1);
ag.display.setText("Hello");
ag.display.show();
#endif
}
void loop() {
uint32_t ms;
#if TEST_SENSOR_SenseAirS8
static uint32_t lastTime = 0;
/** Wait for sensor ready */
// if(co2s8.isReady())
// {
// Get sensor data each 1sec
ms = (uint32_t)(millis() - lastTime);
if (ms >= 1000) {
lastTime = millis();
Serial.printf("CO2: %d (PMM)\r\n", ag.s8.getCo2());
}
// }
#endif
#if TEST_SENSOR_PMS5003
static uint32_t pmsTime = 0;
ms = (uint32_t)(millis() - pmsTime);
if (ms >= 1000) {
pmsTime = millis();
if (ag.pms5003.readData()) {
Serial.printf("Passive mode PM 1.0 (ug/m3): %d\r\n",
ag.pms5003.getPm01Ae());
Serial.printf("Passive mode PM 2.5 (ug/m3): %d\r\n",
ag.pms5003.getPm25Ae());
Serial.printf("Passive mode PM 10.5 (ug/m3): %d\r\n",
ag.pms5003.getPm10Ae());
}
}
#endif
#if TEST_SENSOR_SHT4x
/**
* @brief Get SHT sensor data each 1sec
*
*/
static uint32_t shtTime = 0;
ms = (uint32_t)(millis() - shtTime);
if (ms >= 1000) {
shtTime = millis();
float temperature, humidity;
Serial.printf("SHT Temperature: %f, Humidity: %f\r\n",
ag.sht.getTemperature(), ag.sht.getRelativeHumidity());
}
#endif
#if TEST_SENSOR_SGP4x
static uint32_t sgpTime;
ms = (uint32_t)(millis() - sgpTime);
/***
* Must call this task on loop and avoid delay on loop over 1000 ms
*/
ag.sgp41.handle();
if (ms >= 1000) {
sgpTime = millis();
Serial.printf("SGP TVOC: %d, NOx: %d\r\n", ag.sgp41.getTvocIndex(),
ag.sgp41.getNoxIndex());
}
#endif
#if TEST_SWITCH
static PushButton::State stateOld = PushButton::State::BUTTON_RELEASED;
PushButton::State state = ag.button.getState();
if (state != stateOld) {
stateOld = state;
Serial.printf("Button state changed: %s\r\n",
ag.button.toString(state).c_str());
}
#endif
}

View File

@ -1,5 +1,6 @@
/*
This is sample code for the AirGradient library with a minimal implementation to read PM values from the Plantower sensor.
This is sample code for the AirGradient library with a minimal implementation
to read PM values from the Plantower sensor.
CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
*/
@ -7,11 +8,10 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
#include <AirGradient.h>
#ifdef ESP8266
AirGradient ag = AirGradient(BOARD_DIY_PRO_INDOOR_V4_2);
// AirGradient ag = AirGradient(BOARD_DIY_BASIC_KIT);
AirGradient ag = AirGradient(DIY_BASIC);
#else
// AirGradient ag = AirGradient(BOARD_ONE_INDOOR_MONITOR_V9_0);
AirGradient ag = AirGradient(BOARD_OUTDOOR_MONITOR_V1_3);
AirGradient ag = AirGradient(ONE_INDOOR);
// AirGradient ag = AirGradient(OPEN_AIR_OUTDOOR);
#endif
void failedHandler(String msg);
@ -19,46 +19,72 @@ void failedHandler(String msg);
void setup() {
Serial.begin(115200);
#ifdef ESP8266
if(ag.pms5003.begin(&Serial) == false) {
if (ag.pms5003.begin(&Serial) == false) {
failedHandler("Init PMS5003 failed");
}
#else
if (ag.getBoardType() == BOARD_OUTDOOR_MONITOR_V1_3) {
if(ag.pms5003t_1.begin(Serial0) == false) {
if (ag.getBoardType() == OPEN_AIR_OUTDOOR) {
if (ag.pms5003t_1.begin(Serial0) == false) {
failedHandler("Init PMS5003T failed");
}
} else {
if(ag.pms5003.begin(Serial0) == false) {
if (ag.pms5003.begin(Serial0) == false) {
failedHandler("Init PMS5003T failed");
}
}
#endif
}
uint32_t lastRead = 0;
void loop() {
int PM2;
#ifdef ESP8266
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() == BOARD_OUTDOOR_MONITOR_V1_3) {
PM2 = ag.pms5003t_1.getPm25Ae();
} else {
PM2 = ag.pms5003.getPm25Ae();
}
Serial.printf("PM2.5 in ug/m3: %d\r\n", PM2);
if (ag.getBoardType() == BOARD_OUTDOOR_MONITOR_V1_3) {
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));
}
#endif
bool readResul = false;
delay(5000);
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;
}
}
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

@ -0,0 +1,39 @@
#include <AirGradient.h>
#if defined(ESP8266)
AirGradient ag(DIY_BASIC);
#else
AirGradient ag(ONE_INDOOR);
#endif
void failedHandler(String msg);
void setup() {
Serial.begin(115200);
Serial.println("Hello");
Wire.begin(ag.getI2cSdaPin(), ag.getI2cSclPin());
delay(1000);
if (ag.sht.begin(Wire) == false) {
failedHandler("SHT init failed");
}
}
void loop() {
if (ag.sht.measure()) {
float hum = ag.sht.getRelativeHumidity();
float temp = ag.sht.getTemperature();
Serial.printf("Get temperature: %f\r\n", temp);
Serial.printf(" Get humidity: %f\r\n", hum);
} else {
Serial.println("Measure failed");
}
delay(1000);
}
void failedHandler(String msg) {
while (true) {
Serial.println(msg);
delay(1000);
}
}

View File

@ -1,5 +1,5 @@
name=AirGradient Air Quality Sensor
version=3.0.0
version=3.1.0-beta.1
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

24
platformio.ini Normal file
View File

@ -0,0 +1,24 @@
; 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-devkitm-1]
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
monitor_filters = time
[platformio]
src_dir = examples/OneOpenAir

161
src/AgApiClient.cpp Normal file
View File

@ -0,0 +1,161 @@
#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("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) {
logWarning("Ignore fetch server configuration");
// Clear server configuration failed flag, cause it's ignore but not
// really failed
getConfigFailed = false;
return false;
}
String uri =
"http://hw.airgradient.com/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();
if (retCode != 200) {
client.end();
getConfigFailed = true;
return false;
}
/** clear failed */
getConfigFailed = 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) {
return false;
}
client.addHeader("content-type", "application/json");
int retCode = client.POST(data);
client.end();
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; }
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));
}

40
src/AgApiClient.h Normal file
View File

@ -0,0 +1,40 @@
/**
* @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;
bool getConfigFailed;
bool postToServerFailed;
public:
AgApiClient(Stream &stream, Configuration &config);
~AgApiClient();
void begin(void);
bool fetchServerConfiguration(void);
bool postToServer(String data);
bool isFetchConfigureFailed(void);
bool isPostToServerFailed(void);
void setAirGradient(AirGradient *ag);
bool sendPing(int rssi, int bootCount);
};
#endif /** _AG_API_CLIENT_H_ */

825
src/AgConfigure.cpp Normal file
View File

@ -0,0 +1,825 @@
#include "AgConfigure.h"
#include "EEPROM.h"
#include "Libraries/Arduino_JSON/src/Arduino_JSON.h"
const char *CONFIGURATION_CONTROL_NAME[] = {
[ConfigurationControlLocal] = "local",
[ConfigurationControlCloud] = "cloud",
[ConfigurationControlBoth] = "both"};
const char *LED_BAR_MODE_NAMES[] = {
[LedBarModeOff] = "off",
[LedBarModePm] = "pm",
[LedBarModeCO2] = "co2",
};
static bool jsonTypeInvalid(JSONVar root, String validType) {
String type = JSON.typeof_(root);
if (type == validType || type == "undefined" || type == "unknown" ||
type == "null") {
return false;
}
return true;
}
/**
* @brief Get LedBarMode Name
*
* @param mode LedBarMode value
* @return String
*/
String Configuration::getLedBarModeName(LedBarMode mode) {
if (mode == LedBarModeOff) {
return String(LED_BAR_MODE_NAMES[LedBarModeOff]);
} else if (mode == LedBarModePm) {
return String(LED_BAR_MODE_NAMES[LedBarModePm]);
} else if (mode == LedBarModeCO2) {
return String(LED_BAR_MODE_NAMES[LedBarModeCO2]);
}
return String("unknown");
}
/**
* @brief Save configure to device storage (EEPROM)
*
*/
void Configuration::saveConfig(void) {
config._check = 0;
int len = sizeof(config) - sizeof(config._check);
uint8_t *data = (uint8_t *)&config;
for (int i = 0; i < len; i++) {
config._check += data[i];
}
#ifdef ESP8266
for (int i = 0; i < sizeof(config); i++) {
EEPROM.write(i, data[i]);
}
#else
EEPROM.writeBytes(0, &config, sizeof(config));
#endif
EEPROM.commit();
logInfo("Save Config");
}
void Configuration::loadConfig(void) {
bool readSuccess = false;
#ifdef ESP8266
uint8_t *data = (uint8_t *)&config;
for (int i = 0; i < sizeof(config); i++) {
data[i] = EEPROM.read(i);
}
readSuccess = true;
#else
if (EEPROM.readBytes(0, &config, sizeof(config)) == sizeof(config)) {
readSuccess = true;
}
#endif
if (!readSuccess) {
logError("Load configure failed");
defaultConfig();
} else {
uint32_t sum = 0;
uint8_t *data = (uint8_t *)&config;
int len = sizeof(config) - sizeof(config._check);
for (int i = 0; i < len; i++) {
sum += data[i];
}
if (sum != config._check) {
logError("Configure validate invalid");
defaultConfig();
} else {
/** Correct configuration parameter value. */
bool changed = false;
if ((config.temperatureUnit != 'c') && (config.temperatureUnit != 'f')) {
config.temperatureUnit = 'c';
changed = true;
logError("Temperture unit invalid, set default 'c'");
}
if ((config.useRGBLedBar != (uint8_t)LedBarModeCO2) &&
(config.useRGBLedBar != (uint8_t)LedBarModePm) &&
(config.useRGBLedBar != (uint8_t)LedBarModeOff)) {
config.useRGBLedBar = (uint8_t)LedBarModeCO2;
changed = true;
logError("LedBarMode invalid, set default: co2");
}
if (changed) {
saveConfig();
}
}
}
}
/**
* @brief Set configuration default
*
*/
void Configuration::defaultConfig(void) {
// Default country is null
memset(config.country, 0, sizeof(config.country));
// Default MQTT broker is null.
memset(config.mqttBroker, 0, sizeof(config.mqttBroker));
config.configurationControl = ConfigurationControl::ConfigurationControlBoth;
config.inUSAQI = false; // pmStandard = ugm3
config.inF = false;
config.postDataToAirGradient = true;
config.displayMode = true;
config.useRGBLedBar = LedBarMode::LedBarModeCO2;
config.abcDays = 8;
config.tvocLearningOffset = 12;
config.noxLearningOffset = 12;
config.temperatureUnit = 'c';
saveConfig();
}
/**
* @brief Show configuration as JSON string message over log
*
*/
void Configuration::printConfig(void) { logInfo(toString().c_str()); }
/**
* @brief Construct a new Ag Configure:: Ag Configure object
*
* @param debugLog Serial Stream
*/
Configuration::Configuration(Stream &debugLog)
: PrintLog(debugLog, "Configure") {}
/**
* @brief Destroy the Ag Configure:: Ag Configure object
*
*/
Configuration::~Configuration() {}
/**
* @brief Initialize configuration
*
* @return true Success
* @return false Failure
*/
bool Configuration::begin(void) {
EEPROM.begin(512);
loadConfig();
printConfig();
return true;
}
/**
* @brief Parse JSON configura string to local configure
*
* @param data JSON string data
* @param isLocal true of data got from local, otherwise get from Aigradient
* server
* @return true Success
* @return false Failure
*/
bool Configuration::parse(String data, bool isLocal) {
logInfo("Parse configure: " + data);
JSONVar root = JSON.parse(data);
failedMessage = "";
if (JSON.typeof_(root) == "undefined") {
failedMessage = "JSON invalid";
logError(failedMessage);
return false;
}
logInfo("Parse configure success");
/** Is configuration changed */
bool changed = false;
/** Get ConfigurationControl */
if (isLocal) {
uint8_t configurationControl = config.configurationControl;
if (JSON.typeof_(root["configurationControl"]) == "string") {
String configurationControl = root["configurationControl"];
if (configurationControl !=
String(CONFIGURATION_CONTROL_NAME[config.configurationControl])) {
if (configurationControl ==
String(CONFIGURATION_CONTROL_NAME
[ConfigurationControl::ConfigurationControlLocal])) {
config.configurationControl =
(uint8_t)ConfigurationControl::ConfigurationControlLocal;
changed = true;
} else if (configurationControl ==
String(
CONFIGURATION_CONTROL_NAME
[ConfigurationControl::ConfigurationControlCloud])) {
config.configurationControl =
(uint8_t)ConfigurationControl::ConfigurationControlCloud;
changed = true;
} else if (configurationControl ==
String(
CONFIGURATION_CONTROL_NAME
[ConfigurationControl::ConfigurationControlBoth])) {
config.configurationControl =
(uint8_t)ConfigurationControl::ConfigurationControlBoth;
changed = true;
} else {
failedMessage = jsonValueInvalidMessage("configurationControl",
configurationControl);
jsonInvalid();
return false;
}
}
} else {
if (jsonTypeInvalid(root["configurationControl"], "string")) {
failedMessage =
jsonTypeInvalidMessage("configurationControl", "string");
jsonInvalid();
return false;
}
}
if (changed) {
changed = false;
saveConfig();
configLogInfo(
"configurationControl",
String(CONFIGURATION_CONTROL_NAME[configurationControl]),
String(CONFIGURATION_CONTROL_NAME[config.configurationControl]));
}
if ((config.configurationControl ==
(byte)ConfigurationControl::ConfigurationControlCloud)) {
failedMessage = "Local configure ignored";
jsonInvalid();
return false;
}
} else {
if (config.configurationControl ==
(byte)ConfigurationControl::ConfigurationControlLocal) {
failedMessage = "Cloud configure ignored";
jsonInvalid();
return false;
}
}
char temperatureUnit = 0;
if (JSON.typeof_(root["country"]) == "string") {
String country = root["country"];
if (country.length() == 2) {
if (country != String(config.country)) {
changed = true;
configLogInfo("country", String(config.country), country);
snprintf(config.country, sizeof(config.country), country.c_str());
}
} else {
failedMessage = "Country name " + country +
" invalid. Find details here (ALPHA-2): "
"https://www.iban.com/country-codes";
jsonInvalid();
return false;
}
} else {
if (jsonTypeInvalid(root["country"], "string")) {
failedMessage = jsonTypeInvalidMessage("country", "string");
jsonInvalid();
return false;
}
}
if (JSON.typeof_(root["pmStandard"]) == "string") {
String pmStandard = root["pmStandard"];
bool inUSAQI = true;
if (pmStandard == getPMStandardString(false)) {
inUSAQI = false;
} else if (pmStandard == getPMStandardString(true)) {
inUSAQI = true;
} else {
failedMessage = jsonValueInvalidMessage("pmStandard", pmStandard);
jsonInvalid();
return false;
}
if (inUSAQI != config.inUSAQI) {
configLogInfo("pmStandard", getPMStandardString(config.inUSAQI), pmStandard);
config.inUSAQI = inUSAQI;
changed = true;
}
} else {
if (jsonTypeInvalid(root["pmStandard"], "string")) {
failedMessage = jsonTypeInvalidMessage("pmStandard", "string");
jsonInvalid();
return false;
}
}
if (JSON.typeof_(root["co2CalibrationRequested"]) == "boolean") {
co2CalibrationRequested = root["co2CalibrationRequested"];
if(co2CalibrationRequested) {
logInfo("co2CalibrationRequested: " +
String(co2CalibrationRequested ? "True" : "False"));
}
} else {
if (jsonTypeInvalid(root["co2CalibrationRequested"], "boolean")) {
failedMessage =
jsonTypeInvalidMessage("co2CalibrationRequested", "boolean");
jsonInvalid();
return false;
}
}
if (JSON.typeof_(root["ledBarTestRequested"]) == "boolean") {
ledBarTestRequested = root["ledBarTestRequested"];
if(ledBarTestRequested){
logInfo("ledBarTestRequested: " +
String(ledBarTestRequested ? "True" : "False"));
}
} else {
if (jsonTypeInvalid(root["ledBarTestRequested"], "boolean")) {
failedMessage = jsonTypeInvalidMessage("ledBarTestRequested", "boolean");
jsonInvalid();
return false;
}
}
if (JSON.typeof_(root["ledBarMode"]) == "string") {
String mode = root["ledBarMode"];
uint8_t ledBarMode = LedBarModeOff;
if (mode == String(LED_BAR_MODE_NAMES[LedBarModeCO2])) {
ledBarMode = LedBarModeCO2;
} else if (mode == String(LED_BAR_MODE_NAMES[LedBarModePm])) {
ledBarMode = LedBarModePm;
} else if (mode == String(LED_BAR_MODE_NAMES[LedBarModeOff])) {
ledBarMode = LedBarModeOff;
} else {
failedMessage = jsonValueInvalidMessage("ledBarMode", mode);
jsonInvalid();
return false;
}
if (ledBarMode != config.useRGBLedBar) {
configLogInfo("useRGBLedBar",
String(LED_BAR_MODE_NAMES[config.useRGBLedBar]),
String(LED_BAR_MODE_NAMES[ledBarMode]));
config.useRGBLedBar = ledBarMode;
changed = true;
}
} else {
if (jsonTypeInvalid(root["ledBarMode"], "string")) {
failedMessage = jsonTypeInvalidMessage("ledBarMode", "string");
jsonInvalid();
return false;
}
}
if (JSON.typeof_(root["displayMode"]) == "string") {
String mode = root["displayMode"];
bool displayMode = false;
if (mode == getDisplayModeString(true)) {
displayMode = true;
} else if (mode == getDisplayModeString(false)) {
displayMode = false;
} else {
failedMessage = jsonTypeInvalidMessage("displayMode", mode);
jsonInvalid();
return false;
}
if (displayMode != config.displayMode) {
changed = true;
configLogInfo("displayMode", getDisplayModeString(config.displayMode), mode);
config.displayMode = displayMode;
}
} else {
if (jsonTypeInvalid(root["displayMode"], "string")) {
failedMessage = jsonTypeInvalidMessage("displayMode", "string");
jsonInvalid();
return false;
}
}
if (JSON.typeof_(root["abcDays"]) == "number") {
int abcDays = root["abcDays"];
if (abcDays <= 0) {
abcDays = 0;
}
if (abcDays != config.abcDays) {
logInfo("Set abcDays: " + String(abcDays));
configLogInfo("abcDays", getAbcDayString(config.abcDays),
String(getAbcDayString(abcDays)));
config.abcDays = abcDays;
changed = true;
}
} else {
if (jsonTypeInvalid(root["abcDays"], "number")) {
failedMessage = jsonTypeInvalidMessage("abcDays", "number");
jsonInvalid();
return false;
}
}
_tvocLearningOffsetChanged = false;
if (JSON.typeof_(root["tvocLearningOffset"]) == "number") {
int tvocLearningOffset = root["tvocLearningOffset"];
if (tvocLearningOffset != config.tvocLearningOffset) {
changed = true;
_tvocLearningOffsetChanged = true;
configLogInfo("tvocLearningOffset", String(config.tvocLearningOffset),
String(tvocLearningOffset));
config.tvocLearningOffset = tvocLearningOffset;
}
} else {
if (jsonTypeInvalid(root["tvocLearningOffset"], "number")) {
failedMessage = jsonTypeInvalidMessage("tvocLearningOffset", "number");
jsonInvalid();
return false;
}
}
_noxLearnOffsetChanged = false;
if (JSON.typeof_(root["noxLearningOffset"]) == "number") {
int noxLearningOffset = root["noxLearningOffset"];
if (noxLearningOffset != config.noxLearningOffset) {
changed = true;
_noxLearnOffsetChanged = true;
configLogInfo("noxLearningOffset", String(config.noxLearningOffset),
String(noxLearningOffset));
config.noxLearningOffset = noxLearningOffset;
}
} else {
if (jsonTypeInvalid(root["noxLearningOffset"], "number")) {
failedMessage = jsonTypeInvalidMessage("noxLearningOffset", "number");
jsonInvalid();
return false;
}
}
if (JSON.typeof_(root["mqttBrokerUrl"]) == "string") {
String broker = root["mqttBrokerUrl"];
if (broker.length() < sizeof(config.mqttBroker)) {
if (broker != String(config.mqttBroker)) {
changed = true;
configLogInfo("mqttBrokerUrl", String(config.mqttBroker), broker);
snprintf(config.mqttBroker, sizeof(config.mqttBroker), broker.c_str());
}
} else {
failedMessage =
"'mqttBroker' value length invalid: " + String(broker.length());
jsonInvalid();
return false;
}
} else {
if (jsonTypeInvalid(root["mqttBrokerUrl"], "string")) {
failedMessage = jsonTypeInvalidMessage("mqttBrokerUrl", "string");
jsonInvalid();
return false;
}
}
if (JSON.typeof_(root["temperatureUnit"]) == "string") {
String unit = root["temperatureUnit"];
unit.toLowerCase();
if ((unit == "c") || (unit == "celsius")) {
temperatureUnit = 'c';
} else if ((unit == "f") || (unit == "fahrenheit")) {
temperatureUnit = 'f';
} else {
failedMessage = "'temperatureUnit' value '" + unit + "' invalid";
logError(failedMessage);
return false;
}
} else {
if (jsonTypeInvalid(root["temperatureUnit"], "string")) {
failedMessage = jsonTypeInvalidMessage("temperatureUnit", "string");
jsonInvalid();
return false;
}
}
if (temperatureUnit != 0 && temperatureUnit != config.temperatureUnit) {
changed = true;
configLogInfo("temperatureUnit", String(config.temperatureUnit),
String(temperatureUnit));
config.temperatureUnit = temperatureUnit;
}
if (isLocal) {
if (JSON.typeof_(root["postDataToAirGradient"]) == "boolean") {
bool post = root["postDataToAirGradient"];
if (post != config.postDataToAirGradient) {
changed = true;
configLogInfo("postDataToAirGradient",
String(config.postDataToAirGradient ? "true" : "false"),
String(post ? "true" : "false"));
config.postDataToAirGradient = post;
}
} else {
if (jsonTypeInvalid(root["postDataToAirGradient"], "boolean")) {
failedMessage =
jsonTypeInvalidMessage("postDataToAirGradient", "boolean");
jsonInvalid();
return false;
}
}
}
/** Parse data only got from AirGradient server */
if (isLocal == false) {
if (JSON.typeof_(root["model"]) == "string") {
String model = root["model"];
if (model.length() < sizeof(config.model)) {
if (model != String(config.model)) {
changed = true;
configLogInfo("model", String(config.model), model);
snprintf(config.model, sizeof(config.model), model.c_str());
}
} else {
failedMessage =
"'modal' value length invalid: " + String(model.length());
jsonInvalid();
return false;
}
} else {
if (jsonTypeInvalid(root["model"], "string")) {
failedMessage = jsonTypeInvalidMessage("model", "string");
jsonInvalid();
return false;
}
}
}
if (changed) {
udpated = true;
saveConfig();
printConfig();
} else {
logInfo("Nothing changed ignore udpate");
if (ledBarTestRequested || co2CalibrationRequested) {
udpated = true;
}
}
return true;
}
/**
* @brief Get current configuration value as JSON string
*
* @return String
*/
String Configuration::toString(void) {
JSONVar root;
/** "country" */
root["country"] = String(config.country);
/** "pmStandard" */
root["pmStandard"] = getPMStandardString(config.inUSAQI);
/** co2CalibrationRequested */
/** ledBarTestRequested */
/** "ledBarMode" */
root["ledBarMode"] = getLedBarModeName();
/** "displayMode" */
if (config.displayMode) {
root["displayMode"] = "on";
} else {
root["displayMode"] = "off";
}
/** "abcDays" */
root["abcDays"] = config.abcDays;
/** "tvocLearningOffset" */
root["tvocLearningOffset"] = config.tvocLearningOffset;
/** "noxLearningOffset" */
root["noxLearningOffset"] = config.noxLearningOffset;
/** "mqttBrokerUrl" */
root["mqttBrokerUrl"] = String(config.mqttBroker);
/** "temperatureUnit" */
root["temperatureUnit"] = String(config.temperatureUnit);
/** configurationControl */
root["configurationControl"] =
String(CONFIGURATION_CONTROL_NAME[config.configurationControl]);
/** "postDataToAirGradient" */
root["postDataToAirGradient"] = config.postDataToAirGradient;
return JSON.stringify(root);
}
/**
* @brief Temperature unit (F or C)
*
* @return true F
* @return false C
*/
bool Configuration::isTemperatureUnitInF(void) {
return (config.temperatureUnit == 'f');
}
/**
* @brief Country name, it's short name ex: TH = Thailand
*
* @return String
*/
String Configuration::getCountry(void) { return String(config.country); }
/**
* @brief PM unit standard (USAQI, ugm3)
*
* @return true USAQI
* @return false ugm3
*/
bool Configuration::isPmStandardInUSAQI(void) { return config.inUSAQI; }
/**
* @brief Get CO2 calibration ABC time
*
* @return int Number of day
*/
int Configuration::getCO2CalibrationAbcDays(void) { return config.abcDays; }
/**
* @brief Get Led Bar Mode
*
* @return LedBarMode
*/
LedBarMode Configuration::getLedBarMode(void) {
return (LedBarMode)config.useRGBLedBar;
}
/**
* @brief Get LED bar mode name
*
* @return String
*/
String Configuration::getLedBarModeName(void) {
return getLedBarModeName((LedBarMode)config.useRGBLedBar);
}
/**
* @brief Get display mode
*
* @return true On
* @return false Off
*/
bool Configuration::getDisplayMode(void) { return config.displayMode; }
/**
* @brief Get MQTT uri
*
* @return String
*/
String Configuration::getMqttBrokerUri(void) {
return String(config.mqttBroker);
}
/**
* @brief Get configuratoin post data to AirGradient cloud
*
* @return true Post
* @return false No-Post
*/
bool Configuration::isPostDataToAirGradient(void) {
return config.postDataToAirGradient;
}
/**
* @brief Get current configuration control
*
* @return ConfigurationControl
*/
ConfigurationControl Configuration::getConfigurationControl(void) {
return (ConfigurationControl)config.configurationControl;
}
/**
* @brief CO2 manual calib request, the request flag will clear after get. Must
* call this after parse success
*
* @return true Requested
* @return false Not requested
*/
bool Configuration::isCo2CalibrationRequested(void) {
bool requested = co2CalibrationRequested;
co2CalibrationRequested = false; // clear requested
return requested;
}
/**
* @brief LED bar test request, the request flag will clear after get. Must call
* this function after parse success
*
* @return true Requested
* @return false Not requested
*/
bool Configuration::isLedBarTestRequested(void) {
bool requested = ledBarTestRequested;
ledBarTestRequested = false;
return requested;
}
/**
* @brief Reset default configure
*/
void Configuration::reset(void) {
defaultConfig();
logInfo("Reset to default configure");
printConfig();
}
/**
* @brief Get model name, it's usage for offline mode
*
* @return String
*/
String Configuration::getModel(void) { return String(config.model); }
bool Configuration::isUpdated(void) {
bool updated = this->udpated;
this->udpated = false;
return updated;
}
String Configuration::jsonTypeInvalidMessage(String name, String type) {
return "'" + name + "' type invalid, it's should '" + type + "'";
}
String Configuration::jsonValueInvalidMessage(String name, String value) {
return "'" + name + "' value '" + value + "' invalid";
}
void Configuration::jsonInvalid(void) {
loadConfig();
logError(failedMessage);
}
void Configuration::configLogInfo(String name, String fromValue,
String toValue) {
logInfo(String("Setting '") + name + String("' from '") + fromValue +
String("' to '") + toValue + String("'"));
}
String Configuration::getPMStandardString(bool usaqi) {
if (usaqi) {
return "us-aqi";
}
return "ugm3";
}
String Configuration::getDisplayModeString(bool dispMode) {
if(dispMode){
return String("on");
}
return String("off");
}
String Configuration::getAbcDayString(int value) {
if(value <= 0){
return String("off");
}
return String(value);
}
String Configuration::getFailedMesage(void) { return failedMessage; }
void Configuration::setPostToAirGradient(bool enable) {
if (enable != config.postDataToAirGradient) {
config.postDataToAirGradient = enable;
logInfo("postDataToAirGradient set to: " + String(enable));
saveConfig();
} else {
logInfo("postDataToAirGradient: Ignored set to " + String(enable));
}
}
bool Configuration::noxLearnOffsetChanged(void) {
bool changed = _noxLearnOffsetChanged;
_noxLearnOffsetChanged = false;
return changed;
}
bool Configuration::tvocLearnOffsetChanged(void) {
bool changed = _tvocLearningOffsetChanged;
_tvocLearningOffsetChanged = false;
return changed;
}
int Configuration::getTvocLearningOffset(void) {
return config.tvocLearningOffset;
}
int Configuration::getNoxLearningOffset(void) {
return config.noxLearningOffset;
}
String Configuration::wifiSSID(void) {
return "airgradient-" + ag->deviceId();
}
String Configuration::wifiPass(void) { return String("cleanair"); }
void Configuration::setAirGradient(AirGradient *ag) { this->ag = ag;}

94
src/AgConfigure.h Normal file
View File

@ -0,0 +1,94 @@
#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:
struct Config {
char model[20];
char country[3]; /** Country name has only 2 character, ex: TH = Thailand */
char mqttBroker[256]; /** MQTT broker URI */
bool inUSAQI; /** If PM standard "ugm3" inUSAQI = false, otherwise is true
*/
bool inF; /** Temperature unit F */
bool postDataToAirGradient; /** If true, monitor will not POST data to
airgradient server. Make sure no error
message shown on monitor */
uint8_t configurationControl; /** If true, configuration from airgradient
server will be ignored */
bool displayMode; /** true if enable display */
uint8_t useRGBLedBar;
uint8_t abcDays;
int tvocLearningOffset;
int noxLearningOffset;
char temperatureUnit; // 'f' or 'c'
uint32_t _check;
};
struct Config config;
bool co2CalibrationRequested;
bool ledBarTestRequested;
bool udpated;
String failedMessage;
bool _noxLearnOffsetChanged;
bool _tvocLearningOffsetChanged;
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 getDisplayModeString(bool dispMode);
String getAbcDayString(int value);
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);
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);
};
#endif /** _AG_CONFIG_H_ */

310
src/AgOledDisplay.cpp Normal file
View File

@ -0,0 +1,310 @@
#include "AgOledDisplay.h"
#include "Libraries/U8g2/src/U8g2lib.h"
#include "Libraries/QRCode/src/qrcode.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[10];
if (value.Temperature > -1001) {
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 (value.Humidity >= 0) {
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);
}
}
/**
* @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;
}
/** 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;
}
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;
}
/** Free u8g2 */
delete DISP();
u8g2 = NULL;
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) {
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());
}
/**
* @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) {
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());
}
/**
* @brief Update dashboard content
*
*/
void OledDisplay::showDashboard(void) { showDashboard(NULL); }
/**
* @brief Update dashboard content and error status
*
*/
void OledDisplay::showDashboard(const char *status) {
char strBuf[10];
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 (value.CO2 > 0) {
int val = 9999;
if (value.CO2 < 10000) {
val = value.CO2;
}
sprintf(strBuf, "%d", val);
} 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(45, 14, 45, 64);
DISP()->drawLine(82, 14, 82, 64);
/** Draw PM2.5 label */
DISP()->setFont(u8g2_font_t0_12_tf);
DISP()->drawStr(48, 27, "PM2.5");
/** Draw PM2.5 value */
DISP()->setFont(u8g2_font_t0_22b_tf);
if (config.isPmStandardInUSAQI()) {
if (value.pm25_1 >= 0) {
sprintf(strBuf, "%d", ag->pms5003.convertPm25ToUsAqi(value.pm25_1));
} else {
sprintf(strBuf, "%s", "-");
}
DISP()->drawStr(48, 48, strBuf);
DISP()->setFont(u8g2_font_t0_12_tf);
DISP()->drawUTF8(48, 61, "AQI");
} else {
if (value.pm25_1 >= 0) {
sprintf(strBuf, "%d", value.pm25_1);
} else {
sprintf(strBuf, "%s", "-");
}
DISP()->drawStr(48, 48, strBuf);
DISP()->setFont(u8g2_font_t0_12_tf);
DISP()->drawUTF8(48, 61, "ug/m³");
}
/** Draw tvocIndexlabel */
DISP()->setFont(u8g2_font_t0_12_tf);
DISP()->drawStr(85, 27, "tvoc:");
/** Draw tvocIndexvalue */
if (value.TVOC >= 0) {
sprintf(strBuf, "%d", value.TVOC);
} else {
sprintf(strBuf, "%s", "-");
}
DISP()->drawStr(85, 39, strBuf);
/** Draw NOx label */
DISP()->drawStr(85, 53, "NOx:");
if (value.NOx >= 0) {
sprintf(strBuf, "%d", value.NOx);
} else {
sprintf(strBuf, "%s", "-");
}
DISP()->drawStr(85, 63, strBuf);
} while (DISP()->nextPage());
}
void OledDisplay::showWiFiQrCode(String content, String label) {
QRCode qrcode;
int version = 6;
int x_start = (DISP()->getWidth() - (version * 4 + 17))/ 2;
uint8_t qrcodeData[qrcode_getBufferSize(version)];
qrcode_initText(&qrcode, qrcodeData, version, 0, content.c_str());
DISP()->firstPage();
do {
for (uint8_t y = 0; y < qrcode.size; y++) {
for (uint8_t x = 0; x < qrcode.size; x++) {
if (qrcode_getModule(&qrcode, x, y)) {
DISP()->drawPixel(x + x_start, y);
}
}
}
DISP()->setFont(u8g2_font_t0_16_tf);
x_start = (DISP()->getWidth() - DISP()->getStrWidth(label.c_str()))/2;
DISP()->drawStr(x_start, 60, label.c_str());
} while (DISP()->nextPage());
}

37
src/AgOledDisplay.h Normal file
View File

@ -0,0 +1,37 @@
#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;
void showTempHum(bool hasStatus);
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 showWiFiQrCode(String content, String label);
};
#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_ */

761
src/AgStateMachine.cpp Normal file
View File

@ -0,0 +1,761 @@
#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 */
/**
* @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 <= 400) {
/** G; 1 */
ag->ledBar.setColor(0, 255, 0, ag->ledBar.getNumberOfLeds() - 1);
} else if (co2Value <= 700) {
/** GG; 2 */
ag->ledBar.setColor(0, 255, 0, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(0, 255, 0, ag->ledBar.getNumberOfLeds() - 2);
} else if (co2Value <= 1000) {
/** YYY; 3 */
ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 3);
} else if (co2Value <= 1333) {
/** YYYY; 4 */
ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 4);
} else if (co2Value <= 1666) {
/** YYYYY; 5 */
ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 5);
} else if (co2Value <= 2000) {
/** RRRRRR; 6 */
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 6);
} else if (co2Value <= 2666) {
/** RRRRRRR; 7 */
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 6);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 7);
} else if (co2Value <= 3333) {
/** RRRRRRRR; 8 */
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 6);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 7);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 8);
} else if (co2Value <= 4000) {
/** RRRRRRRRR; 9 */
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 6);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 7);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 8);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 9);
} else { /** > 4000 */
/* PRPRPRPRP; 9 */
ag->ledBar.setColor(153, 153, 0, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(153, 153, 0, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(153, 153, 0, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 6);
ag->ledBar.setColor(153, 153, 0, ag->ledBar.getNumberOfLeds() - 7);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 8);
ag->ledBar.setColor(153, 153, 0, 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(0, 255, 0, ag->ledBar.getNumberOfLeds() - 1);
} else if (pm25Value <= 10) {
/** GG; 2 */
ag->ledBar.setColor(0, 255, 0, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(0, 255, 0, ag->ledBar.getNumberOfLeds() - 2);
} else if (pm25Value <= 20) {
/** YYY; 3 */
ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 3);
} else if (pm25Value <= 35) {
/** YYYY; 4 */
ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 4);
} else if (pm25Value <= 45) {
/** YYYYY; 5 */
ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(255, 255, 0, ag->ledBar.getNumberOfLeds() - 5);
} else if (pm25Value <= 55) {
/** RRRRRR; 6 */
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 6);
} else if (pm25Value <= 65) {
/** RRRRRRR; 7 */
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 6);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 7);
} else if (pm25Value <= 150) {
/** RRRRRRRR; 8 */
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 6);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 7);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 8);
} else if (pm25Value <= 250) {
/** RRRRRRRRR; 9 */
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 6);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 7);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 8);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 9);
} else { /** > 250 */
/* PRPRPRPRP; 9 */
ag->ledBar.setColor(153, 153, 0, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(153, 153, 0, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(153, 153, 0, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 6);
ag->ledBar.setColor(153, 153, 0, ag->ledBar.getNumberOfLeds() - 7);
ag->ledBar.setColor(255, 0, 0, ag->ledBar.getNumberOfLeds() - 8);
ag->ledBar.setColor(153, 153, 0, 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()) {
String str =
"after " + String(SENSOR_CO2_CALIB_COUNTDOWN_MAX - i) + " sec";
disp.setText("Start CO2 calib", 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()) {
disp.setText("Calibration", "success", "");
} else {
logInfo("CO2 Calibration: success");
}
delay(1000);
if (ag->isOne()) {
disp.setText("Wait for", "calib finish", "...");
} 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()) {
String str = "after " + String(count);
disp.setText("Calib finish", str.c_str(), "sec");
} else {
logInfo("CO2 Calibration: finish after " + String(count) + " sec");
}
delay(2000);
} else {
if (ag->isOne()) {
disp.setText("Calibration", "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::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 ONE_INDOOR board
if (!ag->isOne()) {
return;
}
if (state > AgStateMachineNormal) {
logError("displayHandle: State invalid");
return;
}
dispState = state;
switch (state) {
case AgStateMachineWiFiManagerMode:
case AgStateMachineWiFiManagerPortalActive: {
// if (wifiConnectCountDown >= 0) {
// 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--;
// }
if (wifiConnectCountDown >= 0) {
String qrContent = "WIFI:S:" + config.wifiSSID() +
";T:WPA;P:" + config.wifiPass() + ";;";
String label = "Scan me (" + String(wifiConnectCountDown) + String(")");
disp.showWiFiQrCode(qrContent, label);
wifiConnectCountDown--;
}
break;
}
case AgStateMachineWiFiManagerStaConnecting: {
disp.setText("Trying to", "connect to WiFi", "...");
break;
}
case AgStateMachineWiFiManagerStaConnected: {
disp.setText("WiFi connection", "successful", "");
break;
}
case AgStateMachineWiFiOkServerConnecting: {
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: {
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: {
uint32_t ms = (uint32_t)(millis() - addToDashboardTime);
if (ms >= 5000) {
addToDashboardTime = millis();
if (addToDashBoard) {
disp.showDashboard("Add to Dashboard");
} else {
disp.showDashboard(ag->deviceId().c_str());
}
addToDashBoard = !addToDashBoard;
}
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) {
addToDashBoard = true;
addToDashboardTime = millis();
}
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
*
* @param state
*/
void StateMachine::handleLeds(AgStateMachineState state) {
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 midle 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;
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);
}

54
src/AgStateMachine.h Normal file
View File

@ -0,0 +1,54 @@
#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;
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 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);
};
#endif /** _AG_STATE_MACHINE_H_ */

184
src/AgValue.cpp Normal file
View File

@ -0,0 +1,184 @@
#include "AgValue.h"
#include "AgConfigure.h"
#include "AirGradient.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) {
if (this->CO2 >= 0) {
root["rco2"] = this->CO2;
}
}
if (ag->isOne()) {
if (config->hasSensorPMS1) {
if (this->pm01_1 >= 0) {
root["pm01"] = this->pm01_1;
}
if (this->pm25_1 >= 0) {
root["pm02"] = this->pm25_1;
}
if (this->pm10_1 >= 0) {
root["pm10"] = this->pm10_1;
}
if (this->pm03PCount_1 >= 0) {
root["pm003Count"] = this->pm03PCount_1;
}
}
if (config->hasSensorSHT) {
if (this->Temperature > -1001) {
root["atmp"] = ag->round2(this->Temperature);
if (localServer) {
root["atmpCompensated"] = ag->round2(this->Temperature);
}
}
if (this->Humidity >= 0) {
root["rhum"] = this->Humidity;
if (localServer) {
root["rhumCompensated"] = this->Humidity;
}
}
}
} else {
if (config->hasSensorPMS1 && config->hasSensorPMS2) {
root["pm01"] = ag->round2((this->pm01_1 + this->pm01_2) / 2.0);
root["pm02"] = ag->round2((this->pm25_1 + this->pm25_2) / 2.0);
root["pm10"] = ag->round2((this->pm10_1 + this->pm10_2) / 2.0);
root["pm003Count"] =
ag->round2((this->pm03PCount_1 + this->pm03PCount_2) / 2.0);
root["atmp"] = ag->round2((this->temp_1 + this->temp_2) / 2.0f);
root["rhum"] = ag->round2((this->hum_1 + this->hum_2) / 2.0f);
if (localServer) {
root["atmpCompensated"] =
ag->round2(ag->pms5003t_2.temperatureCompensated(
(this->temp_1 + this->temp_2) / 2.0f));
root["rhumCompensated"] = (int)ag->pms5003t_2.humidityCompensated(
(this->hum_1 + this->hum_2) / 2.0f);
}
}
if (fwMode == FW_MODE_O_1PS || fwMode == FW_MODE_O_1PST) {
if (config->hasSensorPMS1) {
root["pm01"] = this->pm01_1;
root["pm02"] = this->pm25_1;
root["pm10"] = this->pm10_1;
root["pm003Count"] = this->pm03PCount_1;
root["atmp"] = ag->round2(this->temp_1);
root["rhum"] = this->hum_1;
if (localServer) {
root["atmpCompensated"] =
ag->round2(ag->pms5003t_1.temperatureCompensated(this->temp_1));
root["rhumCompensated"] =
(int)ag->pms5003t_1.humidityCompensated(this->hum_1);
}
}
if (config->hasSensorPMS2) {
root["pm01"] = this->pm01_2;
root["pm02"] = this->pm25_2;
root["pm10"] = this->pm10_2;
root["pm003Count"] = this->pm03PCount_2;
root["atmp"] = ag->round2(this->temp_2);
root["rhum"] = this->hum_2;
if (localServer) {
root["atmpCompensated"] =
ag->round2(ag->pms5003t_2.temperatureCompensated(this->temp_2));
root["rhumCompensated"] =
(int)ag->pms5003t_2.humidityCompensated(this->hum_2);
}
}
} else {
if (fwMode == FW_MODE_O_1P) {
if (config->hasSensorPMS1) {
root["pm01"] = this->pm01_1;
root["pm02"] = this->pm25_1;
root["pm10"] = this->pm10_1;
root["pm003Count"] = this->pm03PCount_1;
root["atmp"] = ag->round2(this->temp_1);
root["rhum"] = this->hum_1;
if (localServer) {
root["atmpCompensated"] =
ag->round2(ag->pms5003t_1.temperatureCompensated(this->temp_1));
root["rhumCompensated"] =
(int)ag->pms5003t_1.humidityCompensated(this->hum_1);
}
} else if (config->hasSensorPMS2) {
root["pm01"] = this->pm01_2;
root["pm02"] = this->pm25_2;
root["pm10"] = this->pm10_2;
root["pm003Count"] = this->pm03PCount_2;
root["atmp"] = ag->round2(this->temp_2);
root["rhum"] = this->hum_2;
if (localServer) {
root["atmpCompensated"] =
ag->round2(ag->pms5003t_1.temperatureCompensated(this->temp_2));
root["rhumCompensated"] =
(int)ag->pms5003t_1.humidityCompensated(this->hum_2);
}
}
} else {
if (config->hasSensorPMS1) {
root["channels"]["1"]["pm01"] = this->pm01_1;
root["channels"]["1"]["pm02"] = this->pm25_1;
root["channels"]["1"]["pm10"] = this->pm10_1;
root["channels"]["1"]["pm003Count"] = this->pm03PCount_1;
root["channels"]["1"]["atmp"] = ag->round2(this->temp_1);
root["channels"]["1"]["rhum"] = this->hum_1;
if (localServer) {
root["channels"]["1"]["atmpCompensated"] =
ag->round2(ag->pms5003t_1.temperatureCompensated(this->temp_1));
root["channels"]["1"]["rhumCompensated"] =
(int)ag->pms5003t_1.humidityCompensated(this->hum_1);
}
} else if (config->hasSensorPMS2) {
root["channels"]["2"]["pm01"] = this->pm01_2;
root["channels"]["2"]["pm02"] = this->pm25_2;
root["channels"]["2"]["pm10"] = this->pm10_2;
root["channels"]["2"]["pm003Count"] = this->pm03PCount_2;
root["channels"]["2"]["atmp"] = ag->round2(this->temp_2);
root["channels"]["2"]["rhum"] = this->hum_2;
if (localServer) {
root["channels"]["2"]["atmpCompensated"] =
ag->round2(ag->pms5003t_1.temperatureCompensated(this->temp_2));
root["channels"]["2"]["rhumCompensated"] =
(int)ag->pms5003t_1.humidityCompensated(this->hum_2);
}
}
}
}
}
if (config->hasSensorSGP) {
if (this->TVOC >= 0) {
root["tvocIndex"] = this->TVOC;
}
if (this->TVOCRaw >= 0) {
root["tvocRaw"] = this->TVOCRaw;
}
if (this->NOx >= 0) {
root["noxIndex"] = this->NOx;
}
if (this->NOxRaw >= 0) {
root["noxRaw"] = this->NOxRaw;
}
}
root["bootCount"] = bootCount;
if (localServer) {
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_ */

341
src/AgWiFiConnector.cpp Normal file
View File

@ -0,0 +1,341 @@
#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; }
#ifdef ESP32
/**
* @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) {}
#else
WifiConnector::WifiConnector(Stream &log) : PrintLog(log, "WiFiConnector") {}
#endif
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);
#ifdef ESP32
WIFI()->setAPCallback([this](WiFiManager *obj) { _wifiApCallback(); });
WIFI()->setSaveConfigCallback([this]() { _wifiSaveConfig(); });
WIFI()->setSaveParamsCallback([this]() { _wifiSaveParamCallback(); });
if (ag->isOne()) {
disp.setText("Connecting to", "WiFi", "...");
} else {
logInfo("Connecting to WiFi...");
}
ssid = "airgradient-" + ag->deviceId();
#else
ssid = "AG-" + String(ESP.getChipId(), HEX);
#endif
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);
#ifdef ESP32
// Task handle WiFi connection.
xTaskCreate(
[](void *obj) {
WifiConnector *connector = (WifiConnector *)obj;
while (connector->_wifiConfigPortalActive()) {
connector->_wifiProcess();
}
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.
}
/** Show display wifi connect result failed */
if (WiFi.isConnected() == false) {
sm.handleLeds(AgStateMachineWiFiManagerConnectFailed);
if (ag->isOne()) {
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;
}
#else
_wifiProcess();
#endif
return true;
}
/**
* @brief Disconnect to current connected WiFi AP
*
*/
void WifiConnector::disconnect(void) {
if (WiFi.isConnected()) {
logInfo("Disconnect");
WiFi.disconnect();
}
}
#ifdef ESP32
#else
void WifiConnector::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();
delay(100);
}
#endif
/**
* @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;
}
#ifdef ESP32
/**
* @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();
}
#endif
/**
* @brief Process WiFiManager connection
*
*/
void WifiConnector::_wifiProcess() {
#ifdef ESP32
WIFI()->process();
#else
int count = WIFI_CONNECT_COUNTDOWN_MAX;
displayShowText(String(WIFI_CONNECT_COUNTDOWN_MAX) + " sec", "SSID:", ssid);
while (WIFI()->getConfigPortalActive()) {
WIFI()->process();
uint32_t lastTime = millis();
uint32_t ms = (uint32_t)(millis() - lastTime);
if (ms >= 1000) {
lastTime = millis();
displayShowText(String(count) + " sec", "SSID:", ssid);
count--;
// Timeout
if (count == 0) {
break;
}
}
}
if (!WiFi.isConnected()) {
displayShowText("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();
// Serial.printf("Re-Connect WiFi\r\n");
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) { 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(); }

55
src/AgWiFiConnector.h Normal file
View File

@ -0,0 +1,55 @@
#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;
#ifdef ESP32
OledDisplay &disp;
StateMachine &sm;
Configuration &config;
#else
void displayShowText(String ln1, String ln2, String ln3);
#endif
String ssid;
void *wifi = NULL;
bool hasConfig;
uint32_t lastRetry;
bool hasPortalConfig = false;
bool wifiClientConnected(void);
public:
void setAirGradient(AirGradient *ag);
#ifdef ESP32
WifiConnector(OledDisplay &disp, Stream &log, StateMachine &sm, Configuration& config);
#else
WifiConnector(Stream &log);
#endif
~WifiConnector();
bool connect(void);
void disconnect(void);
void handle(void);
#ifdef ESP32
void _wifiApCallback(void);
void _wifiSaveConfig(void);
void _wifiSaveParamCallback(void);
bool _wifiConfigPortalActive(void);
#endif
void _wifiProcess();
bool isConnected(void);
void reset(void);
int RSSI(void);
String localIpStr(void);
};
#endif /** _AG_WIFI_CONNECTOR_H_ */

View File

@ -1,11 +1,14 @@
#include "AirGradient.h"
#define AG_LIB_VER "3.0.0"
#ifdef ESP8266
#include <ESP8266WiFi.h>
#else
#include "WiFi.h"
#endif
AirGradient::AirGradient(BoardType type)
: pms5003(type), pms5003t_1(type), pms5003t_2(type), s8(type), sht(type), sgp41(type),
: pms5003(type), pms5003t_1(type), pms5003t_2(type), s8(type), sgp41(type),
display(type), boardType(type), button(type), statusLed(type),
ledBar(type), watchdog(type) {}
ledBar(type), watchdog(type), sht(type) {}
/**
* @brief Get pin number for I2C SDA
@ -33,6 +36,31 @@ 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) {
return (int)(value * 100 + 0.5) / 100.0;
}
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;
}
String AirGradient::deviceId(void) {
String mac = WiFi.macAddress();
mac.replace(":", "");
mac.toLowerCase();
return mac;
}

View File

@ -1,18 +1,26 @@
#ifndef _AIR_GRADIENT_H_
#define _AIR_GRADIENT_H_
#include "bsp/BoardDef.h"
#include "bsp/LedBar.h"
#include "bsp/PushButton.h"
#include "bsp/StatusLed.h"
#include "bsp/HardwareWatchdog.h"
#include "co2/s8.h"
#include "display/oled.h"
#include "pm/pms5003.h"
#include "pm/pms5003t.h"
#include "sgp/sgp41.h"
#include "sht/sht4x.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"
#ifndef GIT_VERSION
#define GIT_VERSION "snapshot"
#endif
/**
* @brief Class with define all the sensor has supported by Airgradient. Each
* sensor usage must be init before use.
*/
class AirGradient {
public:
AirGradient(BoardType type);
@ -21,7 +29,15 @@ public:
* @brief Plantower PMS5003 sensor
*/
PMS5003 pms5003;
/**
* @brief Plantower PMS5003T sensor: connect to PM1 connector on
* OPEN_AIR_OUTDOOR.
*/
PMS5003T pms5003t_1;
/**
* @brief Plantower PMS5003T sensor: connect to PM2 connector on
* OPEN_AIR_OUTDOOR.
*/
PMS5003T pms5003t_2;
/**
@ -30,18 +46,19 @@ public:
S8 s8;
/**
* @brief Temperature and humidity sensor
* @brief Temperature and humidity sensor supported SHT3x and SHT4x
*
*/
Sht sht;
/**
* @brief TVOC and NOx sensor
* @brief SGP41 TVOC and NOx sensor
*
*/
Sgp41 sgp41;
/**
* @brief Display
* @brief OLED Display
*
*/
Display display;
@ -55,20 +72,75 @@ public:
* @brief LED
*/
StatusLed statusLed;
/**
* @brief RGB LED array
*
*/
LedBar ledBar;
/**
* @brief Hardware watchdog
* @brief External hardware watchdog
*/
HardwareWatchdog watchdog;
/**
* @brief Get I2C SDA pin has of board supported
*
* @return int Pin number if -1 invalid
*/
int getI2cSdaPin(void);
/**
* @brief Get I2C SCL pin has of board supported
*
* @return int Pin number if -1 invalid
*/
int getI2cSclPin(void);
/**
* @brief Get the Board Type
*
* @return BoardType @ref BoardType
*/
BoardType getBoardType(void);
/**
* @brief Get the library version string
*
* @return String
*/
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 Get device Id
*
* @return String
*/
String deviceId(void);
private:
BoardType boardType;
};

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

@ -0,0 +1,21 @@
#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";
default:
break;
}
return "UNKNOWN";
}

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

@ -0,0 +1,103 @@
#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,
/** 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 */
};
const char *AgFirmwareModeName(AgFirmwareMode mode);
#endif /** _APP_DEF_H_ */

282
src/Display/Display.cpp Normal file
View File

@ -0,0 +1,282 @@
#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) { \
((Adafruit_SSD1306 *)(this->oled))->func; \
} else { \
((Adafruit_SH110X *)(this->oled))->func; \
}
#if defined(ESP8266)
void Display::begin(TwoWire &wire, Stream &debugStream) {
this->_debugStream = &debugStream;
this->begin(wire);
}
#else
#endif
Display::Display(BoardType type) : _boardType(type) {}
/**
* @brief Initialize display, should be call this function before call of ther,
* if not it's always return failure.
*
* @param wire TwoWire instance, Must be initialized
*/
void Display::begin(TwoWire &wire) {
if (_isBegin) {
AgLog("Initialized, call end() then try again");
return;
}
this->_bsp = getBoardDef(this->_boardType);
if ((this->_bsp == nullptr) || (this->_bsp->I2C.supported == false) ||
(this->_bsp->OLED.supported == false)) {
AgLog("Init failed: board not supported");
return;
}
/** Init OLED */
if (this->_boardType == DIY_BASIC) {
AgLog("Init Adafruit_SSD1306");
Adafruit_SSD1306 *_oled = new Adafruit_SSD1306();
_oled->begin(wire, SSD1306_SWITCHCAPVCC, this->_bsp->OLED.addr);
this->oled = _oled;
} else {
AgLog("Init Adafruit_SH1106G");
Adafruit_SH1106G *_oled = new Adafruit_SH1106G(
this->_bsp->OLED.width, this->_bsp->OLED.height, &wire);
_oled->begin(this->_bsp->OLED.addr, false);
this->oled = _oled;
}
this->_isBegin = true;
disp(clearDisplay());
AgLog("Initialize");
}
/**
* @brief Clear display buffer
*
*/
void Display::clear(void) {
if (this->isBegin() == false) {
return;
}
disp(clearDisplay());
}
/**
* @brief Invert display color
*
* @param i 0: black, other is white
*/
void Display::invertDisplay(uint8_t i) {
if (this->isBegin() == false) {
return;
}
disp(invertDisplay(i));
}
/**
* @brief Send display frame buffer to OLED
*
*/
void Display::show() {
if (this->isBegin() == false) {
return;
}
disp(display());
}
/**
* @brief Set display contract
*
* @param value Contract (0;255);
*/
void Display::setContrast(uint8_t value) {
if (this->isBegin() == false) {
return;
}
disp(setContrast(value));
}
/**
* @brief Draw pixel into display frame buffer, call show to draw to
* display(OLED)
*
* @param x X Position
* @param y Y Position
* @param color Color (0: black, other white)
*/
void Display::drawPixel(int16_t x, int16_t y, uint16_t color) {
if (this->isBegin() == false) {
return;
}
disp(drawPixel(x, y, color));
}
/**
* @brief Set text size, it's scale default font instead of point to multiple
* font has define for special size
*
* @param size Size of text (default = 1)
*/
void Display::setTextSize(int size) {
if (this->isBegin() == false) {
return;
}
disp(setTextSize(size));
}
/**
* @brief Move draw cursor into new position
*
* @param x X Position
* @param y Y Position
*/
void Display::setCursor(int16_t x, int16_t y) {
if (this->isBegin() == false) {
return;
}
disp(setCursor(x, y));
}
/**
* @brief Set Text Color
*
* @param color 0:black, 1: While
*/
void Display::setTextColor(uint16_t color) {
if (this->isBegin() == false) {
return;
}
disp(setTextColor(color));
}
/**
* @brief Set text foreground color and background color
*
* @param foreGroundColor Text Color (foreground color)
* @param backGroundColor Text background color
*/
void Display::setTextColor(uint16_t foreGroundColor, uint16_t backGroundColor) {
if (this->isBegin() == false) {
return;
}
disp(setTextColor(foreGroundColor, backGroundColor));
}
/**
* @brief Draw text to display framebuffer, call show() to draw to display
* (OLED)
*
* @param text String
*/
void Display::setText(String text) {
if (this->isBegin() == false) {
return;
}
disp(print(text));
}
/**
* @brief Draw bitmap into display framebuffer, call show() to draw to display
* (OLED)
*
* @param x X Position
* @param y Y Position
* @param bitmap Bitmap buffer
* @param w Bitmap width
* @param h Bitmap hight
* @param color Bitmap color
*/
void Display::drawBitmap(int16_t x, int16_t y, const uint8_t bitmap[],
int16_t w, int16_t h, uint16_t color) {
if (this->isBegin() == false) {
return;
}
disp(drawBitmap(x, y, bitmap, w, h, color));
}
/**
* @brief Set text to display framebuffer, call show() to draw into to display
* (OLED)
*
* @param text Character buffer
*/
void Display::setText(const char text[]) {
if (this->isBegin() == false) {
return;
}
disp(print(text));
}
/**
* @brief Draw line to display framebuffer, call show() to draw to
* display(OLED)
*
* @param x0 Start X position
* @param y0 Start Y position
* @param x1 End X Position
* @param y1 End Y Position
* @param color Color (0: black, otherwise white)
*/
void Display::drawLine(int x0, int y0, int x1, int y1, uint16_t color) {
if (this->isBegin() == false) {
return;
}
disp(drawLine(x0, y0, x1, y1, color));
}
/**
* @brief Draw circle to display framebuffer,
*
* @param x
* @param y
* @param r
* @param color
*/
void Display::drawCircle(int x, int y, int r, uint16_t color) {
if (this->isBegin() == false) {
return;
}
disp(drawCircle(x, y, r, color));
}
void Display::drawRect(int x0, int y0, int x1, int y1, uint16_t color) {
if (this->isBegin() == false) {
return;
}
disp(drawRect(x0, y0, x1, y1, color));
}
bool Display::isBegin(void) {
if (this->_isBegin) {
return true;
}
AgLog("Display not-initialized");
return false;
}
void Display::setRotation(uint8_t r) {
if (isBegin() == false) {
return;
}
disp(setRotation(r));
}
void Display::end(void) {
if (this->_isBegin == false) {
return;
}
_isBegin = false;
if (this->_boardType == DIY_BASIC) {
delete ((Adafruit_SSD1306 *)(this->oled));
} else {
delete ((Adafruit_SH110X *)(this->oled));
}
AgLog("De-initialize");
}

View File

@ -1,10 +1,14 @@
#ifndef _AIR_GRADIENT_OLED_H_
#define _AIR_GRADIENT_OLED_H_
#include "../bsp/BoardDef.h"
#include "../Main/BoardDef.h"
#include <Arduino.h>
#include <Wire.h>
/**
* @brief The class define how to handle the OLED display on Airgradient has
* attached or support OLED display like: ONE-V9, Basic-V4
*/
class Display {
public:
const uint16_t COLOR_WHILTE = 1;
@ -15,10 +19,11 @@ public:
#endif
Display(BoardType type);
void begin(TwoWire &wire);
void end(void);
void clear(void); // .clear
void clear(void);
void invertDisplay(uint8_t i);
void show(); // .show()
void show();
void setContrast(uint8_t value);
void drawPixel(int16_t x, int16_t y, uint16_t color);
@ -39,14 +44,14 @@ private:
BoardType _boardType;
const BoardDef *_bsp = nullptr;
void *oled;
bool _isInit = false;
bool _isBegin = false;
#if defined(ESP8266)
const char *TAG = "oled";
Stream *_debugStream = nullptr;
#else
#endif
bool checkInit(void);
bool isBegin(void);
};
#endif /** _AIR_GRADIENT_OLED_H_ */

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