Compare commits

..

624 Commits

Author SHA1 Message Date
831c844c24 Merge pull request #330 from airgradienthq/feat/post-ccid
Provide SIM card ICCID when performing ota update
2025-06-20 17:38:22 +07:00
060a7f6815 Provide iccid when checking if firmware update available 2025-06-20 01:08:46 +07:00
d8eb6b3c1a Prepare release 3.3.9 2025-06-18 14:25:14 +07:00
969858b5cb Merge pull request #324 from airgradienthq/fix/comms-ag-server
Post measures and fetch configuration on boot only if respective configuration is set
2025-06-10 01:29:58 +07:00
09b5805686 Apply brightness 2025-06-10 01:26:29 +07:00
b09b753339 Only send first measures on boot if postDataToAirgradient is enabled 2025-06-10 01:10:14 +07:00
ddb3dba131 Skip fetch configuration on boot when configuration control is local 2025-06-10 01:09:53 +07:00
e780b0ace6 Merge pull request #323 from airgradienthq/fix/local-config-update
Update configuration changes by callback
2025-06-09 02:21:07 +07:00
e82da5401e Add new flag for command request
Such as led bar test and co2 calibration test
2025-06-09 02:13:32 +07:00
50a98acde4 Update configuration changes to main by callback 2025-06-06 04:10:53 +07:00
7049d21a41 Prepare release 3.3.8 2025-05-14 13:09:12 +07:00
d5cdeaa9f3 Fix print average function schedule
if pms value invalid show the channel
2025-05-14 13:01:23 +07:00
09207c6923 Merge pull request #319 from airgradienthq/fix/resizing-queue
Fix resizing measurement queue after post by cellular post
2025-05-14 12:53:07 +07:00
0a64424196 add show content delay for display brightness 2025-05-14 12:39:42 +07:00
5b38ca222b Prepare release 3.3.7 2025-05-12 10:54:38 +07:00
9ee35341a5 Merge pull request #318 from airgradienthq/feat/improve-measure-logs
Improve measurements logging
2025-05-11 14:22:08 +07:00
cec0514444 print measurements on schedule 2025-05-11 14:10:51 +07:00
626a2240fa Fix resizing queue after success post
This fix should be make it more consistent
2025-05-09 15:45:53 +07:00
174ec6568f Merge pull request #313 from airgradienthq/fix/mode-cloud-disable
Fix bootloop when cloud connection is disabled
2025-05-08 01:28:07 +07:00
6b55719399 Fix cloud connection mode
Use continue instead of return to ignore the rest of transmission code
2025-05-05 17:37:28 +07:00
e2084f0738 Fix OTA request on boot when cloud Connection disabled 2025-05-05 17:36:14 +07:00
5e07923690 Merge pull request #311 from ccoley/fix/submodule-url
Fix failing to clone submodules recursively
2025-05-04 14:13:47 +07:00
04049439b1 Merge pull request #310 from airgradienthq/fix/openmetrics
Fix calling airgradient client before initialization on open metrics
2025-05-04 13:29:24 +07:00
c148d256d7 Use a relative path for submodule URL
We keep the username in the path so that forks don't need to also fork
the submodules
2025-04-30 23:25:30 -07:00
02849a1938 Fix pass agclient to openmetrics
previously, agclient initialized after setAirgradient
2025-05-01 13:38:42 +08:00
074337a96d Merge pull request #304 from airgradienthq/fix/api-root
FIX: HTTP domain configuration changes applied for OTA too
2025-04-21 13:42:26 +07:00
4daa817a0b Change airgradient-ota commit to main branch 2025-04-21 13:41:15 +07:00
81a4502952 Fix: http domain applied for OTA 2025-04-21 13:27:05 +07:00
764e2eae38 Prepare release 3.3.6 2025-04-16 12:34:17 +07:00
79bf9811be Merge pull request #303 from airgradienthq/fix/ce-tvoc
Fix incorrect TVOC / NOx values when when network option is cellular
2025-04-15 12:25:39 +07:00
9475724d0c Remove comment 2025-04-15 12:20:26 +07:00
e7603a7659 Update feedback
Change airgradient-ota submodule to latest main instead of branch
2025-04-14 15:24:53 +07:00
9bba89722e Fix sgp unreliable value by only pause task when performing ota 2025-04-12 02:25:04 +07:00
81945a358e SGP41 add method to pause and resume task handle 2025-04-12 02:22:55 +07:00
3d26a54d69 Prepare release 3.3.5 2025-04-11 15:56:05 +07:00
b70ee75d50 Merge pull request #302 from airgradienthq/improve-ce-reconnection
Improve cellular client reconnection
2025-04-11 15:49:25 +07:00
c6846c818a Rename MICROS_TO_MINUTES() to follow convention 2025-04-11 15:46:21 +07:00
0b1c901a76 Rename cellularModule object name to cellularCard
Rename checkCellularClientNotReady to restartIfCeClientIssueOverTwoHours
2025-04-11 13:41:07 +07:00
83504c8628 Bump libs to latest 2025-04-10 19:05:28 +07:00
4487992748 Remove unnecessary code 2025-04-10 14:58:51 +07:00
3c8a65a329 Use esp_timer_get_time for timer of ce client not ready 2025-04-10 14:58:11 +07:00
673d564ddb Fix based on feedback 2025-04-10 12:45:18 +07:00
423eb4808f Change airgradient-client to latest main 2025-04-10 02:14:34 +07:00
18a710ffc2 Make sure transmit cycle not too long to wait divisible by 3 2025-04-10 02:06:11 +07:00
040cb79a4d Transmit measures only if queue size is 1 or divisible by 3 2025-04-10 00:27:44 +07:00
52d3dc03f1 Redundant check if cellular client not ready for 2 hours
Check calls happen in both task
2025-04-09 23:46:03 +07:00
1c6bc3ec55 Bump airgradient-client fix esp8266 compile 2025-04-09 22:48:21 +07:00
34d7c93e14 Improve reconnection of CE network option
Restart system if it already too long
2025-04-09 15:51:54 +07:00
fee1dc25d6 Improve reconnection of CE network option
Restart system if it already too long
Bump airgradient-client: Improve ensureClientConnection
2025-04-09 15:49:34 +07:00
9fb01d42f4 Prepare release 3.3.4 2025-04-07 16:56:54 +07:00
7bb013939c Merge pull request #301 from airgradienthq/feat/signal
Include cellular signal in RSSI (dbm) when post measures
2025-04-07 16:55:42 +07:00
0da21155e7 bump submodule to post measures with new endpoint
that include signal in rssi
2025-04-07 16:29:54 +07:00
7a153cc0ea add cellular signal quality to post measures payload
If value invalid 0, then do include it to payload
2025-04-07 16:29:15 +07:00
b079c35e6b Include cellular signal in rssi to measurement cycle 2025-04-07 16:28:37 +07:00
6051e183b8 Merge pull request #300 from airgradienthq/fix/pms-error
Remove CORE_DEBUG_LEVEL that affected PM sensor reading
2025-04-07 15:33:37 +07:00
c95379b957 Update submodule to the latest main branch 2025-04-07 15:30:52 +07:00
0cae8bc185 Change ag log level to info 2025-04-05 23:56:14 +07:00
5902a4c8e4 Remove arduino-esp32 core debug level from build_flags
And change it to airgradient log level that take effect to airgradient submodules
Temporary bump submodule to WIP branch
2025-04-05 23:45:46 +07:00
66818cd075 prepare 3.3.3 release 2025-04-04 11:31:19 +07:00
c1a6ddc68f Merge pull request #299 from airgradienthq/tmp/avg-max-period
Calculate measurement average max period use the same constant
2025-04-04 11:04:15 +07:00
20a32dd22c Measures average max period use the same constant
Cellular network options using wifi measurement interval as the constant reference to calculate max period
2025-04-04 10:54:03 +07:00
263dc9934e Merge pull request #298 from airgradienthq/fix/recover-cellular-connection
Restarting cellular module when cellular client is not ready
2025-04-04 10:43:59 +07:00
61b863b7f1 Fix esp_log logs not come out on O-PP 2025-04-04 10:21:50 +07:00
e01c1029fe Bump ag client
ensure client connection properly
2025-04-04 10:09:34 +07:00
ba5d817739 Merge pull request #297 from airgradienthq/feat/api-root
New local configuration to set HTTP domain name for monitor to post measures and fetch configuration from server
2025-04-03 16:51:04 +07:00
a91747e379 Update config sample 2025-04-02 16:30:13 +07:00
029457c3fa Add accepted value to http domain 2025-04-02 16:26:07 +07:00
55710dd4d9 Update docs for new configuration http domain 2025-04-02 16:18:55 +07:00
4886163cda Show on oled when httpDomain is set 2025-04-02 02:33:24 +07:00
7c57477238 Add local configuration to set http domain
change http domain by PUT from local server request
2025-04-02 02:12:13 +07:00
9ed58d1853 Prepare release 3.3.2 2025-03-31 17:12:15 +07:00
6c52b038e9 Merge pull request #295 from airgradienthq/feat/enable-at-debug
Enable cellular AT command debug when in network registration
2025-03-31 17:09:43 +07:00
2f69932ef7 add depth submodule update 2025-03-31 17:04:37 +07:00
1d96a274a6 Merge branch 'develop' into feat/enable-at-debug 2025-03-31 16:55:12 +07:00
df9f6dfc95 Fix bugs from 3.3.1 release 2025-03-31 16:52:09 +07:00
3fc02b3f54 Check signal when initialize cellular client 2025-03-31 16:51:29 +07:00
958ed0bd80 Fix TVOC and NOx payload position 2025-03-31 15:26:34 +07:00
e9be9dcc83 Fix mqtt host still exist on local when on server is disabled 2025-03-31 14:51:53 +07:00
7fbab82088 Change log level when correction not found 2025-03-31 14:07:30 +07:00
decdecdf22 Don't start mqtt when network option is cellular
Even when mqtt host is set
2025-03-31 14:01:49 +07:00
145c612867 Enable cellular at debug when registering network
On boot, airgradient-client change cellular init timeout to 5 mins
2025-03-31 13:53:56 +07:00
37de127887 prepare 3.3.1 release 2025-03-28 14:37:08 +07:00
baf80ce250 untrack compile_commands.json 2025-03-28 14:24:13 +07:00
80100e2475 prepare 3.3.0 release 2025-03-28 14:13:40 +07:00
d9c3fc6ec4 Merge pull request #292 from airgradienthq/feat/cellular
Add cellular connection as network options for AirGradient ONE and Open Air
2025-03-28 13:55:17 +07:00
67d377a514 Rename measurementCycle on agVlaue to Measures
capitalize static const for image bit
2025-03-28 13:45:07 +07:00
fff982f35f Apply stop main task for wifi too
Improve flow OTA success display
2025-03-27 17:46:08 +07:00
86cd90b94a Handling cellular client not ready better 2025-03-27 16:10:56 +07:00
656509c74d resize measurement cycle queue if already more than reserved 2025-03-27 14:34:26 +07:00
01f83cb02e update how to contribute 2025-03-27 14:11:10 +07:00
5c9c25c6b5 update how to compile with submodule 2025-03-27 14:04:24 +07:00
9291598209 Fix compile error esp8266 boards 2025-03-26 21:38:21 +07:00
429adb5e5e Remove otahandler from source file 2025-03-26 17:57:13 +07:00
4e651afc8c Remove oneopenair deps from AgApiClient 2025-03-26 17:43:39 +07:00
859abbe177 Update github action to recursive submodules when checkout 2025-03-26 17:41:49 +07:00
f079bb30d2 Update submodule
Printout http urls
fix progress more than 100%
2025-03-26 16:23:32 +07:00
070a103234 build CE payload include tvoc and nox index
If measures value invalid, set it to empty
New schedule to print network signal
2025-03-26 16:18:48 +07:00
ef87cde9d6 Change error status on display to icon 2025-03-24 03:52:46 +07:00
ea5e23b307 Fix cellular payload
Interval value should be in seconds
2025-03-24 03:49:33 +07:00
c2a26e78a0 agclient keep serial number on initialization 2025-03-23 21:48:46 +07:00
0297059e91 Fix check pm0003 count is valid
AgLog set to debug level
2025-03-21 08:46:14 +07:00
30622fca99 MeasurementCycle queue only applied for cellular
Cellular post measures payload different with wifi
Update submodule to support different cellular post endpoint
2025-03-21 04:40:27 +07:00
7c2aa35e4f Fix wifi connection error when using cellular 2025-03-18 01:02:10 +07:00
e93009f31c Decrease delay otaInProgress check
Do not run NetworkingTask when in offline mode
2025-03-18 00:01:04 +07:00
26db6372cd Tested ota cellular integration 2025-03-17 22:17:59 +07:00
d94ebbc570 Integrate ota 2025-03-17 15:12:11 +07:00
299234ac40 Update OneOpenAir.ino 2025-03-17 02:20:43 +07:00
76b2b3f940 Adjust interval based on network options 2025-03-16 23:15:01 +07:00
bf09b746c7 Handle reconnection when network option is cellular 2025-03-16 22:47:21 +07:00
b5c67cb0b1 Better network mode representation
Handle wifi network reconnection
if measurementCycleQueue empty, skip transmission
Move agclient implementation on initializeNetwork function
2025-03-16 16:13:14 +07:00
5f40a327b3 Run networking related on seperate task
A couple of todos still needs to address
2025-03-16 02:22:38 +07:00
66b0c63de5 New function for measurement cycle
getMeasurementCycle to capture current measurement that will be added to queue
buildMeasurementPayload using measurementCycle to build json string as transmission payload
2025-03-16 02:19:22 +07:00
cc3228f49a Fix: submodule changes 2025-03-14 11:48:12 +07:00
8728589ca1 Make sure CE load switch disable on boot 2025-03-14 11:04:32 +07:00
4b356920c2 First working integration using airgradientClient 2025-03-14 01:41:23 +07:00
c94b886360 Add airgradient-client as submodule 2025-03-14 01:18:36 +07:00
e056e44917 Add how to compile document 2025-02-28 16:10:56 +07:00
3b00fa69b8 Remove forgottern to delete text 2025-02-28 16:01:37 +07:00
e0720ac580 Add notes about arduino-esp32 version compatibility
Adding how to contribute section
2025-02-28 15:59:46 +07:00
0861c2dcaa Missing install library step for diy model 2025-02-26 14:43:14 +07:00
59fc0c409b Update to more comprehensive steps 2025-02-26 14:37:31 +07:00
6d63fdf643 Remove step to plug the monitor 2025-02-24 21:42:14 +07:00
033358e2c2 Update example sketch headers 2025-02-24 14:59:12 +07:00
47034f62b4 Add howto compile docs 2025-02-24 14:53:47 +07:00
71a21ce7e6 Merge branch 'master' into develop 2025-02-24 14:15:13 +07:00
3f5e5eebbb Merge pull request #279 from MallocArray/patch-2
Update workflow to use compile-sketches action
2025-02-16 04:32:28 +07:00
f9be400a5d Address PR comments on versions 2025-02-15 15:28:17 -06:00
54808ac076 Merge remote-tracking branch 'origin/develop' 2025-02-10 01:37:41 +07:00
063bb2a227 Prepare release 3.2.0 2025-02-10 01:36:41 +07:00
93f79173b2 Release 3.2.0-alpha 2025-02-07 20:06:10 +07:00
615c2389e7 Prepare 3.2.0 alpha release 2025-02-07 19:16:16 +07:00
f972637cca Merge pull request #283 from airgradienthq/feat/update-pm-correction
Apply PM corrections to all models
2025-02-07 19:05:20 +07:00
4c7e72b8e7 Better logging
Fix notif when wifi not connect
2025-02-07 10:45:14 +07:00
d4b4f51c3c Map batch PM correction as custom enum 2025-02-06 17:35:01 +07:00
1c42ff083d Make PM correction applied for all model 2025-02-06 15:58:15 +07:00
17d2e62b15 Remove delayed oled display 2025-02-06 15:38:36 +07:00
38aebeb50a Reformat pm correction enum naming 2025-02-06 12:49:41 +07:00
b0f5263829 Merge pull request #277 from airgradienthq/feat/correction-temp-hum
Apply temperature and humidity correction based on configuration
2025-02-06 10:14:38 +07:00
3226c14b6d Merge pull request #278 from airgradienthq/feat/disable-cloud
Fully disable cloud connection to airgradient server option
2025-02-06 10:13:50 +07:00
0e26aa1b5d Improve comments 2025-02-05 14:06:33 +07:00
830f652bf9 Remove unnecessary todo 2025-02-05 13:52:38 +07:00
bd9dbec663 Rename functions 2025-02-05 13:46:10 +07:00
29d701780a Improve logging 2025-02-05 11:37:16 +07:00
15869be234 Rename prefix temp hum correction enum member 2025-02-05 11:05:36 +07:00
4b09b98524 Fix variable naming ahum to rhum 2025-02-05 10:57:02 +07:00
afd498074b Fix grammar error 2025-02-05 10:43:56 +07:00
e851d0781c Merge branch 'develop' into feat/disable-cloud 2025-02-05 10:06:39 +07:00
2c27c6904c Merge pull request #282 from airgradienthq/fix/extend-connect-timeout
HTTP client failed/timeout to establish connection to airgradient server
2025-02-05 10:01:24 +07:00
03f1b969c2 Add comment describe two timeout functions call 2025-02-05 01:24:59 +07:00
85ba13de12 Set default ag client timeout to 15s 2025-02-05 01:18:46 +07:00
6ec545b00e Merge branch 'develop' into fix/extend-connect-timeout 2025-02-05 01:16:14 +07:00
05dbe60db2 Merge pull request #281 from airgradienthq/fix/zero-display
Fix display 0 measurements value on boot
2025-02-03 18:47:13 +07:00
d2ee3a5d24 Set default value for each measurements value to invalid 2025-02-03 01:28:42 +07:00
1839664137 Extend connect to server timeout
Default 5s from HTTPClient
2025-02-01 14:20:54 +07:00
154f3ecf8a Fix display 0 measurements on boot 2025-02-01 13:52:12 +07:00
b75e40b800 Rename variable for readability 2025-01-30 15:09:27 +07:00
84a358291b Rename function
from configure to configuration
2025-01-30 14:23:39 +07:00
0e41b2d630 Rename function from initiateNetwork to initializeNetwork 2025-01-30 10:01:15 +07:00
3e48a562e7 Change comment of sendDataToAg function call 2025-01-26 22:50:17 +07:00
f0c4df42b7 Fix wording on local-server 2025-01-26 13:02:42 +07:00
a50e1e2472 Update workflow to use compile-sketches action
Instead of straight CLI, uses the arduino/compile-sketches action.

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

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

A future improvement could be to also add https://github.com/arduino/report-size-deltas that can generate a comment on a PR that also shows the delta information.
2025-01-25 13:39:25 -06:00
c8f0e6a0d2 Update diy samples
That accomodate ApiClient changes
Fix apiClient begin on OneOpenAir
2025-01-25 04:08:17 +07:00
1537d5d480 Update docs
Notes about offlineMode and disableCloudConnection
2025-01-25 03:10:31 +07:00
32c78f6018 Apply disableCloudConnection
fetch configuration, post data and ota will be ignored
2025-01-25 03:01:32 +07:00
c5c0dae4bb Add config to disableCloudConnection functions 2025-01-25 01:51:20 +07:00
4bb97fc8be Fix changes to other examples 2025-01-24 18:17:41 +07:00
a28931493a Add docs about how to set correction from local server 2025-01-24 18:03:13 +07:00
af16c1c060 Apply temphum correction 2025-01-24 09:56:01 +07:00
1666923ab3 Add AirGradient and Configuration object to AgValue 2025-01-23 03:33:47 +07:00
89475ddf95 Get correction of temp and hum based on configuration 2025-01-23 01:21:27 +07:00
20db9d699b Retrieve temp hum correction object 2025-01-23 01:16:49 +07:00
88c2437907 Handle correction configuration for atmp and rhum
From local and cloud
2025-01-22 01:53:55 +07:00
e9b27185b4 Fix spaces 2025-01-22 01:46:08 +07:00
4534f7319a Merge pull request #275 from airgradienthq/fix/invalid-average
Fix invalidate value check for getAverage function
2025-01-19 15:50:47 +07:00
c842346724 Fix invalidate value check for getAverage 2025-01-19 15:45:09 +07:00
92e74feabd Merge branch 'develop'
# Conflicts:
#	library.properties
#	src/AirGradient.h
2024-12-05 15:36:32 +07:00
cc0fd88068 Prepared to release 3.1.21 2024-12-05 15:35:37 +07:00
56809a412c Merge branch 'develop' 2024-12-05 15:17:34 +07:00
6a83743e2a Prepared to release 3.1.16 2024-12-05 15:06:10 +07:00
faaf051e39 Prepared to release 3.1.15 2024-12-05 15:00:25 +07:00
5bc1821ef9 Merge pull request #264 from airgradienthq/feat/ssl
Airgradient API calls using https
2024-12-05 13:23:52 +07:00
280ea5e997 Prepared to release 3.1.14 2024-12-04 10:38:13 +07:00
e95627ece6 Merge pull request #270 from airgradienthq/fix/openmetrics-correction-openair
Fix openmetrics pm25 correction on openair
2024-12-03 03:36:25 +07:00
80b9ae11d8 Fix openmetrics pm25 correction on openair 2024-12-03 03:33:46 +07:00
1937e3d59e Merge pull request #267 from jakpor/fix/basic_temp_display
(Basic DIY): Fix temperature parsing in OLED and debug for Basic DYI version
2024-12-03 02:18:07 +07:00
107fb21331 Merge pull request #269 from airgradienthq/fix/openmetrics-correction
PM25 correction for openmetrics
2024-12-03 02:00:16 +07:00
ccc1ab463a PM25 correction for openmetrics 2024-12-03 01:55:11 +07:00
38e792b88d if guard uri for esp8266 2024-11-30 04:45:04 +07:00
aeee0cad01 OTA bin download using https
rename server root ca constant name
2024-11-30 04:33:58 +07:00
401326d00d Move airgradient server CA const to AirGradient.h file
Constant will be used by api client and otahandler
2024-11-30 04:13:25 +07:00
fb0dcad54d Fix CI error esp32 2024-11-30 03:52:32 +07:00
3556e4a96a Fix CI error 2024-11-30 03:42:45 +07:00
283646a699 Move OtaHandler to src
OTA only valid for esp32 based monitor
2024-11-30 02:47:05 +07:00
6312612ada resetReason and freeheap only for esp32 based mcu 2024-11-30 00:45:46 +07:00
c1f22674e2 Add reset reason to transmission payload 2024-11-29 18:01:34 +07:00
40d38a75d8 Add reset reason to transmission payload 2024-11-29 18:01:14 +07:00
39ef69cbdf Fix printing of debug logs throught serial 2024-11-27 17:18:43 +01:00
3473e30e2e Fix temperature float formatting for basic oled display 2024-11-27 17:17:22 +01:00
566f8a63b4 Prepare 3.1.13 release 2024-11-27 13:35:26 +07:00
9e4d52454b Merge pull request #266 from airgradienthq/fix/openmetrics
Fix measurements value in prometheus metrics endpoints
2024-11-27 03:38:57 +07:00
5f5e985309 Fix openmetrics esp8266 based 2024-11-27 00:43:03 +07:00
d638573ca7 Fix open metrics for OneOpenAir 2024-11-27 00:15:32 +07:00
4c165b31f5 Add freeheap to cloud payload 2024-11-21 02:36:56 +07:00
2be91b3968 Boot count using setter and getter 2024-11-21 02:30:03 +07:00
3ca2d1d208 Fix esp8266 build issue 2024-11-21 01:26:05 +07:00
aad12fc868 Using https
For get config and post measurements
2024-11-21 01:18:51 +07:00
79fbd901bd Merge branch 'develop' 2024-11-19 18:45:31 +07:00
3644dc43fe Prepared to release 3.1.12 2024-11-19 18:44:23 +07:00
03fa62d8f0 Merge pull request #263 from airgradienthq/fix/correction
Fix EPA compensated on top of SLR correction
2024-11-19 02:20:51 +07:00
902a768f28 Handle parsing invalid json string 2024-11-19 01:45:50 +07:00
1de9344f43 Fix typo on docs 2024-11-18 22:50:36 +07:00
46f6309b77 Fix use the right function 2024-11-18 22:42:43 +07:00
a6b48acb41 CO2 led bar indicator sync fix 2024-11-18 21:24:47 +07:00
1b4d89e1a1 fix correction on top of compensation 2024-11-18 19:53:28 +07:00
0d2b0fb657 Fix typo on local-server.md
Fix curl correction command example typos
2024-11-17 11:18:39 +07:00
9f08af44b0 Prepared to release 3.1.11 2024-11-11 20:04:47 +07:00
6b661cdeb7 Merge pull request #261 from airgradienthq/feat/allavg
Values on display and led bar using measurement average values
2024-11-11 12:40:48 +07:00
dc299c4b54 Fix pm2.5 not using getAverage for ledbar 2024-11-10 19:20:09 +07:00
2f595b4e41 Update local server docs correction examples 2024-11-10 04:26:54 +07:00
a30535f75f Use avg value for display and led bar 2024-11-10 04:13:58 +07:00
a513943cba New agvalue member to get avg values 2024-11-09 23:27:29 +07:00
96bb6952fb correction function return raw if algorithm is none 2024-11-09 21:28:33 +07:00
10653bfe26 Fix pms5003 correction default value 2024-11-09 20:55:41 +07:00
c7f89fa7b7 Decrease period length moving average to 80% 2024-11-09 20:53:52 +07:00
b11c461b60 Merge pull request #260 from airgradienthq/fix/led-flicker
Fix flicker on led bar led state change
2024-11-09 20:42:01 +07:00
404c14aad2 Fix typo and comment 2024-11-07 22:08:36 +07:00
bfbae680fd Fix led bar flicker when state change
clear only neccessary led
2024-11-07 22:03:01 +07:00
3ae5982380 Merge remote-tracking branch 'origin/develop' into develop 2024-11-04 13:21:46 +07:00
db2c2ef052 Prepared to release 3.1.10 2024-11-04 13:21:20 +07:00
593547cdbe Updated version number for 3.1.10 and enabled debug mode for verbose log messages 2024-11-03 14:38:37 +07:00
673c46950d Merge pull request #251 from airgradienthq/feat/ledbar-sensor-indicator
Update led bar indicator for CO2 and PM2 sensor level
2024-11-03 14:13:56 +07:00
ae0b4038d4 Merge branch 'develop' into feat/ledbar-sensor-indicator 2024-11-03 14:12:57 +07:00
cac0bd5355 Merge pull request #259 from airgradienthq/feat/pm-correction
Low Readings correction for Plantower PMS5003
2024-11-03 14:08:51 +07:00
3d6203dabf Fix some grammar on docs 2024-11-03 14:06:32 +07:00
1db8fbefe9 Corrections from local server
Tidy some things
2024-11-02 18:44:44 +07:00
d850d27dc1 Clear slr when not avail 2024-11-02 17:31:05 +07:00
f49e4a4b37 Fix casting enum issue
Previously if algo is slr, it's always consider new update
2024-11-02 17:23:15 +07:00
75f88b0009 Remove slr correction for pms5003t 2024-11-02 16:15:46 +07:00
c6961b3ca8 Validate raw pm before correction 2024-11-02 16:11:47 +07:00
ade72ff3b8 Apply correction to transmission payload
Only for indoor
2024-11-02 16:11:00 +07:00
9fbbea22ff Fix typo 2024-11-02 14:52:58 +07:00
7b0381dea3 Apply pm correction to display and led bar 2024-11-02 14:44:32 +07:00
5867d0f1d5 Fix pmcorrection member datatype
Log using printlog
Function to check if correction is not none
2024-11-02 14:40:35 +07:00
a98d77e0c3 slr pm2.5 correction implementation 2024-11-02 11:02:36 +07:00
641003f9d1 Get pm config function 2024-11-02 10:41:01 +07:00
0275aee370 Copy correction object to jconfig 2024-11-02 10:34:35 +07:00
ea46b812c1 Handle saving back to eeprom
rename the function
2024-11-02 00:53:33 +07:00
16c932962a Handle pm correction algorithm from ag server config 2024-11-02 00:10:08 +07:00
f90b2e1a07 Merge pull request #258 from airgradienthq/feat/particle-count
Include PMS particle count 0.5 and 5.0
2024-11-01 18:46:19 +07:00
3a9bb16c09 Change json key name for particle count 2.5 and 5.0 2024-11-01 17:46:58 +07:00
bb754edc51 Add other sensor json key field as const 2024-11-01 17:44:11 +07:00
1d991b1004 json key field from constants 2024-11-01 17:26:18 +07:00
3ebcc584a4 Update localserver docs 2024-10-31 21:19:46 +07:00
4d40ae421c Comment data to post
Payload already print out on toString() measurement
2024-10-31 21:17:28 +07:00
3004a82e7e pms disconnected log 2024-10-31 21:16:29 +07:00
4af5ca2665 Update local server docs 2024-10-31 21:12:29 +07:00
e6696f3d41 New particle count 0.5 and 5.0 2024-10-29 23:51:19 +07:00
2b33823162 Merge pull request #257 from airgradienthq/feat/pms-data
Send all particle counts and standard particle values provided by the PMS sensor
2024-10-23 20:57:58 +07:00
bf0768c7da Comment description to invalidValue variable 2024-10-23 20:55:45 +07:00
33e2977eb4 Fix comments 2024-10-23 11:18:18 +07:00
85e779cfc2 Use camel case for transmission payload 2024-10-23 11:05:16 +07:00
4783684443 Update local server payloads 2024-10-22 18:37:56 +07:00
3b0c77ca4d New measurements add to transmission payload 2024-10-22 18:28:56 +07:00
eeba41f497 Include other PMS data to measurements 2024-10-22 17:13:15 +07:00
fd1f35f6d8 Getter to get other PMS sensor data 2024-10-22 15:28:58 +07:00
eb76eff403 Merge pull request #256 from airgradienthq/restructure-agvalue
Restructure Measurements class
2024-10-22 13:40:50 +07:00
4673999dda Fix var type 2024-10-22 12:50:43 +07:00
83aa6a4502 Apply for other monitor series 2024-10-22 00:11:58 +07:00
8a87b865e6 Handle consecutive invalid value update
Set measurements type average value to invalid when invalidCounter reached max period
2024-10-21 22:37:44 +07:00
c3068be6e9 Fix calculation PPT compensated PM2.5 2024-10-21 22:00:47 +07:00
63bb5f8ddb Compensate function use float data type 2024-10-21 01:49:01 +07:00
8548d3e9f4 Optional to debug every measurement update value 2024-10-21 00:43:04 +07:00
f7e1363da9 Rename function 2024-10-21 00:22:50 +07:00
2ffe0a62aa Reduce update interval for max period to 50% 2024-10-21 00:15:59 +07:00
2cda36ed0d set measurement max period as function 2024-10-20 23:27:27 +07:00
7de2d0cc30 Set proper max period for moving average based on update interval
SHT read set to 6s
2024-10-20 23:20:16 +07:00
f478dd16c8 get value function consume 2024-10-20 22:30:49 +07:00
43ca0a2c2e get and getFloat function specific for latest value
Update functions comments
2024-10-20 20:04:07 +07:00
84884d0c15 Move average member value to update struct 2024-10-20 19:20:17 +07:00
f36f860c2e Switch to moving average for sensor data
average value to floating points
2024-10-20 19:01:41 +07:00
e47a9057ea Update AQI breakpoints to 2024 values (#208)
* Update breakpoints to 2024 standard

* Update formula to match Wikipedia
https://en.wikipedia.org/wiki/Air_quality_index#Computing_the_AQI

---------

Co-authored-by: Samuel Siburian <samuelbles07@gmail.com>
2024-10-19 14:55:08 +07:00
399b4ca1dc Other related class use new AgValue structure 2024-10-19 01:35:36 +07:00
2e4f4643fa maxUpdate 2024-10-19 01:33:00 +07:00
0ccf46c219 Rename AgValueType to MeasurementType
Just use plain enum instead of enum class
Remove unecessary legacy variables and function
2024-10-19 01:32:41 +07:00
76a2f332d7 Fix rhum precission on buildIndoor measurements 2024-10-17 12:08:00 +07:00
ed344d3e1a measurement toString
Tested on I-9PSL monitor
Update OneOpenAir post and mqtt to use new measurement toString
2024-10-17 00:53:49 +07:00
2082a2fa93 Fix missing line when commit 2024-10-14 02:14:10 +07:00
e145d32714 First test, console
Working average from main only
2024-10-14 02:05:30 +07:00
a2c19438c0 validateChannel implementation 2024-10-14 01:54:55 +07:00
ac838efdb5 validate measurement type channel 2024-10-14 01:25:35 +07:00
751d4e8380 Get value from each data type 2024-10-14 01:22:44 +07:00
6925b1ac9a Provide channel for neccessary ValueType
To support OA that have 2 PMS sensor
2024-10-13 23:30:03 +07:00
77a23b4202 Always increment bootcount when send measurements data is scheduled (#255) 2024-10-13 15:46:41 +07:00
ea91cf9b6c New function to set max update before averaging
Rename enum member
2024-10-11 22:47:43 +07:00
467b3e8637 Tidy up 2024-10-11 22:11:26 +07:00
2a5cf78b68 updateValue return bool to indicate max average is set or not
Add more comments
Update naming
2024-10-11 22:05:03 +07:00
9c09b82efd The data structure and update value function 2024-10-11 20:54:05 +07:00
60d01c0d94 First init the data structure 2024-10-10 22:51:37 +07:00
e7a91c53bc Update PM ledbar threshold 2024-10-08 00:34:29 +07:00
4e41fd5d71 Fix default for ledBarMode (#252) 2024-10-07 21:31:24 +07:00
fe4389bff4 Fix color under <4000 for co2 2024-09-30 22:43:08 +07:00
9325830fad Replace orange to yellow for ledbar co2 indicator 2024-09-30 22:38:52 +07:00
b86f0d45e3 Update PM2 level ledbar indicator 2024-09-30 22:33:52 +07:00
210f0a5ff9 Update CO2 level ledbar indicator 2024-09-30 22:27:45 +07:00
c841476ca4 Merge pull request #247 from airgradienthq/fix/pms-read-data
Fix: PMS sensor read failed in case PM value is low
2024-09-24 20:45:04 +07:00
359394af53 fix: compile failed for esp32-c3 2024-09-24 20:13:01 +07:00
b8e10f473e update API change on example 2024-09-24 20:07:31 +07:00
cb511903ef Update the API use Stream instead of Hardware/Software serial 2024-09-24 20:07:14 +07:00
ebb3f01dcd set active mode on init 2024-09-24 10:39:17 +07:00
2e0ba26c97 Merge commit '0370a8aa15ffaf776f9055f84b5d7c221046b9be' into fix/pms-read-data 2024-09-24 10:39:04 +07:00
c1a4758c6c update timeout handle 2024-09-24 10:28:41 +07:00
0370a8aa15 Update AirGradient.h to 3.1.9 2024-09-24 10:05:04 +07:00
863a37132a Update library.properties to 3.1.9 2024-09-24 10:04:28 +07:00
612317d976 Update local-server config example 2024-09-24 09:46:18 +07:00
8873bacf55 Merge pull request #243 from airgradienthq/feature/add-pm-configuratin-for-display
Add configuration: monitorDisplayCompensatedValues
2024-09-24 09:42:50 +07:00
bf2388b121 Merge pull request #241 from DmitryPustovit/support-display-disable-for-diy-board
Added support clearing display at 0 brightness for DIY Boards
2024-09-24 09:41:01 +07:00
b3918bd1fb Merge pull request #242 from airgradienthq/hw-watchdog-feed
Update hardware watchdog reset
2024-09-24 09:40:11 +07:00
2a6fce674e add variable comment 2024-09-23 06:51:01 +07:00
2f0663ced0 Merge pull request #246 from airgradienthq/fix/display-msg
Fix showing "Server N/A" when postDataToAirGradient is false
2024-09-22 14:26:17 +07:00
3adf58537a Changed error message 2024-09-22 14:13:24 +07:00
e10c9ff854 Update status notification in 1 function call 2024-09-22 13:18:15 +07:00
12c6ec9910 format code 2024-09-21 17:48:18 +07:00
d108b63a57 Update read proccess 2024-09-21 17:47:59 +07:00
6e212714fc Fix/mqtt-log (#235)
Ignore init mqtt when it's not configured
2024-09-21 14:57:05 +07:00
866684eb30 fix load configuration value changed 2024-09-21 14:26:06 +07:00
9d01479406 Update show PM compensate value on display and documents 2024-09-21 14:08:42 +07:00
20245f2110 Saving work 2024-09-21 13:06:01 +07:00
3890919f54 Update log message 2024-09-21 08:46:05 +07:00
76e40fea8c let hw watchdog run independently of POST success 2024-09-21 08:09:58 +07:00
c4024f49fb Added support clearing display at 0 brightness for DIY Boards to AgOledDisplay.
Currently, the only affect the brightness setting has with the DIY boards is an attempt to set the contrast. 

Setting the contrast to 0 does not have any effect. 
This appears to be a know limitation for these display boards.
2024-09-20 00:36:34 -07:00
ca5fc8d65b fix WiFi reset 2024-09-18 12:10:23 +07:00
fd2cef153e Merge pull request #239 from airgradienthq/hotfix/led-bar-show-pm-status
Fix: Correct LED bar show PM status
2024-09-17 10:25:42 +07:00
507b958fdf Correct LED bar show PM value use compensate 2024-09-17 10:15:47 +07:00
335c29ebb1 Merge remote-tracking branch 'origin/develop' into hotfix/led-bar-show-pm-status 2024-09-17 10:01:58 +07:00
2907d6f14e Merge pull request #238 from airgradienthq/hotfix/PM-compensation-receiving-temperature-instead-of-RH
Fix pm compensation: receiving temperature instead of humidity
2024-09-17 09:44:50 +07:00
c8d5b546ed correct PM compensate the input argument value humidity instead of temperature, fix #234 2024-09-16 14:52:04 +07:00
b7cfdc4c4d Update AirGradient.h to v 3.1.8 2024-09-16 12:47:50 +07:00
994d281e02 Update Version to 3.1.8 2024-09-16 12:47:08 +07:00
39470384e4 Merge pull request #233 from airgradienthq/cubic-PM2009X
Changed PM initialization to also support the Cubic PM2009X
2024-09-16 12:08:18 +07:00
c25ba764bf Merge pull request #236 from airgradienthq/add-log-pms-version-code
Add log: PMS5003x sensor print log firmware version
2024-09-16 11:02:32 +07:00
826ff00f42 add log message PM sensor firmware version 2024-09-16 10:36:45 +07:00
520550037d Explicitly set active mode for PM sensor upon initialization 2024-09-15 08:26:38 +07:00
90f336dee7 Revert "Explicitly set active mode for PM sensor upon initialization"
This reverts commit 0d39643e76.
2024-09-15 08:23:32 +07:00
0d39643e76 Explicitly set active mode for PM sensor upon initialization 2024-09-15 08:22:50 +07:00
21232ec49d Optimize PMS sensor read data on active mode send each second 2024-09-14 14:05:35 +07:00
b7339de31f Merge pull request #232 from samuelbles07/feat/ag-client-timeout
Feat/ag-client-timeout
2024-09-12 15:06:23 +07:00
013fb94307 Only for tcp timeout
Ignoring connect to server timeout
2024-09-11 16:37:50 +07:00
e16373a64d Add new public member to set http client timeout by caller 2024-09-11 16:02:13 +07:00
f929623443 Fix uri formatting postToServer to use apiRoot 2024-09-11 16:01:16 +07:00
59587ce2b7 Add http request timeout number for ApiClient 2024-09-11 15:48:44 +07:00
9ec74450a5 Merge branch 'master' into develop 2024-09-02 19:56:46 +07:00
28096e9faf Update version to 3.1.7 2024-09-02 19:55:16 +07:00
682378a47c Merge pull request #231 from airgradienthq/develop
Add WiFi feature
2024-09-02 19:53:33 +07:00
a1861be7b7 Merge pull request #230 from airgradienthq/feature/wifi-connect-to-default
Add default WiFi connect
2024-09-02 19:50:47 +07:00
99ddd24432 Merge branch 'develop' into feature/wifi-connect-to-default 2024-09-02 19:44:53 +07:00
29491e4cbe Merge pull request #229 from airgradienthq/develop
Merge to Master to Release 3.1.6
2024-09-02 12:19:46 +07:00
87cc3fc45f Update library.properties to v 3.1.6 2024-09-02 12:18:04 +07:00
7471d8079a Update AirGradient.h to v 3.1.6 2024-09-02 12:17:08 +07:00
8b0fe967f1 Merge pull request #223 from airgradienthq/hotfix/print-log-wrong-format
Fix print log message number format
2024-09-02 12:11:43 +07:00
6f1cef4e67 Merge pull request #224 from airgradienthq/hotfix/pms25-compensated-show-on-display
[Fix] PM2.5 compensated show on display
2024-09-02 12:09:51 +07:00
02b63ff816 Merge pull request #226 from airgradienthq/fix/pm2.5-compensated-formula
Fix pm2.5 compensation formula
2024-09-02 12:06:39 +07:00
228bf83e92 Merge pull request #228 from airgradienthq/feature/support-led-test-on-openair
`OpenAir` handle `ledBarTestRequested`
2024-09-02 12:05:20 +07:00
d3534cda52 handle ledBarTestRequested on OpenAir 2024-09-01 20:19:18 +07:00
aafaa42a68 Update formula link 2024-09-01 19:56:11 +07:00
2e9ff0d7dd add link to formula document 2024-08-30 19:21:54 +07:00
244b7814a6 add link to formula documents 2024-08-30 19:20:08 +07:00
28d27ee8fd Rename temperatureCompensated to compensateTemp and humidityCompensated to compensateHum 2024-08-30 19:17:58 +07:00
753f22923c rename isValidPMS to isValidPm 2024-08-30 19:07:31 +07:00
c45901706f Merge branch 'develop' into hotfix/pms25-compensated-show-on-display 2024-08-30 19:02:50 +07:00
663836e277 Merge pull request #205 from airgradienthq/feature/send-pms-sensor-fw-version-to-ag-cloud
Send PMS5003T firmware version to Ag Cloud
2024-08-30 10:54:36 +07:00
d39e10908d Merge branch 'develop' into hotfix/print-log-wrong-format 2024-08-28 09:57:45 +07:00
c52962d628 Update float constant 2024-08-26 20:47:48 +07:00
6b65efd3d6 fix pm2.5 compensated formula, #225 2024-08-26 20:43:48 +07:00
8bb87a75ef Merge pull request #222 from airgradienthq/hotfix/pms-fail-count-restart
Hotfix/pms fail count restart
2024-08-26 20:17:20 +07:00
1afcca25a1 Fix compile failed. 2024-08-26 15:54:41 +07:00
17238cff86 fix compile failed. 2024-08-26 15:52:31 +07:00
03e2afbf54 WiFi Connect to default airgradient if WiFi connected is empty 2024-08-26 15:47:49 +07:00
104d58a8c0 resolve review #222 2024-08-26 14:14:42 +07:00
7a988ea6c1 rename compensated to compensate 2024-08-26 11:56:01 +07:00
54ed83cb89 Revert las misstake commit changed. 2024-08-25 20:56:30 +07:00
e461b92c9f Fix build failed 2024-08-25 20:51:07 +07:00
db21648e91 Merge branch 'develop' into feature/send-pms-sensor-fw-version-to-ag-cloud 2024-08-25 20:46:28 +07:00
a9654506f5 Update log format, #218 2024-08-25 20:40:45 +07:00
63f653d5cd fix PM2.5 compensated on display, #221 2024-08-25 20:37:38 +07:00
b049a23657 Restart device after PMS sensor read failed 10 times 2024-08-25 20:21:26 +07:00
d6766ef68b Correct print log number format, fix #218 2024-08-25 08:37:25 +07:00
6c3259b94b Merge branch 'master' into develop 2024-08-23 09:16:10 +07:00
b1aaa04421 update GIT_BUILD version 2024-08-23 09:12:43 +07:00
2df78e9066 Merge branch 'master' into develop 2024-08-23 08:59:49 +07:00
186f0d27ab Update version 2024-08-23 08:59:28 +07:00
e25aa87ecc Merge pull request #215 from airgradienthq/develop
Develop
2024-08-23 08:50:04 +07:00
1cc8941a5c csv alignment format 2024-08-22 11:27:25 +07:00
9bf1495be7 Merge branch 'hotfix/firmware-version' into develop 2024-08-22 11:24:26 +07:00
73089b51f5 add firmware version for arduino build 2024-08-22 11:23:53 +07:00
625e60a5bf Merge branch 'hotfix/local-configuration-return-result' into develop 2024-08-21 12:54:47 +07:00
88e3d0bd3f Ignore and return failed if configurationControl is cloud 2024-08-21 12:54:06 +07:00
171821cfcf Merge pull request #214 from airgradienthq/hotfix/api-client-log
API client log post data
2024-08-21 12:28:50 +07:00
900a2da2ac Log POST data 2024-08-21 12:27:56 +07:00
fb57a112c9 Merge pull request #201 from McJoppy/feature/root-api
Reintroduce 'ROOTAPI' so domain and protocol can be configured
2024-08-20 09:25:48 +07:00
ab69b686ec Merge branch 'develop' into feature/root-api 2024-08-20 09:22:36 +07:00
6746d25dc2 Add comment to all example scripts showing how to change APIROOT 2024-08-18 16:46:56 +12:00
be150e105a Merge pull request #204 from airgradienthq/feature/add-http-request-to-ag-log
Add log message HTTP request and response to AG server
2024-08-18 11:37:42 +07:00
ecadeeb156 Merge pull request #209 from airgradienthq/hotfix/json-round-2-decimal-place-support-negative-value
`round2` support negative value
2024-08-18 11:33:39 +07:00
219ff73132 Merge pull request #212 from airgradienthq/feature/correct-pm2.5-formula
correct pm2.5 formula to show value on display
2024-08-18 11:32:50 +07:00
0a9142204d Merge pull request #213 from airgradienthq/hotfix/sensor-value-out-of-range
sensor value out of range
2024-08-18 11:30:27 +07:00
81b13134d2 update PM2.5 firmware prefix to PMS5003x 2024-08-16 06:42:43 +07:00
f3a9c722b2 Change pm25Compensated to compensated 2024-08-16 06:39:52 +07:00
3be3218115 Update invalid value and optimize code operator. 2024-08-15 09:11:38 +07:00
5edb21cfe9 Fix: PMS5003T only return positive temperature value 2024-08-15 09:10:48 +07:00
6cd587b008 Merge remote-tracking branch 'origin/develop' into feature/correct-pm2.5-formula 2024-08-15 08:19:10 +07:00
6d01366887 change pmsFirmare to firmware 2024-08-15 08:04:30 +07:00
1a347e9cfe Merge pull request #206 from airgradienthq/hotfix/display-show-nox-incorrect-position
`NOx` show incorrect position on display
2024-08-12 09:20:21 +07:00
6432e4451e Merge pull request #210 from airgradienthq/hotfix/set-connect-to-default-wifi-on-factory-reset
Set WiFi connect to default WiFi on factory reset
2024-08-10 07:45:06 +07:00
97f0696002 set default wifi on factory reset. 2024-08-09 13:19:28 +07:00
e46e11c030 round2 support negative value 2024-08-08 05:53:24 +07:00
dc261f668d Update local-server.md II 2024-08-07 11:08:48 +07:00
b5cced40d2 Update local-server.md 2024-08-07 11:05:39 +07:00
040bd28038 Add report PMS5003 and PMS5003T firmware version 2024-08-07 08:50:43 +07:00
b0ae851427 Fix nox position 2024-07-30 20:26:19 +07:00
01943f594d Send PMS5003T firmware version to cloud 2024-07-29 13:20:07 +07:00
01a69668cc Merge branch 'develop' into feature/send-pms-sensor-fw-version-to-ag-cloud 2024-07-29 12:56:45 +07:00
ed7b8df6fe log URLs of all HTTP requests to AG backend / log status codes of responses 2024-07-29 06:00:54 +07:00
6c1c914716 Merge pull request #196 from airgradienthq/hotfix/led-bar-show-sensor-data-incorrect
Correct LED bar show sensor value level color
2024-07-26 06:16:33 +07:00
6a0d88ff10 Merge pull request #197 from airgradienthq/hotfix/ignore-parameter-out-of-range
Ignore parameter values out of range
2024-07-26 06:12:04 +07:00
9097eed137 [fix] typo comment 2024-07-24 20:31:43 +07:00
c9b5e5f0d7 Merge branch 'develop' into hotfix/ignore-parameter-out-of-range 2024-07-24 20:24:27 +07:00
c12bac4ce3 Update invalid temperature value 2024-07-24 20:19:06 +07:00
9ae9b2ac9c display float value on display with 1 digit 2024-07-24 20:18:48 +07:00
7fb3e68b6d Merge pull request #195 from airgradienthq/hotfix/change-tvoc-to-VOC
Change `tvoc` to `VOC` on display
2024-07-24 09:19:03 +02:00
cf65a1f901 Merge pull request #200 from airgradienthq/feature/doc-quote-properties-name
Update local-server.md
2024-07-24 09:16:09 +02:00
5fb27b6d1e Check value sensor value 2024-07-24 09:05:57 +07:00
7b9dac756b Reintroduce 'ROOTAPI' so domain and protocol can be configured
eg. setter for api root added as comment in examples/BASIC/BASIC.ino
2024-07-21 19:38:50 +12:00
4b2a5f5540 Add PM2.5 correction formula, #182 2024-07-21 07:13:34 +07:00
4af77d532e Update local-server.md
Quote properties name
2024-07-20 09:39:29 +07:00
812c2ab803 add PMS5003T get module firmware version code 2024-07-20 08:53:19 +07:00
0ece16f434 Update screen layout, #139 2024-07-18 11:28:12 +07:00
df6cca3714 Ignore parameter values out of range #190 2024-07-11 06:25:31 +07:00
c8aa07ae20 Correct LED bar show sensor value level color, #161 2024-07-09 06:44:04 +07:00
a1d216ac77 Change tvoc to VOC, #139 2024-07-09 06:10:07 +07:00
a9d9c60dfa Merge branch 'develop' 2024-06-30 07:23:06 +07:00
e58ce1cbea Upgraded version number for Arduino library manager 2024-06-29 15:08:36 +07:00
64827223ec Merge pull request #191 from airgradienthq/feature/add-esp8266-examples
Add esp8266 examples
2024-06-29 15:03:56 +07:00
bddd4fef25 Update BASIC CO2 calibration display message. 2024-06-25 17:32:19 +07:00
0eba54fd28 Remove don't use code 2024-06-25 17:20:04 +07:00
a4176f966a Update board name comment 2024-06-25 17:13:18 +07:00
2de54ca6a6 update platformio.ini 2024-06-25 16:54:42 +07:00
bfd20a73da Rename board type 3.7 to 3.3 2024-06-25 16:43:50 +07:00
3ebce4ac44 Update firmware model name 2024-06-25 16:29:21 +07:00
f08c8edd19 Rename example 2024-06-25 16:27:35 +07:00
6b3e8e3096 Update default configuration json for special board 2024-06-25 16:24:12 +07:00
beb17de7dc [Fix] WiFi reset failed and crash in offline mode 2024-06-25 16:08:57 +07:00
bccadd17d7 [Fix] Wrong firmware model name 2024-06-25 15:35:18 +07:00
863b1f908e Merge commit '5a2c1bd1d0290e5cae0b8dec02eb2a931211d1af' into feature/add-esp8266-examples 2024-06-25 15:19:28 +07:00
5a2c1bd1d0 Merge pull request #175 from airgradienthq/fix/local-configuration-response-result
[Fix] Local `configurationControl` return failed if set `cloud`
2024-06-25 15:18:49 +07:00
dbc63194e6 Update BASIC.ino example 2024-06-24 18:34:24 +07:00
57c33e4900 check to handle sgp sensor 2024-06-24 18:33:46 +07:00
48f1a8042a Fixed FW_MODE_I_37PS 2024-06-22 13:47:13 +07:00
fbd5779fe6 platformio.ini update 2024-06-20 11:36:07 +07:00
45a4d98267 Fix wrong firmware mode 2024-06-20 11:34:43 +07:00
0119a4d62a Add support board DIY Pro 3.7 and add example 2024-06-20 11:34:23 +07:00
7560251deb Update mqtt print log message 2024-06-20 10:12:37 +07:00
31655b0a4f [Fix] PCB 4.3: Boot loop if not connected to Dashboard, #186 2024-06-20 10:12:19 +07:00
7d15c37ad9 Update firmware mode, fix #185 2024-06-20 09:34:25 +07:00
411beda220 Update tvocRaw and noxRaw, fix #184 2024-06-20 09:03:21 +07:00
0e869d1c0c Update README.md 2024-06-19 15:18:51 +07:00
f25f816428 Add MQTT 2024-06-19 15:17:48 +07:00
c8745e123b Update board support handle and fix connect timeout handle. 2024-06-19 10:33:02 +07:00
5ea01b8e9f [Fix] WiFi reset and set connect to factory wifi 2024-06-18 20:35:50 +07:00
a11eaea532 [fix] build fail on example BASIC 2024-06-18 19:59:29 +07:00
3b6859f483 Updated documentation for local server 2024-06-17 08:46:56 +07:00
bd237ed95d Add DiyProIndoorV4_2.ino example 2024-06-15 15:40:50 +07:00
d24b20a734 Merge pull request #177 from airgradienthq/feature/factory-reset-open-air-connect-to-airgradient-wifi
OpenAir set WiFi connect to `airgradient` on factory reset
2024-06-14 09:38:21 +07:00
227a4f76f7 Merge pull request #176 from airgradienthq/fix/display-show-configureation-failed-status
Fix display message is not correct.
2024-06-12 06:07:01 +07:00
adabb9baa4 OpenAir set WiFi connect to airgradient on factory reset 2024-06-12 06:04:13 +07:00
935e7f365f Fix display message is not correct. 2024-06-10 22:04:23 +07:00
d4ea03f39e Handle configurationControl set cloud return success 2024-06-08 12:28:17 +07:00
89aefdda43 fix type 2024-06-08 12:01:42 +07:00
8a83e408d3 Merge pull request #173 from airgradienthq/develop
Next version 3.1.3
2024-06-06 13:17:08 +07:00
7a1a0337d1 Merge pull request #163 from joostlek/typo
Fix typos in docs
2024-06-06 13:13:00 +07:00
6bdd4cb02f Merge pull request #172 from airgradienthq/hotfix/display-show-rebooting-but-not-rebooting
[Fix] Reboot device after WiFi Configure porttal timeout
2024-06-06 13:11:33 +07:00
38bd758b69 Merge pull request #171 from airgradienthq/feature/update-display-message
Update the message show on display
2024-06-06 13:11:19 +07:00
6aa19ea3e6 Set next version 2024-06-06 08:44:12 +07:00
da323b1a46 fix reboot device after WiFi portal configure timeout. 2024-06-05 19:01:25 +07:00
2ae90444bb Fix reboot device after WiFi Connector perform but not connected 2024-06-05 14:31:34 +07:00
21e802da33 Merge pull request #170 from airgradienthq/feature/regularly-call-ota-handler
OTA update each 60 min
2024-06-05 13:38:05 +07:00
e0869fbaf0 Merge pull request #167 from airgradienthq/hotfix/too-many-get-config-request
[Fix] Too many server GET config request
2024-06-05 13:24:59 +07:00
70662091ec Merge pull request #166 from airgradienthq/hotfix/wifi-not-reset-in-offline-mode
[Fix] WiFi not reset on offline mode
2024-06-05 13:24:44 +07:00
960d2bad64 Merge pull request #169 from airgradienthq/feature/add-boott-support-ha
Add bootCount to support on HA
2024-06-05 13:22:19 +07:00
7abf7f5e6a Merge pull request #162 from joostlek/update-put-table
Update configuration parameters
2024-06-05 10:34:03 +07:00
6bad4fd04b Merge pull request #164 from joostlek/json
Reformat JSON in docs
2024-06-05 10:30:56 +07:00
a17f18b3db Merge pull request #165 from joostlek/table
Reformat table
2024-06-05 10:29:59 +07:00
3da4900462 Update show mesage on display 2024-06-05 09:58:08 +07:00
d2d81f6b4b OTA update each 60 min 2024-06-05 09:29:30 +07:00
0b12f56513 add bootCount to measure report 2024-06-04 22:17:20 +07:00
f7e811e34b Update server GET configure period to 60 sec 2024-06-04 18:30:33 +07:00
037bb37184 Change macro name SERVER_CONFIG_UPDATE_INTERVAL' to SERVER_CONFIG_SYNC_INTERVAL` 2024-06-04 18:29:35 +07:00
fde510ba96 Merge pull request #137 from airgradienthq/hotfix/correct-ota-update-message-on-display
Correct OTA process message show on display
2024-06-04 18:15:01 +07:00
14b152a2a7 Merge branch 'develop' into hotfix/correct-ota-update-message-on-display 2024-06-04 18:09:04 +07:00
ef6b041529 Fix: ota not callback on first powerup perform 2024-06-04 18:01:55 +07:00
11ecea1493 Change showFirmwareUpdateSuccess input fromString to int 2024-06-04 18:01:28 +07:00
3e2e8b15eb Fix: WiFi configuration not reset in offline mode. 2024-06-04 15:51:01 +07:00
4249a86fd5 Reformat table 2024-06-04 09:21:29 +02:00
4a58b0b1c7 Reformat JSON in docs 2024-06-04 09:20:18 +02:00
5f93329e96 Fix typos in docs 2024-06-04 09:17:33 +02:00
59d189e1d3 Update configuration parameters 2024-06-04 09:14:30 +02:00
2e62abe2d7 Merge pull request #156 from airgradienthq/develop
Develop
2024-05-29 18:02:27 +07:00
7569b114bf Merge pull request #155 from airgradienthq/feature/update-led-bar-button-test
Handle LED bar test button before init sensor initialize
2024-05-29 16:36:08 +07:00
3c1d0a862f update comment 2024-05-29 16:34:00 +07:00
4d883af77e Merge pull request #138 from airgradienthq/hotfix/ledbar-flickers
Fix LED bar flickers
2024-05-29 08:01:05 +07:00
e9224a5de0 Remove test logging message 2024-05-29 07:58:49 +07:00
af0fbadd80 Handle LED bar button test before init sensor. 2024-05-29 07:55:27 +07:00
79f6c040c7 Merge pull request #143 from airgradienthq/feature/reboot-after-wifi-connect-timeout
Reboot After 180s WiFi Manager not Connect.
2024-05-28 13:43:08 +07:00
f262866148 Merge pull request #154 from airgradienthq/hotfix/display-co2-calibration-message
Update CO2 calibration message show on display
2024-05-28 13:42:14 +07:00
78a2a78020 Update CO2 calibration message show on display 2024-05-25 20:25:50 +07:00
65e759fb90 Merge branch 'develop' into hotfix/correct-ota-update-message-on-display 2024-05-24 13:30:09 +07:00
3fc7e4b55e Merge pull request #149 from airgradienthq/hotfix/reporting-fw-version-on-dashboard
Official OTA 3.1.1 version not reporting FW version on dashboard
2024-05-24 09:30:13 +07:00
5857388c2d Merge pull request #152 from airgradienthq/hotfix/remove-configuration-missleading-message
Remove `Update ignored due to local unofficial changes`
2024-05-24 09:01:04 +07:00
5770b41fd4 remove Update ignored due to local unofficial changes 2024-05-24 08:45:50 +07:00
d85d890878 Change OTA update period from 2h to 1h 2024-05-24 08:26:24 +07:00
9fbd31d0c8 Merge pull request #150 from airgradienthq/hotfix/show-message-monitor-not-setup-on-dashboard
disp.setText("Monitor not", "setup on", "dashboard") called when monitor is actually already on dashboard
2024-05-24 08:23:52 +07:00
c5b7c43293 disp.setText("Monitor not", "setup on", "dashboard") called when monitor is actually already on dashboard 2024-05-22 12:13:57 +07:00
7550ef7b0c change OTA update period each 2h. 2024-05-22 11:45:56 +07:00
805546b78e check firmware update after powerup 2024-05-22 11:34:42 +07:00
59880f4be5 fix typo 2024-05-22 11:17:11 +07:00
ee7837a471 Merge pull request #148 from MallocArray/patch-1
Correct API value for boot
2024-05-20 08:50:57 +07:00
ebbf0adf2f Correct API value for boot 2024-05-18 21:19:40 -05:00
d9551dc560 Reboot After 180s WiFi Manager not Connect. 2024-05-18 08:47:49 +07:00
6ea0ab9272 Set process 100% after received firmware file. 2024-05-17 20:18:59 +07:00
6e54409512 rename function 2024-05-17 20:17:49 +07:00
f35bc4feaa Fix LED bar flickers 2024-05-17 11:52:22 +07:00
c04ab90fd2 Update OTA processing message. Fix always retry of bootCount = 0 2024-05-17 10:32:20 +07:00
ed02f66ca2 Correct OTA update process show message on display 2024-05-16 21:12:02 +07:00
4b94926651 Merge pull request #136 from airgradienthq/develop
merge to master to prepare for release
2024-05-16 16:36:17 +07:00
f505b39247 Merge pull request #135 from airgradienthq/hotfix/pm25-appear-colors
Update PM2.5 LED bar color and level
2024-05-16 13:09:58 +07:00
9e44cd89d9 Update PM2.5 LED bar color and level 2024-05-16 12:50:38 +07:00
e7b95c0bde Merge pull request #126 from airgradienthq/OTA-dev
added doc regarding the OTA mechanism
2024-05-16 11:55:57 +07:00
b72394b004 Merge pull request #127 from airgradienthq/hotfix/purple-color
Hotfix/purple color
2024-05-16 11:55:31 +07:00
1544989fe6 Merge pull request #128 from airgradienthq/hotfix/change-ota-update-period
Change the OTA update period use timestamp to bootCount
2024-05-16 11:54:56 +07:00
d7b5e999c1 Merge pull request #129 from airgradienthq/hotfix/turn-led-off-on-offlinemode
Turn LED indicate wifi failed, cloud failed and get cloud configuration failed to off
2024-05-16 11:54:00 +07:00
9b0210f7b5 Merge pull request #130 from airgradienthq/hotfilx/print-log-wrong-number-format
Fix print number value wrong format
2024-05-16 11:53:16 +07:00
3715266f8c Merge pull request #131 from airgradienthq/hotfix/update-local-configure-repsonse-fail-message
Update local configuration response failed message
2024-05-16 11:52:57 +07:00
a4eb607174 Merge pull request #132 from airgradienthq/hotfix/update-configuration-log-message
Update configuration log message
2024-05-16 11:52:21 +07:00
f55fa6a617 Update configuration log mesasge 2024-05-15 17:20:10 +07:00
4f9f800cce Update local configuration response failed message 2024-05-15 17:15:36 +07:00
cad5d1f0e7 fix print number value wrong format 2024-05-15 17:13:39 +07:00
55ede2b04d turn led indicate wifi failed, cloud fail and get cloud configuration fail to off. 2024-05-15 17:11:26 +07:00
563bdfe4b2 Change the OTA update period use timestamp to bootCount 2024-05-15 17:09:06 +07:00
e2154af85f Merge commit '9a2bbd7a57a7dd4a6eaa27b85576c20a85d7ca09' into hotfix/purple-color 2024-05-15 16:10:08 +07:00
9a2bbd7a57 fix: Purple color 2024-05-15 14:02:49 +07:00
a8c8246632 added doc regarding the OTA mechanism 2024-05-15 11:05:37 +07:00
a71c038864 Fix camelCase, #122 2024-05-13 21:28:37 +07:00
799217e724 Remove displayMode from configuration, #123 2024-05-13 21:25:00 +07:00
b3f02f0a58 Merge pull request #125 from airgradienthq/hotfix/openair-sync-miss-data
Fix: Firmware mode `O-1PP` send miss data channel-2
2024-05-13 20:44:26 +07:00
c640cf773e Merge pull request #124 from airgradienthq/hotfix/configuration-handle
Configuration failed response message and failed condition handle.
2024-05-13 20:39:45 +07:00
c145666fcb Merge pull request #121 from airgradienthq/hotfix/led-bar-power-up-test-wifi-connection-still-perform
Fix issue: LED bar test presssed but WiFi connection still perform
2024-05-13 20:39:26 +07:00
466bb0eb21 Merge pull request #120 from airgradienthq/hotfix/not-show-the-message-add-to-dashboard
Fix issue: dashboard not show if get cloud configuration failed.
2024-05-13 20:39:11 +07:00
4612f4b793 Add condition to handle configuration changed. 2024-05-13 19:00:34 +07:00
45c7279866 Fix: Firmware mode O-1PP send miss data channel-2 2024-05-13 18:25:03 +07:00
5cb838af29 fix: OpenAir send incorrect model(firmware mode) 2024-05-13 18:11:46 +07:00
3201fd8d9c Fix: Configuratoin failed response mesasge and failed condition handle. 2024-05-13 17:43:42 +07:00
f23c7e9e31 Set offline mode incase wifi is not configuraion or configuration ignored. 2024-05-13 15:07:10 +07:00
5b18a8353d Fix issue: LED bar button test pressed but WiFi connection still perform. 2024-05-13 14:43:53 +07:00
1e81c9b125 Fix issue: dashboard not show if get cloud configuration failed. 2024-05-13 13:52:14 +07:00
3dae4cb06d Merge pull request #119 from airgradienthq/hotfix/fix-issue-before-release-new-version
Hotfix: Fix some issue before release new version
2024-05-13 12:26:53 +07:00
b4745ef55d Merge pull request #118 from airgradienthq/hotfix/offline-mode-should-not-show-server-status
Offline mode should not show server status on display
2024-05-13 12:09:59 +07:00
348cb2663a Merge pull request #116 from airgradienthq/hotfix/add-debug-mesage-when-hw-watchdog-reset
Update watchdog reset message
2024-05-13 12:09:45 +07:00
1b69e8a599 Offline mode should not shown status on display. #111 2024-05-13 12:02:17 +07:00
d17ad3cbab Merge branch 'hotfix/factory-reset-offline-mode' into hotfix/offline-mode-should-not-show-server-status 2024-05-13 11:52:01 +07:00
a6d8936ea6 Fix factory reset failed, the configuration not set to default. #112 2024-05-13 11:47:37 +07:00
8b428855b0 Merge branch 'hotfix/add-debug-mesage-when-hw-watchdog-reset' into hotfix/factory-reset-offline-mode 2024-05-13 11:21:01 +07:00
22dc2136e4 Update watchdog reset message 2024-05-13 11:18:08 +07:00
f23f81f575 Merge branch 'hotfix/ledbar-display-should-off-when-brightness-0-percent' into hotfix/fix-issue-before-release-new-version 2024-05-13 10:39:48 +07:00
7a4255b2bb Turn off LED bar and Display if brightness is 0%, fix #114 2024-05-13 10:34:06 +07:00
d844ab09fa Merge pull request #107 from airgradienthq/hotfix/configuration-default-after-ota-success-and-new-firmware-has-change-configuration-param
Move structure configure to JSON
2024-05-12 18:19:39 +07:00
31c60dcbec fix build failed 2024-05-12 11:48:03 +07:00
f0749783fe Merge branch 'develop' into hotfix/configuration-default-after-ota-success-and-new-firmware-has-change-configuration-param 2024-05-12 10:49:13 +07:00
d34605a018 Merge pull request #109 from airgradienthq/feature/adjust-co2-color-and-ranges
Adjust CO2 Colors and Ranges
2024-05-11 20:12:11 +07:00
1bcb9bf5ee Adjust CO2 Colors and Ranges 2024-05-11 19:29:02 +07:00
296bf49e5e Merge pull request #108 from airgradienthq/feature/press-button-for-offline-mode
Display show message for offline/online mode
2024-05-10 09:18:32 +07:00
e3dee42b4b fix esp8266 build fail 2024-05-10 09:16:22 +07:00
279ccb8bfb update configuration filename, log mesage and add configuration default value 2024-05-10 09:11:43 +07:00
a3c9727b02 Update variable a descriptive name 2024-05-10 07:19:11 +07:00
1b886d9843 Merge branch 'hotfix/configuration-default-after-ota-success-and-new-firmware-has-change-configuration-param' into feature/press-button-for-offline-mode 2024-05-09 15:09:00 +07:00
066e81b186 fix esp8266 build fail 2024-05-09 15:03:27 +07:00
c98d078d4c Resolve complex build failed 2024-05-09 14:56:40 +07:00
cb98183e20 Merge branch 'develop' into feature/press-button-for-offline-mode 2024-05-09 14:48:30 +07:00
0e1734b35d Merge branch 'develop' into hotfix/configuration-default-after-ota-success-and-new-firmware-has-change-configuration-param 2024-05-09 14:46:00 +07:00
da6326db0f Add display ask for offline/online mode 2024-05-09 14:32:42 +07:00
ad1da129c0 Merge branch 'hotfix/configuration-default-after-ota-success-and-new-firmware-has-change-configuration-param' into feature/press-button-for-offline-mode 2024-05-09 13:35:46 +07:00
8a90fe511b Merge pull request #104 from airgradienthq/feature/show-ota-process-on-display
Feature/show ota process on display
2024-05-09 06:23:03 +07:00
cca1ab69bb Move rebooting process out of OtaHandler 2024-05-08 12:31:54 +07:00
955172d3d3 Move structure configure to JSON 2024-05-08 12:22:34 +07:00
4500f41ed3 Merge pull request #106 from airgradienthq/feature/wifi-auto-connect-to-airgradient
Code for autoconnect to: airgradient:cleanair
2024-05-07 15:47:22 +07:00
7c2f8e5b9b Add WiFi reset to factory default: connect to SSID airgradient after led bar test and button still keep pressed. 2024-05-07 15:00:32 +07:00
fb84b53077 Merge pull request #102 from airgradienthq/hotfix/led-button-test-not-handle-on-power-up
fix led bar button test not work on power up
2024-05-03 21:03:15 +07:00
3359b1817b Merge pull request #98 from airgradienthq/hotfix/remove-wifi-qrcode
Remove WiFi QrCode
2024-05-03 21:02:16 +07:00
8eb8d4a1ec Remove dependency from StateMachine 2024-05-03 17:27:05 +07:00
d2723de0f8 Remove OtaHandler constructor 2024-05-02 18:22:26 +07:00
4493156739 Implement regular OTA update attempt / indicate OTA processing on display 2024-05-02 10:19:49 +07:00
0acb7d470d Merge remote-tracking branch 'origin/develop' into feature/show-ota-process-on-display 2024-05-02 09:50:28 +07:00
8428442dea Merge branch 'develop' into hotfix/remove-wifi-qrcode 2024-05-02 09:19:14 +07:00
f32d6b1bbe Remove country dependency on LED bar button test 2024-05-02 09:10:43 +07:00
46600f59a3 Merge pull request #103 from airgradienthq/feature/display-and-ledbar-brightness
Feature/display and ledbar brightness
2024-05-02 08:58:45 +07:00
01c42387ed fix: typo 2024-05-01 21:27:01 +07:00
f08438db46 Implement display / ledBar brightness 2024-05-01 21:25:35 +07:00
221730160b fix led bar button test not work on power up 2024-05-01 21:02:57 +07:00
d40b1d37a8 Merge pull request #99 from airgradienthq/hotfix/git-workflow-ota-failed
fix github workflows build fail and platformio multiple project build
2024-05-01 09:54:59 +07:00
ecd3aa988f Merge pull request #101 from airgradienthq/hotfix/EEPROM-data-override-by-after-OTA-perform-success
fix issue EEPROM override after OTA perform by use `spiffs`
2024-05-01 09:54:37 +07:00
095787d1f2 Merge pull request #100 from airgradienthq/hotfix/temperature-unit-supported-shortname-value
Temperature configuration unit support shortname value `c` and `f`
2024-04-30 22:26:53 +07:00
d94d074abe fix issue EEPROM override after OTA perform by use spiffs 2024-04-30 20:51:08 +07:00
cdef9822b3 Merge pull request #97 from airgradienthq/dev/fix-source-code-format
re-format source code
2024-04-30 20:46:14 +07:00
29c1989e78 Temperature configuration unit support shortname value c and f 2024-04-30 20:32:53 +07:00
bc3872e631 fix github workflows build fail and platformio multiple project build 2024-04-30 20:28:34 +07:00
7a182ebb12 Remove WiFi QrCode 2024-04-30 19:59:43 +07:00
5e6f801534 re-format source code 2024-04-29 19:46:13 +07:00
88808a2ad2 Revert "fix: source code formating"
This reverts commit 1580cd51aa.
2024-04-29 19:34:15 +07:00
a4fc954712 Merge remote-tracking branch 'origin/master' into develop 2024-04-29 10:01:10 +07:00
1580cd51aa fix: source code formating 2024-04-29 09:54:37 +07:00
3efba24e24 Merge pull request #96 from danielmoore/prom-pms-data
Report PM data to metrics API for AirGradient ONE
2024-04-29 09:43:45 +07:00
940dd0167c Fix OpenMetrics humidity metric 2024-04-28 22:10:39 -04:00
6b4f86e7e4 Report PM data to metrics API for AirGradient ONE 2024-04-28 21:47:38 -04:00
e5a0c6bc7b Merge pull request #95 from airgradienthq/ota
OTA Update, better handle the 304 and 400 case
2024-04-27 14:14:09 +07:00
b8fdb38db8 Merge branch 'develop' into ota 2024-04-25 16:47:28 +07:00
b2c55c38dc better handle the 304 and 400 case 2024-04-25 16:11:49 +07:00
104 changed files with 11677 additions and 4274 deletions

View File

@ -6,6 +6,8 @@ jobs:
matrix:
example:
- "BASIC"
- "DiyProIndoorV4_2"
- "DiyProIndoorV3_3"
- "TestCO2"
- "TestPM"
- "TestSht"
@ -15,40 +17,47 @@ jobs:
- "esp32:esp32:esp32c3"
include:
- fqbn: "esp8266:esp8266:d1_mini"
core: "esp8266:esp8266@3.1.2"
core: "esp8266:esp8266"
core_version: "3.1.2"
core_url: "https://arduino.esp8266.com/stable/package_esp8266com_index.json"
- 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"
core: "esp32:esp32"
core_version: "2.0.17"
core_url: "https://espressif.github.io/arduino-esp32/package_esp32_index.json"
board_options: "JTAGAdapter=default,CDCOnBoot=cdc,PartitionScheme=min_spiffs,CPUFreq=160,FlashMode=qio,FlashFreq=80,FlashSize=4M,UploadSpeed=921600,DebugLevel=verbose,EraseFlash=none"
exclude:
- example: "BASIC"
fqbn: "esp32:esp32:esp32c3"
- example: "DiyProIndoorV4_2"
fqbn: "esp32:esp32:esp32c3"
- example: "DiyProIndoorV3_3"
fqbn: "esp32:esp32:esp32c3"
- example: "OneOpenAir"
fqbn: "esp8266:esp8266:d1_mini"
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
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 }}'
- uses: actions/checkout@v4.2.2
with:
fetch-depth: 0
submodules: 'true'
- uses: arduino/compile-sketches@v1.1.2
with:
fqbn: ${{ matrix.fqbn }}
sketch-paths: |
examples/${{ matrix.example }}
libraries: |
- source-path: ./
cli-compile-flags: |
- --warnings
- none
- --board-options
- "${{ matrix.board_options }}"
platforms: |
- name: ${{ matrix.core }}
version: ${{ matrix.core_version}}
source-url: ${{ matrix.core_url }}
enable-deltas-report: true
# TODO: at this point it would be a good idea to run some smoke tests on
# the resulting image (e.g. that it boots successfully and sends metrics)
# but that would either require a high fidelity device emulator, or a

5
.gitignore vendored
View File

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

6
.gitmodules vendored Normal file
View File

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

View File

@ -41,6 +41,7 @@ Local server API documentation is available in [/docs/local-server.md](/docs/loc
- [Sensirion I2C SHT](https://github.com/Sensirion/arduino-sht)
- [WiFiManager](https://github.com/tzapu/WiFiManager)
- [Arduino_JSON](https://github.com/arduino-libraries/Arduino_JSON)
- [PubSubClient](https://github.com/knolleary/pubsubclient)
## License
CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

BIN
docs/images/compiled.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

BIN
docs/images/settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

View File

@ -2,7 +2,7 @@
From [firmware version 3.0.10](firmwares) onwards, the AirGradient ONE and Open Air monitors have below API available.
#### Discovery
### Discovery
The monitors run a mDNS discovery. So within the same network, the monitor can be accessed through:
@ -11,95 +11,249 @@ http://airgradient_{{serialnumber}}.local
The following requests are possible:
#### Get Current Air Quality (GET)
### Get Current Air Quality (GET)
With the path "/measures/current" you can get the current air quality data.
http://airgradient_ecda3b1eaaaf.local/measures/current
“ecda3b1eaaaf” being the serial number of your monitor
“ecda3b1eaaaf” being the serial number of your monitor.
You get the following response:
~~~
{"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"}
~~~
```json
{
"wifi": -46,
"serialno": "ecda3b1eaaaf",
"rco2": 447,
"pm01": 3,
"pm02": 7,
"pm10": 8,
"pm003Count": 442,
"atmp": 25.87,
"atmpCompensated": 24.47,
"rhum": 43,
"rhumCompensated": 49,
"tvocIndex": 100,
"tvocRaw": 33051,
"noxIndex": 1,
"noxRaw": 16307,
"boot": 6,
"bootCount": 6,
"ledMode": "pm",
"firmware": "3.1.3",
"model": "I-9PSL",
"monitorDisplayCompensatedValues": true
}
```
|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|
| Properties | Type | Explanation |
|-----------------------------------|---------|----------------------------------------------------------------------------------------|
| `serialno` | String | Serial Number of the monitor |
| `wifi` | Number | WiFi signal strength |
| `pm01` | Number | PM1.0 in ug/m3 (atmospheric environment) |
| `pm02` | Number | PM2.5 in ug/m3 (atmospheric environment) |
| `pm10` | Number | PM10 in ug/m3 (atmospheric environment) |
| `pm02Compensated` | Number | PM2.5 in ug/m3 with correction applied (from fw version 3.1.4 onwards) |
| `pm01Standard` | Number | PM1.0 in ug/m3 (standard particle) |
| `pm02Standard` | Number | PM2.5 in ug/m3 (standard particle) |
| `pm10Standard` | Number | PM10 in ug/m3 (standard particle) |
| `rco2` | Number | CO2 in ppm |
| `pm003Count` | Number | Particle count 0.3um per dL |
| `pm005Count` | Number | Particle count 0.5um per dL |
| `pm01Count` | Number | Particle count 1.0um per dL |
| `pm02Count` | Number | Particle count 2.5um per dL |
| `pm50Count` | Number | Particle count 5.0um per dL (only for indoor monitor) |
| `pm10Count` | Number | Particle count 10um per dL (only for indoor monitor) |
| `atmp` | Number | Temperature in Degrees Celsius |
| `atmpCompensated` | Number | Temperature in Degrees Celsius with correction applied |
| `rhum` | Number | Relative Humidity |
| `rhumCompensated` | Number | Relative Humidity with correction applied |
| `tvocIndex` | Number | Senisiron VOC Index |
| `tvocRaw` | Number | VOC raw value |
| `noxIndex` | Number | Senisirion NOx Index |
| `noxRaw` | Number | NOx raw value |
| `boot` | Number | Counts every measurement cycle. Low boot counts indicate restarts. |
| `bootCount` | Number | Same as boot property. Required for Home Assistant compatability. (deprecated soon!) |
| `ledMode` | String | Current configuration of the LED mode |
| `firmware` | String | Current firmware version |
| `model` | 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}
~~~
Compensated values apply correction algorithms to make the sensor values more accurate. Temperature and relative humidity correction is only applied on the outdoor monitor Open Air but the properties _compensated will still be send also for the indoor monitor AirGradient ONE.
#### Set Configuration Parameters (PUT)
### Get Configuration Parameters (GET)
Configuration parameters can be changed with a put request to the monitor, e.g.
"/config" path returns the current configuration of the monitor.
```json
{
"country": "TH",
"pmStandard": "ugm3",
"ledBarMode": "pm",
"abcDays": 7,
"tvocLearningOffset": 12,
"noxLearningOffset": 12,
"mqttBrokerUrl": "",
"httpDomain": "",
"temperatureUnit": "c",
"configurationControl": "local",
"postDataToAirGradient": true,
"ledBarBrightness": 100,
"displayBrightness": 100,
"offlineMode": false,
"model": "I-9PSL",
"monitorDisplayCompensatedValues": true,
"corrections": {
"pm02": {
"correctionAlgorithm": "epa_2021",
"slr": {}
}
}
}
}
```
### 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 ```
```bash
curl -X PUT -H "Content-Type: application/json" -d '{"co2CalibrationRequested":true}' http://airgradient_84fce612eff4.local/config
```
Example to set monitor to Celcius
Example to set monitor to Celsius
```curl -X PUT -H "Content-Type: application/json" -d '{"temperatureUnit":"c"}' http://airgradient_84fce612eff4.local/config ```
```bash
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.
If you use command prompt on Windows, you need to escape the quotes:
``` -d "{\"param\":\"value\"}" ```
#### Configuration Parameters (GET/PUT)
### Avoiding Conflicts with Configuration on AirGradient Server
|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}|
If the monitor is set up on the AirGradient dashboard, it will also receive the configuration parameters from there. In case you do not want this, please set `configurationControl` to `local`. In case you set it to `cloud` and want to change it to `local`, you need to make a factory reset.
### Configuration Parameters (GET/PUT)
| Properties | Description | Type | Accepted Values | Example |
|-----------------------------------|:-----------------------------------------------------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------|
| `country` | Country where the device is. | String | Country code as [ALPHA-2 notation](https://www.iban.com/country-codes) | `{"country": "TH"}` |
| `model` | Hardware identifier (only GET). | String | I-9PSL-DE | `{"model": "I-9PSL-DE"}` |
| `pmStandard` | Particle matter standard used on the display. | String | `ugm3`: ug/m3 <br> `us-aqi`: USAQI | `{"pmStandard": "ugm3"}` |
| `ledBarMode` | Mode in which the led bar can be set. | String | `co2`: LED bar displays CO2 <br>`pm`: LED bar displays PM <br>`off`: Turn off LED bar | `{"ledBarMode": "off"}` |
| `displayBrightness` | Brightness of the Display. | Number | 0-100 | `{"displayBrightness": 50}` |
| `ledBarBrightness` | Brightness of the LEDBar. | Number | 0-100 | `{"ledBarBrightness": 40}` |
| `abcDays` | Number of days for CO2 automatic baseline calibration. | Number | Maximum 200 days. Default 8 days. | `{"abcDays": 8}` |
| `mqttBrokerUrl` | MQTT broker URL. | String | Maximum 255 characters. Set value to empty string to disable mqtt connection. | `{"mqttBrokerUrl": "mqtt://192.168.0.18:1883"}` |
| `httpDomain` | Domain name for http request. (version > 3.3.2) | String | Maximum 255 characters. Set value to empty string to set http domain to default airgradient | `{"httpDomain": "sub.domain.com"}` |
| `temperatureUnit` | Temperature unit shown on the display. | String | `c` or `C`: Degree Celsius °C <br>`f` or `F`: Degree Fahrenheit °F | `{"temperatureUnit": "c"}` |
| `configurationControl` | The configuration source of the device. | String | `both`: Accept local and cloud configuration <br>`local`: Accept only local configuration <br>`cloud`: Accept only cloud configuration | `{"configurationControl": "both"}` |
| `postDataToAirGradient` | Send data to AirGradient cloud. | Boolean | `true`: Enabled <br>`false`: Disabled | `{"postDataToAirGradient": true}` |
| `co2CalibrationRequested` | Can be set to trigger a calibration. | Boolean | `true`: CO2 calibration (400ppm) will be triggered | `{"co2CalibrationRequested": true}` |
| `ledBarTestRequested` | Can be set to trigger a test. | Boolean | `true` : LEDs will run test sequence | `{"ledBarTestRequested": true}` |
| `noxLearningOffset` | Set NOx learning gain offset. | Number | 0-720 (default 12) | `{"noxLearningOffset": 12}` |
| `tvocLearningOffset` | Set VOC learning gain offset. | Number | 0-720 (default 12) | `{"tvocLearningOffset": 12}` |
| `monitorDisplayCompensatedValues` | Set the display show the PM value with/without compensate value (only on 3.1.9) | Boolean | `false`: Without compensate (default) <br> `true`: with compensate | `{"monitorDisplayCompensatedValues": false }` |
| `corrections` | Sets correction options to display and measurement values on local server response. (version >= 3.1.11) | Object | _see corrections section_ | _see corrections section_ |
**Notes**
- `offlineMode` : the device will disable all network operation, and only show measurements on the display and ledbar; Read-Only; Change can be apply using reset button on boot.
- `disableCloudConnection` : disable every request to AirGradient server, means features like post data to AirGradient server, configuration from AirGradient server and automatic firmware updates are disabled. This configuration overrides `configurationControl` and `postDataToAirGradient`; Read-Only; Change can be apply from wifi setup webpage.
### Corrections
The `corrections` object allows configuring PM2.5, Temperature and Humidity correction algorithms and parameters locally. This affects both the display, local server response and open metrics values.
Example correction configuration:
```json
{
"corrections": {
"pm02": {
"correctionAlgorithm": "<Option In String>",
"slr": {
"intercept": 0,
"scalingFactor": 0,
"useEpa2021": false
}
},
"atmp": {
"correctionAlgorithm": "<Option In String>",
"slr": {
"intercept": 0,
"scalingFactor": 0
}
},
"rhum": {
"correctionAlgorithm": "<Option In String>",
"slr": {
"intercept": 0,
"scalingFactor": 0
}
},
}
}
```
#### PM 2.5
Field Name: `pm02`
| Algorithm | Value | Description | SLR required |
|------------|-------------|------|---------|
| Raw | `"none"` | No correction (default) | No |
| EPA 2021 | `"epa_2021"` | Use EPA 2021 correction factors on top of raw value | No |
| PMS5003_20240104 | `"slr_PMS5003_20240104"` | Correction for PMS5003 sensor batch 20240104| Yes |
| PMS5003_20231218 | `"slr_PMS5003_20231218"` | Correction for PMS5003 sensor batch 20231218| Yes |
| PMS5003_20231030 | `"slr_PMS5003_20231030"` | Correction for PMS5003 sensor batch 20231030| Yes |
**NOTES**:
- Set `useEpa2021` to `true` if want to apply EPA 2021 correction factors on top of SLR correction value, otherwise `false`
- `intercept` and `scalingFactor` values can be obtained from [this article](https://www.airgradient.com/blog/low-readings-from-pms5003/)
- If `configurationControl` is set to `local` (eg. when using Home Assistant), correction need to be set manually, see examples below
**Examples**:
- PMS5003_20231030
```bash
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20231030","slr":{"intercept":0,"scalingFactor":0.02838,"useEpa2021":true}}}}'
```
- PMS5003_20231218
```bash
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20231218","slr":{"intercept":0,"scalingFactor":0.03525,"useEpa2021":true}}}}'
```
- PMS5003_20240104
```bash
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"pm02":{"correctionAlgorithm":"slr_PMS5003_20240104","slr":{"intercept":0,"scalingFactor":0.02896,"useEpa2021":true}}}}'
```
#### Temperature & Humidity
Field Name:
- Temperature: `atmp`
- Humidity: `rhum`
| Algorithm | Value | Description | SLR required |
|------------|-------------|------|---------|
| Raw | `"none"` | No correction (default) | No |
| AirGradient Standard Correction | `"ag_pms5003t_2024"` | Using standard airgradient correction (for outdoor monitor)| No |
| Custom | `"custom"` | custom corrections constant, set `intercept` and `scalingFactor` manually | Yes |
*Table above apply for both Temperature and Humidity*
**Example**
```bash
curl --location -X PUT 'http://airgradient_84fce612eff4.local/config' --header 'Content-Type: application/json' --data '{"corrections":{"atmp":{"correctionAlgorithm":"custom","slr":{"intercept":0.2,"scalingFactor":1.1}}}}'
```

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

@ -0,0 +1,22 @@
## OTA Updates
From [firmware version 3.1.1](https://github.com/airgradienthq/arduino/tree/3.1.1) onwards, the AirGradient ONE and Open Air monitors support over the air (OTA) updates.
#### Mechanism
Upon compilation of an official release the git tag (GIT_VERSION) is compiled into the binary.
The device attempts to update to the latest version on startup and in regular intervals using URL
http://hw.airgradient.com/sensors/{deviceId}/generic/os/firmware.bin?current_firmware={GIT_VERSION}
If does pass the version it is currently running on along to the server through URL parameter 'current_firmware'.
This allows the server to identify if the device is already running on the latest version or should update.
The following scenarios are possible
1. The device is already on the latest firmware. Then the server returns a 304 with a short explanation text in the body saying this.
2. The device reports a firmware unknown to the server. A 400 with an empty payload is returned in this case and the update is not performed. This case is relevant for local changes. The GIT_VERSION then defaults to "snapshot" which is unknown to the server.
3. There is an update available. A 200 along with the binary data of the new version is returned and the update is performed.
More information about the implementation details are available here: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/ota.html

View File

@ -12,10 +12,8 @@ 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"
Compile Instructions:
https://github.com/airgradienthq/arduino/blob/master/docs/howto-compile.md
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
can be set through the AirGradient dashboard.
@ -31,187 +29,384 @@ CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
#include "AgConfigure.h"
#include "AgSchedule.h"
#include "AgWiFiConnector.h"
#include "LocalServer.h"
#include "OpenMetrics.h"
#include "MqttClient.h"
#include <AirGradient.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <WiFiClient.h>
#define 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 \
*/
#define LED_BAR_ANIMATION_PERIOD 100 /** ms */
#define DISP_UPDATE_INTERVAL 2500 /** ms */
#define SERVER_CONFIG_SYNC_INTERVAL 60000 /** ms */
#define SERVER_SYNC_INTERVAL 60000 /** ms */
#define MQTT_SYNC_INTERVAL 60000 /** ms */
#define SENSOR_CO2_CALIB_COUNTDOWN_MAX 5 /** sec */
#define SENSOR_TVOC_UPDATE_INTERVAL 1000 /** ms */
#define SENSOR_CO2_UPDATE_INTERVAL 4000 /** ms */
#define SENSOR_PM_UPDATE_INTERVAL 2000 /** ms */
#define SENSOR_TEMP_HUM_UPDATE_INTERVAL 6000 /** ms */
#define DISPLAY_DELAY_SHOW_CONTENT_MS 2000 /** ms */
/** Create airgradient instance for 'DIY_BASIC' board */
static AirGradient ag = AirGradient(DIY_BASIC);
static AirGradient ag(DIY_BASIC);
static Configuration configuration(Serial);
static AgApiClient apiClient(Serial, configuration);
static WifiConnector wifiConnector(Serial);
static Measurements measurements(configuration);
static OledDisplay oledDisplay(configuration, measurements, Serial);
static StateMachine stateMachine(oledDisplay, Serial, measurements,
configuration);
static WifiConnector wifiConnector(oledDisplay, Serial, stateMachine,
configuration);
static OpenMetrics openMetrics(measurements, configuration, wifiConnector,
apiClient);
static LocalServer localServer(Serial, openMetrics, measurements, configuration,
wifiConnector);
static MqttClient mqttClient(Serial);
static int co2Ppm = -1;
static int pm25 = -1;
static float temp = -1001;
static int hum = -1;
static long val;
static AgFirmwareMode fwMode = FW_MODE_I_BASIC_40PS;
static String fwNewVersion;
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 configurationUpdateSchedule(void);
static void appDispHandler(void);
static void oledDisplaySchedule(void);
static void updateTvoc(void);
static void updatePm(void);
static void sendDataToServer(void);
static void dispHandler(void);
static String getDevId(void);
static void showNr(void);
static void tempHumUpdate(void);
static void co2Update(void);
static void mdnsInit(void);
static void initMqtt(void);
static void factoryConfigReset(void);
static void wdgFeedUpdate(void);
static bool sgp41Init(void);
static void wifiFactoryConfigure(void);
static void mqttHandle(void);
static int calculateMaxPeriod(int updateInterval);
static void setMeasurementMaxPeriod();
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 dispLedSchedule(DISP_UPDATE_INTERVAL, oledDisplaySchedule);
AgSchedule configSchedule(SERVER_CONFIG_SYNC_INTERVAL,
configurationUpdateSchedule);
AgSchedule agApiPostSchedule(SERVER_SYNC_INTERVAL, sendDataToServer);
AgSchedule co2Schedule(SENSOR_CO2_UPDATE_INTERVAL, co2Update);
AgSchedule pmsSchedule(SENSOR_PM_UPDATE_INTERVAL, pmUpdate);
AgSchedule pmsSchedule(SENSOR_PM_UPDATE_INTERVAL, updatePm);
AgSchedule tempHumSchedule(SENSOR_TEMP_HUM_UPDATE_INTERVAL, tempHumUpdate);
AgSchedule tvocSchedule(SENSOR_TVOC_UPDATE_INTERVAL, updateTvoc);
AgSchedule watchdogFeedSchedule(60000, wdgFeedUpdate);
AgSchedule mqttSchedule(MQTT_SYNC_INTERVAL, mqttHandle);
void setup() {
/** Serial for print debug message */
Serial.begin(115200);
showNr();
delay(100); /** For bester show log */
/** Print device ID into log */
Serial.println("Serial nr: " + ag.deviceId());
/** Initialize local configure */
configuration.begin();
/** Init I2C */
Wire.begin(ag.getI2cSdaPin(), ag.getI2cSclPin());
delay(1000);
/** Board init */
boardInit();
/** Init AirGradient server */
apiClient.begin();
apiClient.setAirGradient(&ag);
configuration.setAirGradient(&ag);
oledDisplay.setAirGradient(&ag);
stateMachine.setAirGradient(&ag);
wifiConnector.setAirGradient(&ag);
apiClient.setAirGradient(&ag);
openMetrics.setAirGradient(&ag);
localServer.setAirGraident(&ag);
measurements.setAirGradient(&ag);
/** Show boot display */
displayShowText("DIY basic", "Lib:" + ag.getVersion(), "");
delay(2000);
/** Example set custom API root URL */
// apiClient.setApiRoot("https://example.custom.api");
/** WiFi connect */
// connectToWifi();
if (wifiConnector.connect()) {
if (WiFi.status() == WL_CONNECTED) {
sendDataToAg();
/** Init sensor */
boardInit();
setMeasurementMaxPeriod();
apiClient.fetchServerConfiguration();
if (configuration.isCo2CalibrationRequested()) {
executeCo2Calibration();
// Uncomment below line to print every measurements reading update
// measurements.setDebug(true);
/** Connecting wifi */
bool connectToWifi = false;
connectToWifi = !configuration.isOfflineMode();
if (connectToWifi) {
apiClient.begin();
if (wifiConnector.connect()) {
if (wifiConnector.isConnected()) {
mdnsInit();
localServer.begin();
initMqtt();
sendDataToAg();
if (configuration.getConfigurationControl() !=
ConfigurationControl::ConfigurationControlLocal) {
apiClient.fetchServerConfiguration();
}
configSchedule.update();
if (apiClient.isFetchConfigurationFailed()) {
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
stateMachine.displayHandle(
AgStateMachineWiFiOkServerOkSensorConfigFailed);
} else {
stateMachine.displayClearAddToDashBoard();
}
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
}
} else {
if (wifiConnector.isConfigurePorttalTimeout()) {
oledDisplay.showRebooting();
delay(2500);
oledDisplay.setText("", "", "");
ESP.restart();
}
}
}
}
/** 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();
/** Set offline mode without saving, cause wifi is not configured */
if (wifiConnector.hasConfigurated() == false) {
Serial.println("Set offline mode cause wifi is not configurated");
configuration.setOfflineModeWithoutSave(true);
}
delay(5000);
/** Show display Warning up */
String sn = "SN:" + ag.deviceId();
oledDisplay.setText("Warming Up", sn.c_str(), "");
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
Serial.println("Display brightness: " +
String(configuration.getDisplayBrightness()));
oledDisplay.setBrightness(configuration.getDisplayBrightness());
appDispHandler();
}
void loop() {
/** Handle schedule */
dispLedSchedule.run();
configSchedule.run();
serverSchedule.run();
dispSchedule.run();
if (hasSensorS8) {
agApiPostSchedule.run();
if (configuration.hasSensorS8) {
co2Schedule.run();
}
if (hasSensorPMS) {
if (configuration.hasSensorPMS1) {
pmsSchedule.run();
ag.pms5003.handle();
}
if (hasSensorSHT) {
if (configuration.hasSensorSHT) {
tempHumSchedule.run();
}
if (configuration.hasSensorSGP) {
tvocSchedule.run();
}
watchdogFeedSchedule.run();
/** Check for handle WiFi reconnect */
wifiConnector.handle();
/** Read PMS on loop */
ag.pms5003.handle();
/** factory reset handle */
// factoryConfigReset();
/** check that local configura changed then do some action */
configUpdateHandle();
localServer._handle();
if (configuration.hasSensorSGP) {
ag.sgp41.handle();
}
MDNS.update();
mqttSchedule.run();
mqttClient.handle();
}
static void co2Update(void) {
if (!configuration.hasSensorS8) {
// Device don't have S8 sensor
return;
}
int value = ag.s8.getCo2();
if (utils::isValidCO2(value)) {
measurements.update(Measurements::CO2, value);
} else {
measurements.update(Measurements::CO2, utils::getInvalidCO2());
}
}
static void mdnsInit(void) {
Serial.println("mDNS init");
if (!MDNS.begin(localServer.getHostname().c_str())) {
Serial.println("Init mDNS failed");
return;
}
MDNS.addService("_airgradient", "_tcp", 80);
MDNS.addServiceTxt("_airgradient", "_tcp", "model",
AgFirmwareModeName(fwMode));
MDNS.addServiceTxt("_airgradient", "_tcp", "serialno", ag.deviceId());
MDNS.addServiceTxt("_airgradient", "_tcp", "fw_ver", ag.getVersion());
MDNS.addServiceTxt("_airgradient", "_tcp", "vendor", "AirGradient");
MDNS.announce();
}
static void initMqtt(void) {
String mqttUri = configuration.getMqttBrokerUri();
if (mqttUri.isEmpty()) {
Serial.println(
"MQTT is not configured, skipping initialization of MQTT client");
return;
}
if (mqttClient.begin(mqttUri)) {
Serial.println("Successfully connected to MQTT broker");
} else {
Serial.println("Connection to MQTT broker failed");
}
}
static void wdgFeedUpdate(void) {
ag.watchdog.reset();
Serial.println("External watchdog feed!");
}
static bool sgp41Init(void) {
ag.sgp41.setNoxLearningOffset(configuration.getNoxLearningOffset());
ag.sgp41.setTvocLearningOffset(configuration.getTvocLearningOffset());
if (ag.sgp41.begin(Wire)) {
Serial.println("Init SGP41 success");
configuration.hasSensorSGP = true;
return true;
} else {
Serial.println("Init SGP41 failuire");
configuration.hasSensorSGP = false;
}
return false;
}
static void wifiFactoryConfigure(void) {
WiFi.persistent(true);
WiFi.begin("airgradient", "cleanair");
WiFi.persistent(false);
oledDisplay.setText("Configure WiFi", "connect to", "\'airgradient\'");
delay(2500);
oledDisplay.setText("Rebooting...", "", "");
delay(2500);
oledDisplay.setText("", "", "");
ESP.restart();
}
static void mqttHandle(void) {
if(mqttClient.isConnected() == false) {
mqttClient.connect(String("airgradient-") + ag.deviceId());
}
if (mqttClient.isConnected()) {
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI());
String topic = "airgradient/readings/" + ag.deviceId();
if (mqttClient.publish(topic.c_str(), payload.c_str(), payload.length())) {
Serial.println("MQTT sync success");
} else {
Serial.println("MQTT sync failure");
}
}
}
static void sendDataToAg() {
// delay(1500);
if (apiClient.sendPing(wifiConnector.RSSI(), 0)) {
// Ping Server succses
/** Change oledDisplay and led state */
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting);
delay(1500);
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount())) {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected);
} else {
// Ping server failed
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed);
}
// delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
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);
void dispSensorNotFound(String ss) {
oledDisplay.setText("Sensor", ss.c_str(), "not found");
delay(2000);
}
static void boardInit(void) {
/** Init SHT sensor */
/** Display init */
oledDisplay.begin();
/** Show boot display */
Serial.println("Firmware Version: " + ag.getVersion());
if (ag.isBasic()) {
oledDisplay.setText("DIY Basic", ag.getVersion().c_str(), "");
} else {
oledDisplay.setText("AirGradient ONE",
"FW Version: ", ag.getVersion().c_str());
}
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
ag.watchdog.begin();
/** Show message init sensor */
oledDisplay.setText("Sensor", "init...", "");
/** Init sensor SGP41 */
configuration.hasSensorSGP = false;
// if (sgp41Init() == false) {
// dispSensorNotFound("SGP41");
// }
/** Init SHT */
if (ag.sht.begin(Wire) == false) {
hasSensorSHT = false;
Serial.println("SHT sensor not found");
Serial.println("SHTx sensor not found");
configuration.hasSensorSHT = false;
dispSensorNotFound("SHT");
}
/** CO2 init */
/** Init S8 CO2 sensor */
if (ag.s8.begin(&Serial) == false) {
Serial.println("CO2 S8 snsor not found");
hasSensorS8 = false;
Serial.println("CO2 S8 sensor not found");
configuration.hasSensorS8 = false;
dispSensorNotFound("S8");
}
/** PMS init */
/** Init PMS5003 */
configuration.hasSensorPMS1 = true;
configuration.hasSensorPMS2 = false;
if (ag.pms5003.begin(&Serial) == false) {
Serial.println("PMS sensor not found");
hasSensorPMS = false;
configuration.hasSensorPMS1 = false;
dispSensorNotFound("PMS");
}
/** Display init */
ag.display.begin(Wire);
ag.display.setTextColor(1);
ag.display.clear();
ag.display.show();
delay(100);
/** 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) {
@ -221,181 +416,189 @@ static void failedHandler(String msg) {
}
}
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);
static void configurationUpdateSchedule(void) {
if (configuration.isOfflineMode() ||
configuration.getConfigurationControl() == ConfigurationControl::ConfigurationControlLocal) {
Serial.println("Ignore fetch server configuration. Either mode is offline "
"or configurationControl set to local");
apiClient.resetFetchConfigurationStatus();
return;
}
if (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");
configUpdateHandle();
}
}
static void configUpdateHandle() {
if (configuration.isUpdated() == false) {
return;
}
stateMachine.executeCo2Calibration();
String mqttUri = configuration.getMqttBrokerUri();
if (mqttClient.isCurrentUri(mqttUri) == false) {
mqttClient.end();
initMqtt();
}
if (configuration.hasSensorSGP) {
if (configuration.noxLearnOffsetChanged() ||
configuration.tvocLearnOffsetChanged()) {
ag.sgp41.end();
int oldTvocOffset = ag.sgp41.getTvocLearningOffset();
int oldNoxOffset = ag.sgp41.getNoxLearningOffset();
bool result = sgp41Init();
const char *resultStr = "successful";
if (!result) {
resultStr = "failure";
}
}
if (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");
if (oldTvocOffset != configuration.getTvocLearningOffset()) {
Serial.printf("Setting tvocLearningOffset from %d to %d hours %s\r\n",
oldTvocOffset, configuration.getTvocLearningOffset(),
resultStr);
}
if (oldNoxOffset != configuration.getNoxLearningOffset()) {
Serial.printf("Setting noxLearningOffset from %d to %d hours %s\r\n",
oldNoxOffset, configuration.getNoxLearningOffset(),
resultStr);
}
}
}
if (configuration.isDisplayBrightnessChanged()) {
oledDisplay.setBrightness(configuration.getDisplayBrightness());
}
appDispHandler();
}
static void 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;
static void appDispHandler(void) {
AgStateMachineState state = AgStateMachineNormal;
/** Only show display status on online mode. */
if (configuration.isOfflineMode() == false) {
if (wifiConnector.isConnected() == false) {
state = AgStateMachineWiFiLost;
} else if (apiClient.isFetchConfigurationFailed()) {
state = AgStateMachineSensorConfigFailed;
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
} else {
stateMachine.displayClearAddToDashBoard();
}
} else if (apiClient.isPostToServerFailed()) {
state = AgStateMachineServerLost;
}
}
stateMachine.displayHandle(state);
}
static void oledDisplaySchedule(void) {
appDispHandler();
}
static void updateTvoc(void) {
if (!configuration.hasSensorSGP) {
return;
}
measurements.update(Measurements::TVOC, ag.sgp41.getTvocIndex());
measurements.update(Measurements::TVOCRaw, ag.sgp41.getTvocRaw());
measurements.update(Measurements::NOx, ag.sgp41.getNoxIndex());
measurements.update(Measurements::NOxRaw, ag.sgp41.getNoxRaw());
}
static void updatePm(void) {
if (ag.pms5003.connected()) {
measurements.update(Measurements::PM01, ag.pms5003.getPm01Ae());
measurements.update(Measurements::PM25, ag.pms5003.getPm25Ae());
measurements.update(Measurements::PM10, ag.pms5003.getPm10Ae());
measurements.update(Measurements::PM03_PC, ag.pms5003.getPm03ParticleCount());
} else {
measurements.update(Measurements::PM01, utils::getInvalidPmValue());
measurements.update(Measurements::PM25, utils::getInvalidPmValue());
measurements.update(Measurements::PM10, utils::getInvalidPmValue());
measurements.update(Measurements::PM03_PC, utils::getInvalidPmValue());
}
}
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 sendDataToServer(void) {
/** Increment bootcount when send measurements data is scheduled */
int bootCount = measurements.bootCount() + 1;
measurements.setBootCount(bootCount);
if (configuration.isOfflineMode() || !configuration.isPostDataToAirGradient()) {
Serial.println("Skipping transmission of data to AG server. Either mode is offline "
"or post data to server disabled");
return;
}
if (wifiConnector.isConnected() == false) {
Serial.println("WiFi not connected, skipping data transmission to AG server");
return;
}
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI());
if (apiClient.postToServer(syncData)) {
Serial.println();
Serial.println("Online mode and isPostToAirGradient = true");
Serial.println();
}
}
static void tempHumUpdate() {
static void tempHumUpdate(void) {
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");
}
}
float temp = ag.sht.getTemperature();
float rhum = ag.sht.getRelativeHumidity();
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 + "}";
measurements.update(Measurements::Temperature, temp);
measurements.update(Measurements::Humidity, 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));
// Update compensation temperature and humidity for SGP41
if (configuration.hasSensorSGP) {
ag.sgp41.setCompensationTemperatureHumidity(temp, rhum);
}
} else {
if (pm25 < 0) {
ln1 = "PM :- ug";
} else {
ln1 = "PM :" + String(pm25) + " ug";
}
measurements.update(Measurements::Temperature, utils::getInvalidTemperature());
measurements.update(Measurements::Humidity, utils::getInvalidHumidity());
Serial.println("SHT read failed");
}
if (co2Ppm > -1001) {
ln2 = "CO2:" + String(co2Ppm);
}
/* Set max period for each measurement type based on sensor update interval*/
void setMeasurementMaxPeriod() {
/// Max period for S8 sensors measurements
measurements.maxPeriod(Measurements::CO2, calculateMaxPeriod(SENSOR_CO2_UPDATE_INTERVAL));
/// Max period for SGP sensors measurements
measurements.maxPeriod(Measurements::TVOC, calculateMaxPeriod(SENSOR_TVOC_UPDATE_INTERVAL));
measurements.maxPeriod(Measurements::TVOCRaw, calculateMaxPeriod(SENSOR_TVOC_UPDATE_INTERVAL));
measurements.maxPeriod(Measurements::NOx, calculateMaxPeriod(SENSOR_TVOC_UPDATE_INTERVAL));
measurements.maxPeriod(Measurements::NOxRaw, calculateMaxPeriod(SENSOR_TVOC_UPDATE_INTERVAL));
/// Max period for PMS sensors measurements
measurements.maxPeriod(Measurements::PM25, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
measurements.maxPeriod(Measurements::PM01, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
measurements.maxPeriod(Measurements::PM10, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
measurements.maxPeriod(Measurements::PM03_PC, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
// Temperature and Humidity
if (configuration.hasSensorSHT) {
/// Max period for SHT sensors measurements
measurements.maxPeriod(Measurements::Temperature,
calculateMaxPeriod(SENSOR_TEMP_HUM_UPDATE_INTERVAL));
measurements.maxPeriod(Measurements::Humidity,
calculateMaxPeriod(SENSOR_TEMP_HUM_UPDATE_INTERVAL));
} else {
ln2 = "CO2: -";
/// Temp and hum data retrieved from PMS5003T sensor
measurements.maxPeriod(Measurements::Temperature,
calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
measurements.maxPeriod(Measurements::Humidity, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
}
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;
}
int calculateMaxPeriod(int updateInterval) {
// 0.5 is 50% reduced interval for max period
return (SERVER_SYNC_INTERVAL - (SERVER_SYNC_INTERVAL * 0.5)) / updateInterval;
}

View File

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

View File

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

View File

@ -0,0 +1,204 @@
#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.isFetchConfigurationFailed() ? "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()));
// Initialize default invalid value for each measurements
float _temp = utils::getInvalidTemperature();
float _hum = utils::getInvalidHumidity();
int pm01 = utils::getInvalidPmValue();
int pm25 = utils::getInvalidPmValue();
int pm10 = utils::getInvalidPmValue();
int pm03PCount = utils::getInvalidPmValue();
int co2 = utils::getInvalidCO2();
int atmpCompensated = utils::getInvalidTemperature();
int rhumCompensated = utils::getInvalidHumidity();
int tvoc = utils::getInvalidVOC();
int tvocRaw = utils::getInvalidVOC();
int nox = utils::getInvalidNOx();
int noxRaw = utils::getInvalidNOx();
if (config.hasSensorSHT) {
_temp = measure.getFloat(Measurements::Temperature);
_hum = measure.getFloat(Measurements::Humidity);
atmpCompensated = _temp;
rhumCompensated = _hum;
}
if (config.hasSensorPMS1) {
pm01 = measure.get(Measurements::PM01);
float correctedPm = measure.getCorrectedPM25(false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10);
pm03PCount = measure.get(Measurements::PM03_PC);
}
if (config.hasSensorSGP) {
tvoc = measure.get(Measurements::TVOC);
tvocRaw = measure.get(Measurements::TVOCRaw);
nox = measure.get(Measurements::NOx);
noxRaw = measure.get(Measurements::NOxRaw);
}
if (config.hasSensorS8) {
co2 = measure.get(Measurements::CO2);
}
if (config.hasSensorPMS1) {
if (utils::isValidPm(pm01)) {
add_metric("pm1",
"PM1.0 concentration as measured by the AirGradient PMS "
"sensor, in micrograms per cubic meter",
"gauge", "ugm3");
add_metric_point("", String(pm01));
}
if (utils::isValidPm(pm25)) {
add_metric("pm2d5",
"PM2.5 concentration as measured by the AirGradient PMS "
"sensor, in micrograms per cubic meter",
"gauge", "ugm3");
add_metric_point("", String(pm25));
}
if (utils::isValidPm(pm10)) {
add_metric("pm10",
"PM10 concentration as measured by the AirGradient PMS "
"sensor, in micrograms per cubic meter",
"gauge", "ugm3");
add_metric_point("", String(pm10));
}
if (utils::isValidPm03Count(pm03PCount)) {
add_metric("pm0d3",
"PM0.3 concentration as measured by the AirGradient PMS "
"sensor, in number of particules per 100 milliliters",
"gauge", "p100ml");
add_metric_point("", String(pm03PCount));
}
}
if (config.hasSensorSGP) {
if (utils::isValidVOC(tvoc)) {
add_metric("tvoc_index",
"The processed Total Volatile Organic Compounds (TVOC) index "
"as measured by the AirGradient SGP sensor",
"gauge");
add_metric_point("", String(tvoc));
}
if (utils::isValidVOC(tvocRaw)) {
add_metric("tvoc_raw",
"The raw input value to the Total Volatile Organic Compounds "
"(TVOC) index as measured by the AirGradient SGP sensor",
"gauge");
add_metric_point("", String(tvocRaw));
}
if (utils::isValidNOx(nox)) {
add_metric("nox_index",
"The processed Nitrous Oxide (NOx) index as measured by the "
"AirGradient SGP sensor",
"gauge");
add_metric_point("", String(nox));
}
if (utils::isValidNOx(noxRaw)) {
add_metric("nox_raw",
"The raw input value to the Nitrous Oxide (NOx) index as "
"measured by the AirGradient SGP sensor",
"gauge");
add_metric_point("", String(noxRaw));
}
}
if (utils::isValidCO2(co2)) {
add_metric("co2",
"Carbon dioxide concentration as measured by the AirGradient S8 "
"sensor, in parts per million",
"gauge", "ppm");
add_metric_point("", String(co2));
}
if (utils::isValidTemperature(_temp)) {
add_metric(
"temperature",
"The ambient temperature as measured by the AirGradient SHT / PMS "
"sensor, in degrees Celsius",
"gauge", "celsius");
add_metric_point("", String(_temp));
}
if (utils::isValidTemperature(atmpCompensated)) {
add_metric("temperature_compensated",
"The compensated ambient temperature as measured by the "
"AirGradient SHT / PMS "
"sensor, in degrees Celsius",
"gauge", "celsius");
add_metric_point("", String(atmpCompensated));
}
if (utils::isValidHumidity(_hum)) {
add_metric(
"humidity",
"The relative humidity as measured by the AirGradient SHT sensor",
"gauge", "percent");
add_metric_point("", String(_hum));
}
if (utils::isValidHumidity(rhumCompensated)) {
add_metric("humidity_compensated",
"The compensated relative humidity as measured by the "
"AirGradient SHT / PMS sensor",
"gauge", "percent");
add_metric_point("", String(rhumCompensated));
}
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,656 @@
/*
This is the code for the AirGradient DIY PRO 3.3 Air Quality Monitor with an D1
ESP8266 Microcontroller.
It is an air quality monitor for PM2.5, CO2, Temperature and Humidity with a
small display and can send data over Wifi.
Open source air quality monitors and kits are available:
Indoor Monitor: https://www.airgradient.com/indoor/
Outdoor Monitor: https://www.airgradient.com/outdoor/
Build Instructions:
https://www.airgradient.com/documentation/diy-v4/
Compile Instructions:
https://github.com/airgradienthq/arduino/blob/master/docs/howto-compile.md
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
can be set through the AirGradient dashboard.
If you have any questions please visit our forum at
https://forum.airgradient.com/
CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
*/
#include "AgApiClient.h"
#include "AgConfigure.h"
#include "AgSchedule.h"
#include "AgWiFiConnector.h"
#include "LocalServer.h"
#include "OpenMetrics.h"
#include "MqttClient.h"
#include <AirGradient.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <WiFiClient.h>
#define LED_BAR_ANIMATION_PERIOD 100 /** ms */
#define DISP_UPDATE_INTERVAL 2500 /** ms */
#define SERVER_CONFIG_SYNC_INTERVAL 60000 /** ms */
#define SERVER_SYNC_INTERVAL 60000 /** ms */
#define MQTT_SYNC_INTERVAL 60000 /** ms */
#define SENSOR_CO2_CALIB_COUNTDOWN_MAX 5 /** sec */
#define SENSOR_TVOC_UPDATE_INTERVAL 1000 /** ms */
#define SENSOR_CO2_UPDATE_INTERVAL 4000 /** ms */
#define SENSOR_PM_UPDATE_INTERVAL 2000 /** ms */
#define SENSOR_TEMP_HUM_UPDATE_INTERVAL 6000 /** ms */
#define DISPLAY_DELAY_SHOW_CONTENT_MS 2000 /** ms */
static AirGradient ag(DIY_PRO_INDOOR_V3_3);
static Configuration configuration(Serial);
static AgApiClient apiClient(Serial, configuration);
static Measurements measurements(configuration);
static OledDisplay oledDisplay(configuration, measurements, Serial);
static StateMachine stateMachine(oledDisplay, Serial, measurements,
configuration);
static WifiConnector wifiConnector(oledDisplay, Serial, stateMachine,
configuration);
static OpenMetrics openMetrics(measurements, configuration, wifiConnector,
apiClient);
static LocalServer localServer(Serial, openMetrics, measurements, configuration,
wifiConnector);
static MqttClient mqttClient(Serial);
static AgFirmwareMode fwMode = FW_MODE_I_33PS;
static String fwNewVersion;
static void boardInit(void);
static void failedHandler(String msg);
static void configurationUpdateSchedule(void);
static void appDispHandler(void);
static void oledDisplaySchedule(void);
static void updateTvoc(void);
static void updatePm(void);
static void sendDataToServer(void);
static void tempHumUpdate(void);
static void co2Update(void);
static void mdnsInit(void);
static void initMqtt(void);
static void factoryConfigReset(void);
static void wdgFeedUpdate(void);
static bool sgp41Init(void);
static void wifiFactoryConfigure(void);
static void mqttHandle(void);
static int calculateMaxPeriod(int updateInterval);
static void setMeasurementMaxPeriod();
AgSchedule dispLedSchedule(DISP_UPDATE_INTERVAL, oledDisplaySchedule);
AgSchedule configSchedule(SERVER_CONFIG_SYNC_INTERVAL,
configurationUpdateSchedule);
AgSchedule agApiPostSchedule(SERVER_SYNC_INTERVAL, sendDataToServer);
AgSchedule co2Schedule(SENSOR_CO2_UPDATE_INTERVAL, co2Update);
AgSchedule pmsSchedule(SENSOR_PM_UPDATE_INTERVAL, updatePm);
AgSchedule tempHumSchedule(SENSOR_TEMP_HUM_UPDATE_INTERVAL, tempHumUpdate);
AgSchedule tvocSchedule(SENSOR_TVOC_UPDATE_INTERVAL, updateTvoc);
AgSchedule watchdogFeedSchedule(60000, wdgFeedUpdate);
AgSchedule mqttSchedule(MQTT_SYNC_INTERVAL, mqttHandle);
void setup() {
/** Serial for print debug message */
Serial.begin(115200);
delay(100); /** For bester show log */
/** Print device ID into log */
Serial.println("Serial nr: " + ag.deviceId());
/** Initialize local configure */
configuration.begin();
/** Init I2C */
Wire.begin(ag.getI2cSdaPin(), ag.getI2cSclPin());
delay(1000);
configuration.setAirGradient(&ag);
oledDisplay.setAirGradient(&ag);
stateMachine.setAirGradient(&ag);
wifiConnector.setAirGradient(&ag);
apiClient.setAirGradient(&ag);
openMetrics.setAirGradient(&ag);
localServer.setAirGraident(&ag);
measurements.setAirGradient(&ag);
/** Example set custom API root URL */
// apiClient.setApiRoot("https://example.custom.api");
/** Init sensor */
boardInit();
setMeasurementMaxPeriod();
// Uncomment below line to print every measurements reading update
// measurements.setDebug(true);
/** Connecting wifi */
bool connectToWifi = false;
connectToWifi = !configuration.isOfflineMode();
if (connectToWifi) {
apiClient.begin();
if (wifiConnector.connect()) {
if (wifiConnector.isConnected()) {
mdnsInit();
localServer.begin();
initMqtt();
sendDataToAg();
if (configuration.getConfigurationControl() !=
ConfigurationControl::ConfigurationControlLocal) {
apiClient.fetchServerConfiguration();
}
configSchedule.update();
if (apiClient.isFetchConfigurationFailed()) {
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
stateMachine.displayHandle(
AgStateMachineWiFiOkServerOkSensorConfigFailed);
} else {
stateMachine.displayClearAddToDashBoard();
}
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
}
} else {
if (wifiConnector.isConfigurePorttalTimeout()) {
oledDisplay.showRebooting();
delay(2500);
oledDisplay.setText("", "", "");
ESP.restart();
}
}
}
}
/** Set offline mode without saving, cause wifi is not configured */
if (wifiConnector.hasConfigurated() == false) {
Serial.println("Set offline mode cause wifi is not configurated");
configuration.setOfflineModeWithoutSave(true);
}
/** Show display Warning up */
oledDisplay.setText("Warming Up", "Serial Number:", ag.deviceId().c_str());
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
Serial.println("Display brightness: " +
String(configuration.getDisplayBrightness()));
oledDisplay.setBrightness(configuration.getDisplayBrightness());
appDispHandler();
}
void loop() {
/** Handle schedule */
dispLedSchedule.run();
configSchedule.run();
agApiPostSchedule.run();
if (configuration.hasSensorS8) {
co2Schedule.run();
}
if (configuration.hasSensorPMS1) {
pmsSchedule.run();
ag.pms5003.handle();
}
if (configuration.hasSensorSHT) {
tempHumSchedule.run();
}
if (configuration.hasSensorSGP) {
tvocSchedule.run();
}
watchdogFeedSchedule.run();
/** Check for handle WiFi reconnect */
wifiConnector.handle();
/** factory reset handle */
// factoryConfigReset();
/** check that local configura changed then do some action */
configUpdateHandle();
localServer._handle();
if (configuration.hasSensorSGP) {
ag.sgp41.handle();
}
MDNS.update();
mqttSchedule.run();
mqttClient.handle();
}
static void co2Update(void) {
if (!configuration.hasSensorS8) {
// Device don't have S8 sensor
return;
}
int value = ag.s8.getCo2();
if (utils::isValidCO2(value)) {
measurements.update(Measurements::CO2, value);
} else {
measurements.update(Measurements::CO2, utils::getInvalidCO2());
}
}
static void mdnsInit(void) {
Serial.println("mDNS init");
if (!MDNS.begin(localServer.getHostname().c_str())) {
Serial.println("Init mDNS failed");
return;
}
MDNS.addService("_airgradient", "_tcp", 80);
MDNS.addServiceTxt("_airgradient", "_tcp", "model",
AgFirmwareModeName(fwMode));
MDNS.addServiceTxt("_airgradient", "_tcp", "serialno", ag.deviceId());
MDNS.addServiceTxt("_airgradient", "_tcp", "fw_ver", ag.getVersion());
MDNS.addServiceTxt("_airgradient", "_tcp", "vendor", "AirGradient");
MDNS.announce();
}
static void initMqtt(void) {
String mqttUri = configuration.getMqttBrokerUri();
if (mqttUri.isEmpty()) {
Serial.println(
"MQTT is not configured, skipping initialization of MQTT client");
return;
}
if (mqttClient.begin(mqttUri)) {
Serial.println("Successfully connected to MQTT broker");
} else {
Serial.println("Connection to MQTT broker failed");
}
}
static void factoryConfigReset(void) {
#if 0
if (ag.button.getState() == ag.button.BUTTON_PRESSED) {
if (factoryBtnPressTime == 0) {
factoryBtnPressTime = millis();
} else {
uint32_t ms = (uint32_t)(millis() - factoryBtnPressTime);
if (ms >= 2000) {
// Show display message: For factory keep for x seconds
if (ag.isOne() || ag.isPro4_2()) {
oledDisplay.setText("Factory reset", "keep pressed", "for 8 sec");
} else {
Serial.println("Factory reset, keep pressed for 8 sec");
}
int count = 7;
while (ag.button.getState() == ag.button.BUTTON_PRESSED) {
delay(1000);
String str = "for " + String(count) + " sec";
oledDisplay.setText("Factory reset", "keep pressed", str.c_str());
count--;
if (count == 0) {
/** Stop MQTT task first */
// if (mqttTask) {
// vTaskDelete(mqttTask);
// mqttTask = NULL;
// }
/** Reset WIFI */
// WiFi.enableSTA(true); // Incase offline mode
// WiFi.disconnect(true, true);
wifiConnector.reset();
/** Reset local config */
configuration.reset();
oledDisplay.setText("Factory reset", "successful", "");
delay(3000);
oledDisplay.setText("", "", "");
ESP.restart();
}
}
/** Show current content cause reset ignore */
factoryBtnPressTime = 0;
appDispHandler();
}
}
} else {
if (factoryBtnPressTime != 0) {
appDispHandler();
}
factoryBtnPressTime = 0;
}
#endif
}
static void wdgFeedUpdate(void) {
ag.watchdog.reset();
Serial.println("External watchdog feed!");
}
static bool sgp41Init(void) {
ag.sgp41.setNoxLearningOffset(configuration.getNoxLearningOffset());
ag.sgp41.setTvocLearningOffset(configuration.getTvocLearningOffset());
if (ag.sgp41.begin(Wire)) {
Serial.println("Init SGP41 success");
configuration.hasSensorSGP = true;
return true;
} else {
Serial.println("Init SGP41 failuire");
configuration.hasSensorSGP = false;
}
return false;
}
static void wifiFactoryConfigure(void) {
WiFi.persistent(true);
WiFi.begin("airgradient", "cleanair");
WiFi.persistent(false);
oledDisplay.setText("Configure WiFi", "connect to", "\'airgradient\'");
delay(2500);
oledDisplay.setText("Rebooting...", "", "");
delay(2500);
oledDisplay.setText("", "", "");
ESP.restart();
}
static void mqttHandle(void) {
if(mqttClient.isConnected() == false) {
mqttClient.connect(String("airgradient-") + ag.deviceId());
}
if (mqttClient.isConnected()) {
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI());
String topic = "airgradient/readings/" + ag.deviceId();
if (mqttClient.publish(topic.c_str(), payload.c_str(), payload.length())) {
Serial.println("MQTT sync success");
} else {
Serial.println("MQTT sync failure");
}
}
}
static void sendDataToAg() {
/** Change oledDisplay and led state */
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting);
delay(1500);
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount())) {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected);
} else {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed);
}
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
}
void dispSensorNotFound(String ss) {
ss = ss + " not found";
oledDisplay.setText("Sensor init", "Error:", ss.c_str());
delay(2000);
}
static void boardInit(void) {
/** Display init */
oledDisplay.begin();
/** Show boot display */
Serial.println("Firmware Version: " + ag.getVersion());
oledDisplay.setText("AirGradient ONE",
"FW Version: ", ag.getVersion().c_str());
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
ag.watchdog.begin();
/** Show message init sensor */
oledDisplay.setText("Sensor", "initializing...", "");
/** Init sensor SGP41 */
if (sgp41Init() == false) {
dispSensorNotFound("SGP41");
}
/** Init SHT */
if (ag.sht.begin(Wire) == false) {
Serial.println("SHTx sensor not found");
configuration.hasSensorSHT = false;
dispSensorNotFound("SHT");
}
/** Init S8 CO2 sensor */
if (ag.s8.begin(&Serial) == false) {
Serial.println("CO2 S8 sensor not found");
configuration.hasSensorS8 = false;
dispSensorNotFound("S8");
}
/** Init PMS5003 */
configuration.hasSensorPMS1 = true;
configuration.hasSensorPMS2 = false;
if (ag.pms5003.begin(&Serial) == false) {
Serial.println("PMS sensor not found");
configuration.hasSensorPMS1 = false;
dispSensorNotFound("PMS");
}
/** Set S8 CO2 abc days period */
if (configuration.hasSensorS8) {
if (ag.s8.setAbcPeriod(configuration.getCO2CalibrationAbcDays() * 24)) {
Serial.println("Set S8 AbcDays successful");
} else {
Serial.println("Set S8 AbcDays failure");
}
}
localServer.setFwMode(FW_MODE_I_33PS);
}
static void failedHandler(String msg) {
while (true) {
Serial.println(msg);
delay(1000);
}
}
static void configurationUpdateSchedule(void) {
if (configuration.isOfflineMode() ||
configuration.getConfigurationControl() == ConfigurationControl::ConfigurationControlLocal) {
Serial.println("Ignore fetch server configuration. Either mode is offline "
"or configurationControl set to local");
apiClient.resetFetchConfigurationStatus();
return;
}
if (apiClient.fetchServerConfiguration()) {
configUpdateHandle();
}
}
static void configUpdateHandle() {
if (configuration.isUpdated() == false) {
return;
}
stateMachine.executeCo2Calibration();
String mqttUri = configuration.getMqttBrokerUri();
if (mqttClient.isCurrentUri(mqttUri) == false) {
mqttClient.end();
initMqtt();
}
if (configuration.hasSensorSGP) {
if (configuration.noxLearnOffsetChanged() ||
configuration.tvocLearnOffsetChanged()) {
ag.sgp41.end();
int oldTvocOffset = ag.sgp41.getTvocLearningOffset();
int oldNoxOffset = ag.sgp41.getNoxLearningOffset();
bool result = sgp41Init();
const char *resultStr = "successful";
if (!result) {
resultStr = "failure";
}
if (oldTvocOffset != configuration.getTvocLearningOffset()) {
Serial.printf("Setting tvocLearningOffset from %d to %d hours %s\r\n",
oldTvocOffset, configuration.getTvocLearningOffset(),
resultStr);
}
if (oldNoxOffset != configuration.getNoxLearningOffset()) {
Serial.printf("Setting noxLearningOffset from %d to %d hours %s\r\n",
oldNoxOffset, configuration.getNoxLearningOffset(),
resultStr);
}
}
}
if (configuration.isDisplayBrightnessChanged()) {
oledDisplay.setBrightness(configuration.getDisplayBrightness());
}
appDispHandler();
}
static void appDispHandler(void) {
AgStateMachineState state = AgStateMachineNormal;
/** Only show display status on online mode. */
if (configuration.isOfflineMode() == false) {
if (wifiConnector.isConnected() == false) {
state = AgStateMachineWiFiLost;
} else if (apiClient.isFetchConfigurationFailed()) {
state = AgStateMachineSensorConfigFailed;
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
} else {
stateMachine.displayClearAddToDashBoard();
}
} else if (apiClient.isPostToServerFailed()) {
state = AgStateMachineServerLost;
}
}
stateMachine.displayHandle(state);
}
static void oledDisplaySchedule(void) {
appDispHandler();
}
static void updateTvoc(void) {
if (!configuration.hasSensorSGP) {
return;
}
measurements.update(Measurements::TVOC, ag.sgp41.getTvocIndex());
measurements.update(Measurements::TVOCRaw, ag.sgp41.getTvocRaw());
measurements.update(Measurements::NOx, ag.sgp41.getNoxIndex());
measurements.update(Measurements::NOxRaw, ag.sgp41.getNoxRaw());
}
static void updatePm(void) {
if (ag.pms5003.connected()) {
measurements.update(Measurements::PM01, ag.pms5003.getPm01Ae());
measurements.update(Measurements::PM25, ag.pms5003.getPm25Ae());
measurements.update(Measurements::PM10, ag.pms5003.getPm10Ae());
measurements.update(Measurements::PM03_PC, ag.pms5003.getPm03ParticleCount());
} else {
measurements.update(Measurements::PM01, utils::getInvalidPmValue());
measurements.update(Measurements::PM25, utils::getInvalidPmValue());
measurements.update(Measurements::PM10, utils::getInvalidPmValue());
measurements.update(Measurements::PM03_PC, utils::getInvalidPmValue());
}
}
static void sendDataToServer(void) {
/** Increment bootcount when send measurements data is scheduled */
int bootCount = measurements.bootCount() + 1;
measurements.setBootCount(bootCount);
if (configuration.isOfflineMode() || !configuration.isPostDataToAirGradient()) {
Serial.println("Skipping transmission of data to AG server. Either mode is offline "
"or post data to server disabled");
return;
}
if (wifiConnector.isConnected() == false) {
Serial.println("WiFi not connected, skipping data transmission to AG server");
return;
}
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI());
if (apiClient.postToServer(syncData)) {
Serial.println();
Serial.println("Online mode and isPostToAirGradient = true");
Serial.println();
}
}
static void tempHumUpdate(void) {
if (ag.sht.measure()) {
float temp = ag.sht.getTemperature();
float rhum = ag.sht.getRelativeHumidity();
measurements.update(Measurements::Temperature, temp);
measurements.update(Measurements::Humidity, rhum);
// Update compensation temperature and humidity for SGP41
if (configuration.hasSensorSGP) {
ag.sgp41.setCompensationTemperatureHumidity(temp, rhum);
}
} else {
measurements.update(Measurements::Temperature, utils::getInvalidTemperature());
measurements.update(Measurements::Humidity, utils::getInvalidHumidity());
Serial.println("SHT read failed");
}
}
/* Set max period for each measurement type based on sensor update interval*/
void setMeasurementMaxPeriod() {
/// Max period for S8 sensors measurements
measurements.maxPeriod(Measurements::CO2, calculateMaxPeriod(SENSOR_CO2_UPDATE_INTERVAL));
/// Max period for SGP sensors measurements
measurements.maxPeriod(Measurements::TVOC, calculateMaxPeriod(SENSOR_TVOC_UPDATE_INTERVAL));
measurements.maxPeriod(Measurements::TVOCRaw, calculateMaxPeriod(SENSOR_TVOC_UPDATE_INTERVAL));
measurements.maxPeriod(Measurements::NOx, calculateMaxPeriod(SENSOR_TVOC_UPDATE_INTERVAL));
measurements.maxPeriod(Measurements::NOxRaw, calculateMaxPeriod(SENSOR_TVOC_UPDATE_INTERVAL));
/// Max period for PMS sensors measurements
measurements.maxPeriod(Measurements::PM25, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
measurements.maxPeriod(Measurements::PM01, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
measurements.maxPeriod(Measurements::PM10, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
measurements.maxPeriod(Measurements::PM03_PC, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
// Temperature and Humidity
if (configuration.hasSensorSHT) {
/// Max period for SHT sensors measurements
measurements.maxPeriod(Measurements::Temperature,
calculateMaxPeriod(SENSOR_TEMP_HUM_UPDATE_INTERVAL));
measurements.maxPeriod(Measurements::Humidity,
calculateMaxPeriod(SENSOR_TEMP_HUM_UPDATE_INTERVAL));
} else {
/// Temp and hum data retrieved from PMS5003T sensor
measurements.maxPeriod(Measurements::Temperature,
calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
measurements.maxPeriod(Measurements::Humidity, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
}
}
int calculateMaxPeriod(int updateInterval) {
// 0.5 is 50% reduced interval for max period
return (SERVER_SYNC_INTERVAL - (SERVER_SYNC_INTERVAL * 0.5)) / updateInterval;
}

View File

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

View File

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

View File

@ -0,0 +1,205 @@
#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.isFetchConfigurationFailed() ? "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()));
// Initialize default invalid value for each measurements
float _temp = utils::getInvalidTemperature();
float _hum = utils::getInvalidHumidity();
int pm01 = utils::getInvalidPmValue();
int pm25 = utils::getInvalidPmValue();
int pm10 = utils::getInvalidPmValue();
int pm03PCount = utils::getInvalidPmValue();
int co2 = utils::getInvalidCO2();
int atmpCompensated = utils::getInvalidTemperature();
int rhumCompensated = utils::getInvalidHumidity();
int tvoc = utils::getInvalidVOC();
int tvocRaw = utils::getInvalidVOC();
int nox = utils::getInvalidNOx();
int noxRaw = utils::getInvalidNOx();
if (config.hasSensorSHT) {
_temp = measure.getFloat(Measurements::Temperature);
_hum = measure.getFloat(Measurements::Humidity);
atmpCompensated = _temp;
rhumCompensated = _hum;
}
if (config.hasSensorPMS1) {
pm01 = measure.get(Measurements::PM01);
float correctedPm = measure.getCorrectedPM25(false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10);
pm03PCount = measure.get(Measurements::PM03_PC);
}
if (config.hasSensorSGP) {
tvoc = measure.get(Measurements::TVOC);
tvocRaw = measure.get(Measurements::TVOCRaw);
nox = measure.get(Measurements::NOx);
noxRaw = measure.get(Measurements::NOxRaw);
}
if (config.hasSensorS8) {
co2 = measure.get(Measurements::CO2);
}
if (config.hasSensorPMS1) {
if (utils::isValidPm(pm01)) {
add_metric("pm1",
"PM1.0 concentration as measured by the AirGradient PMS "
"sensor, in micrograms per cubic meter",
"gauge", "ugm3");
add_metric_point("", String(pm01));
}
if (utils::isValidPm(pm25)) {
add_metric("pm2d5",
"PM2.5 concentration as measured by the AirGradient PMS "
"sensor, in micrograms per cubic meter",
"gauge", "ugm3");
add_metric_point("", String(pm25));
}
if (utils::isValidPm(pm10)) {
add_metric("pm10",
"PM10 concentration as measured by the AirGradient PMS "
"sensor, in micrograms per cubic meter",
"gauge", "ugm3");
add_metric_point("", String(pm10));
}
if (utils::isValidPm03Count(pm03PCount)) {
add_metric("pm0d3",
"PM0.3 concentration as measured by the AirGradient PMS "
"sensor, in number of particules per 100 milliliters",
"gauge", "p100ml");
add_metric_point("", String(pm03PCount));
}
}
if (config.hasSensorSGP) {
if (utils::isValidVOC(tvoc)) {
add_metric("tvoc_index",
"The processed Total Volatile Organic Compounds (TVOC) index "
"as measured by the AirGradient SGP sensor",
"gauge");
add_metric_point("", String(tvoc));
}
if (utils::isValidVOC(tvocRaw)) {
add_metric("tvoc_raw",
"The raw input value to the Total Volatile Organic Compounds "
"(TVOC) index as measured by the AirGradient SGP sensor",
"gauge");
add_metric_point("", String(tvocRaw));
}
if (utils::isValidNOx(nox)) {
add_metric("nox_index",
"The processed Nitrous Oxide (NOx) index as measured by the "
"AirGradient SGP sensor",
"gauge");
add_metric_point("", String(nox));
}
if (utils::isValidNOx(noxRaw)) {
add_metric("nox_raw",
"The raw input value to the Nitrous Oxide (NOx) index as "
"measured by the AirGradient SGP sensor",
"gauge");
add_metric_point("", String(noxRaw));
}
}
if (utils::isValidCO2(co2)) {
add_metric("co2",
"Carbon dioxide concentration as measured by the AirGradient S8 "
"sensor, in parts per million",
"gauge", "ppm");
add_metric_point("", String(co2));
}
if (utils::isValidTemperature(_temp)) {
add_metric(
"temperature",
"The ambient temperature as measured by the AirGradient SHT / PMS "
"sensor, in degrees Celsius",
"gauge", "celsius");
add_metric_point("", String(_temp));
}
if (utils::isValidTemperature(atmpCompensated)) {
add_metric("temperature_compensated",
"The compensated ambient temperature as measured by the "
"AirGradient SHT / PMS "
"sensor, in degrees Celsius",
"gauge", "celsius");
add_metric_point("", String(atmpCompensated));
}
if (utils::isValidHumidity(_hum)) {
add_metric(
"humidity",
"The relative humidity as measured by the AirGradient SHT sensor",
"gauge", "percent");
add_metric_point("", String(_hum));
}
if (utils::isValidHumidity(rhumCompensated)) {
add_metric("humidity_compensated",
"The compensated relative humidity as measured by the "
"AirGradient SHT / PMS sensor",
"gauge", "percent");
add_metric_point("", String(rhumCompensated));
}
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,697 @@
/*
This is the code for the AirGradient DIY PRO 4.2 Air Quality Monitor with an D1
ESP8266 Microcontroller.
It is an air quality monitor for PM2.5, CO2, Temperature and Humidity with a
small display and can send data over Wifi.
Open source air quality monitors and kits are available:
Indoor Monitor: https://www.airgradient.com/indoor/
Outdoor Monitor: https://www.airgradient.com/outdoor/
Build Instructions:
https://www.airgradient.com/documentation/diy-v4/
Compile Instructions:
https://github.com/airgradienthq/arduino/blob/master/docs/howto-compile.md
Configuration parameters, e.g. Celsius / Fahrenheit or PM unit (US AQI vs ug/m3)
can be set through the AirGradient dashboard.
If you have any questions please visit our forum at
https://forum.airgradient.com/
CC BY-SA 4.0 Attribution-ShareAlike 4.0 International License
*/
#include "AgApiClient.h"
#include "AgConfigure.h"
#include "AgSchedule.h"
#include "AgWiFiConnector.h"
#include "LocalServer.h"
#include "OpenMetrics.h"
#include "MqttClient.h"
#include <AirGradient.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <WiFiClient.h>
#define LED_BAR_ANIMATION_PERIOD 100 /** ms */
#define DISP_UPDATE_INTERVAL 2500 /** ms */
#define SERVER_CONFIG_SYNC_INTERVAL 60000 /** ms */
#define SERVER_SYNC_INTERVAL 60000 /** ms */
#define MQTT_SYNC_INTERVAL 60000 /** ms */
#define SENSOR_CO2_CALIB_COUNTDOWN_MAX 5 /** sec */
#define SENSOR_TVOC_UPDATE_INTERVAL 1000 /** ms */
#define SENSOR_CO2_UPDATE_INTERVAL 4000 /** ms */
#define SENSOR_PM_UPDATE_INTERVAL 2000 /** ms */
#define SENSOR_TEMP_HUM_UPDATE_INTERVAL 6000 /** ms */
#define DISPLAY_DELAY_SHOW_CONTENT_MS 2000 /** ms */
static AirGradient ag(DIY_PRO_INDOOR_V4_2);
static Configuration configuration(Serial);
static AgApiClient apiClient(Serial, configuration);
static Measurements measurements(configuration);
static OledDisplay oledDisplay(configuration, measurements, Serial);
static StateMachine stateMachine(oledDisplay, Serial, measurements,
configuration);
static WifiConnector wifiConnector(oledDisplay, Serial, stateMachine,
configuration);
static OpenMetrics openMetrics(measurements, configuration, wifiConnector,
apiClient);
static LocalServer localServer(Serial, openMetrics, measurements, configuration,
wifiConnector);
static MqttClient mqttClient(Serial);
static uint32_t factoryBtnPressTime = 0;
static AgFirmwareMode fwMode = FW_MODE_I_42PS;
static String fwNewVersion;
static void boardInit(void);
static void failedHandler(String msg);
static void configurationUpdateSchedule(void);
static void appDispHandler(void);
static void oledDisplaySchedule(void);
static void updateTvoc(void);
static void updatePm(void);
static void sendDataToServer(void);
static void tempHumUpdate(void);
static void co2Update(void);
static void mdnsInit(void);
static void initMqtt(void);
static void factoryConfigReset(void);
static void wdgFeedUpdate(void);
static bool sgp41Init(void);
static void wifiFactoryConfigure(void);
static void mqttHandle(void);
static int calculateMaxPeriod(int updateInterval);
static void setMeasurementMaxPeriod();
AgSchedule dispLedSchedule(DISP_UPDATE_INTERVAL, oledDisplaySchedule);
AgSchedule configSchedule(SERVER_CONFIG_SYNC_INTERVAL,
configurationUpdateSchedule);
AgSchedule agApiPostSchedule(SERVER_SYNC_INTERVAL, sendDataToServer);
AgSchedule co2Schedule(SENSOR_CO2_UPDATE_INTERVAL, co2Update);
AgSchedule pmsSchedule(SENSOR_PM_UPDATE_INTERVAL, updatePm);
AgSchedule tempHumSchedule(SENSOR_TEMP_HUM_UPDATE_INTERVAL, tempHumUpdate);
AgSchedule tvocSchedule(SENSOR_TVOC_UPDATE_INTERVAL, updateTvoc);
AgSchedule watchdogFeedSchedule(60000, wdgFeedUpdate);
AgSchedule mqttSchedule(MQTT_SYNC_INTERVAL, mqttHandle);
void setup() {
/** Serial for print debug message */
Serial.begin(115200);
delay(100); /** For bester show log */
/** Print device ID into log */
Serial.println("Serial nr: " + ag.deviceId());
/** Initialize local configure */
configuration.begin();
/** Init I2C */
Wire.begin(ag.getI2cSdaPin(), ag.getI2cSclPin());
delay(1000);
configuration.setAirGradient(&ag);
oledDisplay.setAirGradient(&ag);
stateMachine.setAirGradient(&ag);
wifiConnector.setAirGradient(&ag);
apiClient.setAirGradient(&ag);
openMetrics.setAirGradient(&ag);
localServer.setAirGraident(&ag);
measurements.setAirGradient(&ag);
/** Example set custom API root URL */
// apiClient.setApiRoot("https://example.custom.api");
/** Init sensor */
boardInit();
setMeasurementMaxPeriod();
// Uncomment below line to print every measurements reading update
// measurements.setDebug(true);
/** Connecting wifi */
bool connectToWifi = false;
/** Show message confirm offline mode, should me perform if LED bar button
* test pressed */
oledDisplay.setText(
"Press now for",
configuration.isOfflineMode() ? "online mode" : "offline mode", "");
uint32_t startTime = millis();
while (true) {
if (ag.button.getState() == ag.button.BUTTON_PRESSED) {
configuration.setOfflineMode(!configuration.isOfflineMode());
oledDisplay.setText(
"Offline Mode",
configuration.isOfflineMode() ? " = True" : " = False", "");
delay(1000);
break;
}
uint32_t periodMs = (uint32_t)(millis() - startTime);
if (periodMs >= 3000) {
Serial.println("Set for offline mode timeout");
break;
}
delay(1);
}
connectToWifi = !configuration.isOfflineMode();
if (connectToWifi) {
apiClient.begin();
if (wifiConnector.connect()) {
if (wifiConnector.isConnected()) {
mdnsInit();
localServer.begin();
initMqtt();
sendDataToAg();
if (configuration.getConfigurationControl() !=
ConfigurationControl::ConfigurationControlLocal) {
apiClient.fetchServerConfiguration();
}
configSchedule.update();
if (apiClient.isFetchConfigurationFailed()) {
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
stateMachine.displayHandle(
AgStateMachineWiFiOkServerOkSensorConfigFailed);
} else {
stateMachine.displayClearAddToDashBoard();
}
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
}
} else {
if (wifiConnector.isConfigurePorttalTimeout()) {
oledDisplay.showRebooting();
delay(2500);
oledDisplay.setText("", "", "");
ESP.restart();
}
}
}
}
/** Set offline mode without saving, cause wifi is not configured */
if (wifiConnector.hasConfigurated() == false) {
Serial.println("Set offline mode cause wifi is not configurated");
configuration.setOfflineModeWithoutSave(true);
}
/** Show display Warning up */
oledDisplay.setText("Warming Up", "Serial Number:", ag.deviceId().c_str());
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
Serial.println("Display brightness: " +
String(configuration.getDisplayBrightness()));
oledDisplay.setBrightness(configuration.getDisplayBrightness());
appDispHandler();
}
void loop() {
/** Handle schedule */
dispLedSchedule.run();
configSchedule.run();
agApiPostSchedule.run();
if (configuration.hasSensorS8) {
co2Schedule.run();
}
if (configuration.hasSensorPMS1) {
pmsSchedule.run();
ag.pms5003.handle();
}
if (configuration.hasSensorSHT) {
tempHumSchedule.run();
}
if (configuration.hasSensorSGP) {
tvocSchedule.run();
}
watchdogFeedSchedule.run();
/** Check for handle WiFi reconnect */
wifiConnector.handle();
/** factory reset handle */
factoryConfigReset();
/** check that local configura changed then do some action */
configUpdateHandle();
localServer._handle();
if (configuration.hasSensorSGP) {
ag.sgp41.handle();
}
MDNS.update();
mqttSchedule.run();
mqttClient.handle();
}
static void co2Update(void) {
if (!configuration.hasSensorS8) {
// Device don't have S8 sensor
return;
}
int value = ag.s8.getCo2();
if (utils::isValidCO2(value)) {
measurements.update(Measurements::CO2, value);
} else {
measurements.update(Measurements::CO2, utils::getInvalidCO2());
}
}
static void mdnsInit(void) {
Serial.println("mDNS init");
if (!MDNS.begin(localServer.getHostname().c_str())) {
Serial.println("Init mDNS failed");
return;
}
MDNS.addService("_airgradient", "_tcp", 80);
MDNS.addServiceTxt("_airgradient", "_tcp", "model",
AgFirmwareModeName(fwMode));
MDNS.addServiceTxt("_airgradient", "_tcp", "serialno", ag.deviceId());
MDNS.addServiceTxt("_airgradient", "_tcp", "fw_ver", ag.getVersion());
MDNS.addServiceTxt("_airgradient", "_tcp", "vendor", "AirGradient");
MDNS.announce();
}
static void initMqtt(void) {
String mqttUri = configuration.getMqttBrokerUri();
if (mqttUri.isEmpty()) {
Serial.println(
"MQTT is not configured, skipping initialization of MQTT client");
return;
}
if (mqttClient.begin(mqttUri)) {
Serial.println("Successfully connected to MQTT broker");
} else {
Serial.println("Connection to MQTT broker failed");
}
}
static void factoryConfigReset(void) {
if (ag.button.getState() == ag.button.BUTTON_PRESSED) {
if (factoryBtnPressTime == 0) {
factoryBtnPressTime = millis();
} else {
uint32_t ms = (uint32_t)(millis() - factoryBtnPressTime);
if (ms >= 2000) {
// Show display message: For factory keep for x seconds
if (ag.isOne() || ag.isPro4_2()) {
oledDisplay.setText("Factory reset", "keep pressed", "for 8 sec");
} else {
Serial.println("Factory reset, keep pressed for 8 sec");
}
int count = 7;
while (ag.button.getState() == ag.button.BUTTON_PRESSED) {
delay(1000);
String str = "for " + String(count) + " sec";
oledDisplay.setText("Factory reset", "keep pressed", str.c_str());
count--;
if (count == 0) {
/** Stop MQTT task first */
// if (mqttTask) {
// vTaskDelete(mqttTask);
// mqttTask = NULL;
// }
/** Reset WIFI */
wifiConnector.reset();
/** Reset local config */
configuration.reset();
oledDisplay.setText("Factory reset", "successful", "");
delay(3000);
oledDisplay.setText("", "", "");
ESP.restart();
}
}
/** Show current content cause reset ignore */
factoryBtnPressTime = 0;
appDispHandler();
}
}
} else {
if (factoryBtnPressTime != 0) {
appDispHandler();
}
factoryBtnPressTime = 0;
}
}
static void wdgFeedUpdate(void) {
ag.watchdog.reset();
Serial.println("External watchdog feed!");
}
static bool sgp41Init(void) {
ag.sgp41.setNoxLearningOffset(configuration.getNoxLearningOffset());
ag.sgp41.setTvocLearningOffset(configuration.getTvocLearningOffset());
if (ag.sgp41.begin(Wire)) {
Serial.println("Init SGP41 success");
configuration.hasSensorSGP = true;
return true;
} else {
Serial.println("Init SGP41 failuire");
configuration.hasSensorSGP = false;
}
return false;
}
static void wifiFactoryConfigure(void) {
WiFi.persistent(true);
WiFi.begin("airgradient", "cleanair");
WiFi.persistent(false);
oledDisplay.setText("Configure WiFi", "connect to", "\'airgradient\'");
delay(2500);
oledDisplay.setText("Rebooting...", "", "");
delay(2500);
oledDisplay.setText("", "", "");
ESP.restart();
}
static void mqttHandle(void) {
if(mqttClient.isConnected() == false) {
mqttClient.connect(String("airgradient-") + ag.deviceId());
}
if (mqttClient.isConnected()) {
String payload = measurements.toString(true, fwMode, wifiConnector.RSSI());
String topic = "airgradient/readings/" + ag.deviceId();
if (mqttClient.publish(topic.c_str(), payload.c_str(), payload.length())) {
Serial.println("MQTT sync success");
} else {
Serial.println("MQTT sync failure");
}
}
}
static void sendDataToAg() {
/** Change oledDisplay and led state */
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnecting);
delay(1500);
if (apiClient.sendPing(wifiConnector.RSSI(), measurements.bootCount())) {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnected);
} else {
stateMachine.displayHandle(AgStateMachineWiFiOkServerConnectFailed);
}
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
}
void dispSensorNotFound(String ss) {
ss = ss + " not found";
oledDisplay.setText("Sensor init", "Error:", ss.c_str());
delay(2000);
}
static void boardInit(void) {
/** Display init */
oledDisplay.begin();
/** Show boot display */
Serial.println("Firmware Version: " + ag.getVersion());
oledDisplay.setText("AirGradient ONE",
"FW Version: ", ag.getVersion().c_str());
delay(DISPLAY_DELAY_SHOW_CONTENT_MS);
ag.button.begin();
ag.watchdog.begin();
/** Run LED test on start up if button pressed */
oledDisplay.setText("Press now for", "factory WiFi", "configure");
uint32_t stime = millis();
while (true) {
if (ag.button.getState() == ag.button.BUTTON_PRESSED) {
wifiFactoryConfigure();
}
delay(1);
uint32_t ms = (uint32_t)(millis() - stime);
if (ms >= 3000) {
break;
}
delay(1);
}
/** Show message init sensor */
oledDisplay.setText("Sensor", "initializing...", "");
/** Init sensor SGP41 */
if (sgp41Init() == false) {
dispSensorNotFound("SGP41");
}
/** Init SHT */
if (ag.sht.begin(Wire) == false) {
Serial.println("SHTx sensor not found");
configuration.hasSensorSHT = false;
dispSensorNotFound("SHT");
}
/** Init S8 CO2 sensor */
if (ag.s8.begin(&Serial) == false) {
Serial.println("CO2 S8 sensor not found");
configuration.hasSensorS8 = false;
dispSensorNotFound("S8");
}
/** Init PMS5003 */
configuration.hasSensorPMS1 = true;
configuration.hasSensorPMS2 = false;
if (ag.pms5003.begin(&Serial) == false) {
Serial.println("PMS sensor not found");
configuration.hasSensorPMS1 = false;
dispSensorNotFound("PMS");
}
/** Set S8 CO2 abc days period */
if (configuration.hasSensorS8) {
if (ag.s8.setAbcPeriod(configuration.getCO2CalibrationAbcDays() * 24)) {
Serial.println("Set S8 AbcDays successful");
} else {
Serial.println("Set S8 AbcDays failure");
}
}
localServer.setFwMode(FW_MODE_I_42PS);
}
static void failedHandler(String msg) {
while (true) {
Serial.println(msg);
delay(1000);
}
}
static void configurationUpdateSchedule(void) {
if (configuration.isOfflineMode() ||
configuration.getConfigurationControl() == ConfigurationControl::ConfigurationControlLocal) {
Serial.println("Ignore fetch server configuration. Either mode is offline "
"or configurationControl set to local");
apiClient.resetFetchConfigurationStatus();
return;
}
if (apiClient.fetchServerConfiguration()) {
configUpdateHandle();
}
}
static void configUpdateHandle() {
if (configuration.isUpdated() == false) {
return;
}
stateMachine.executeCo2Calibration();
String mqttUri = configuration.getMqttBrokerUri();
if (mqttClient.isCurrentUri(mqttUri) == false) {
mqttClient.end();
initMqtt();
}
if (configuration.hasSensorSGP) {
if (configuration.noxLearnOffsetChanged() ||
configuration.tvocLearnOffsetChanged()) {
ag.sgp41.end();
int oldTvocOffset = ag.sgp41.getTvocLearningOffset();
int oldNoxOffset = ag.sgp41.getNoxLearningOffset();
bool result = sgp41Init();
const char *resultStr = "successful";
if (!result) {
resultStr = "failure";
}
if (oldTvocOffset != configuration.getTvocLearningOffset()) {
Serial.printf("Setting tvocLearningOffset from %d to %d hours %s\r\n",
oldTvocOffset, configuration.getTvocLearningOffset(),
resultStr);
}
if (oldNoxOffset != configuration.getNoxLearningOffset()) {
Serial.printf("Setting noxLearningOffset from %d to %d hours %s\r\n",
oldNoxOffset, configuration.getNoxLearningOffset(),
resultStr);
}
}
}
if (configuration.isDisplayBrightnessChanged()) {
oledDisplay.setBrightness(configuration.getDisplayBrightness());
}
appDispHandler();
}
static void appDispHandler(void) {
AgStateMachineState state = AgStateMachineNormal;
/** Only show display status on online mode. */
if (configuration.isOfflineMode() == false) {
if (wifiConnector.isConnected() == false) {
state = AgStateMachineWiFiLost;
} else if (apiClient.isFetchConfigurationFailed()) {
state = AgStateMachineSensorConfigFailed;
if (apiClient.isNotAvailableOnDashboard()) {
stateMachine.displaySetAddToDashBoard();
} else {
stateMachine.displayClearAddToDashBoard();
}
} else if (apiClient.isPostToServerFailed()) {
state = AgStateMachineServerLost;
}
}
stateMachine.displayHandle(state);
}
static void oledDisplaySchedule(void) {
if (factoryBtnPressTime == 0) {
appDispHandler();
}
}
static void updateTvoc(void) {
if (!configuration.hasSensorSGP) {
return;
}
measurements.update(Measurements::TVOC, ag.sgp41.getTvocIndex());
measurements.update(Measurements::TVOCRaw, ag.sgp41.getTvocRaw());
measurements.update(Measurements::NOx, ag.sgp41.getNoxIndex());
measurements.update(Measurements::NOxRaw, ag.sgp41.getNoxRaw());
}
static void updatePm(void) {
if (ag.pms5003.connected()) {
measurements.update(Measurements::PM01, ag.pms5003.getPm01Ae());
measurements.update(Measurements::PM25, ag.pms5003.getPm25Ae());
measurements.update(Measurements::PM10, ag.pms5003.getPm10Ae());
measurements.update(Measurements::PM03_PC, ag.pms5003.getPm03ParticleCount());
} else {
measurements.update(Measurements::PM01, utils::getInvalidPmValue());
measurements.update(Measurements::PM25, utils::getInvalidPmValue());
measurements.update(Measurements::PM10, utils::getInvalidPmValue());
measurements.update(Measurements::PM03_PC, utils::getInvalidPmValue());
}
}
static void sendDataToServer(void) {
/** Increment bootcount when send measurements data is scheduled */
int bootCount = measurements.bootCount() + 1;
measurements.setBootCount(bootCount);
if (configuration.isOfflineMode() || !configuration.isPostDataToAirGradient()) {
Serial.println("Skipping transmission of data to AG server. Either mode is offline "
"or post data to server disabled");
return;
}
if (wifiConnector.isConnected() == false) {
Serial.println("WiFi not connected, skipping data transmission to AG server");
return;
}
String syncData = measurements.toString(false, fwMode, wifiConnector.RSSI());
if (apiClient.postToServer(syncData)) {
Serial.println();
Serial.println("Online mode and isPostToAirGradient = true");
Serial.println();
}
}
static void tempHumUpdate(void) {
if (ag.sht.measure()) {
float temp = ag.sht.getTemperature();
float rhum = ag.sht.getRelativeHumidity();
measurements.update(Measurements::Temperature, temp);
measurements.update(Measurements::Humidity, rhum);
// Update compensation temperature and humidity for SGP41
if (configuration.hasSensorSGP) {
ag.sgp41.setCompensationTemperatureHumidity(temp, rhum);
}
} else {
measurements.update(Measurements::Temperature, utils::getInvalidTemperature());
measurements.update(Measurements::Humidity, utils::getInvalidHumidity());
Serial.println("SHT read failed");
}
}
/* Set max period for each measurement type based on sensor update interval*/
void setMeasurementMaxPeriod() {
/// Max period for S8 sensors measurements
measurements.maxPeriod(Measurements::CO2, calculateMaxPeriod(SENSOR_CO2_UPDATE_INTERVAL));
/// Max period for SGP sensors measurements
measurements.maxPeriod(Measurements::TVOC, calculateMaxPeriod(SENSOR_TVOC_UPDATE_INTERVAL));
measurements.maxPeriod(Measurements::TVOCRaw, calculateMaxPeriod(SENSOR_TVOC_UPDATE_INTERVAL));
measurements.maxPeriod(Measurements::NOx, calculateMaxPeriod(SENSOR_TVOC_UPDATE_INTERVAL));
measurements.maxPeriod(Measurements::NOxRaw, calculateMaxPeriod(SENSOR_TVOC_UPDATE_INTERVAL));
/// Max period for PMS sensors measurements
measurements.maxPeriod(Measurements::PM25, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
measurements.maxPeriod(Measurements::PM01, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
measurements.maxPeriod(Measurements::PM10, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
measurements.maxPeriod(Measurements::PM03_PC, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
// Temperature and Humidity
if (configuration.hasSensorSHT) {
/// Max period for SHT sensors measurements
measurements.maxPeriod(Measurements::Temperature,
calculateMaxPeriod(SENSOR_TEMP_HUM_UPDATE_INTERVAL));
measurements.maxPeriod(Measurements::Humidity,
calculateMaxPeriod(SENSOR_TEMP_HUM_UPDATE_INTERVAL));
} else {
/// Temp and hum data retrieved from PMS5003T sensor
measurements.maxPeriod(Measurements::Temperature,
calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
measurements.maxPeriod(Measurements::Humidity, calculateMaxPeriod(SENSOR_PM_UPDATE_INTERVAL));
}
}
int calculateMaxPeriod(int updateInterval) {
// 0.5 is 50% reduced interval for max period
return (SERVER_SYNC_INTERVAL - (SERVER_SYNC_INTERVAL * 0.5)) / updateInterval;
}

View File

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

View File

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

View File

@ -0,0 +1,204 @@
#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.isFetchConfigurationFailed() ? "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()));
// Initialize default invalid value for each measurements
float _temp = utils::getInvalidTemperature();
float _hum = utils::getInvalidHumidity();
int pm01 = utils::getInvalidPmValue();
int pm25 = utils::getInvalidPmValue();
int pm10 = utils::getInvalidPmValue();
int pm03PCount = utils::getInvalidPmValue();
int co2 = utils::getInvalidCO2();
int atmpCompensated = utils::getInvalidTemperature();
int rhumCompensated = utils::getInvalidHumidity();
int tvoc = utils::getInvalidVOC();
int tvocRaw = utils::getInvalidVOC();
int nox = utils::getInvalidNOx();
int noxRaw = utils::getInvalidNOx();
if (config.hasSensorSHT) {
_temp = measure.getFloat(Measurements::Temperature);
_hum = measure.getFloat(Measurements::Humidity);
atmpCompensated = _temp;
rhumCompensated = _hum;
}
if (config.hasSensorPMS1) {
pm01 = measure.get(Measurements::PM01);
float correctedPm = measure.getCorrectedPM25(false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10);
pm03PCount = measure.get(Measurements::PM03_PC);
}
if (config.hasSensorSGP) {
tvoc = measure.get(Measurements::TVOC);
tvocRaw = measure.get(Measurements::TVOCRaw);
nox = measure.get(Measurements::NOx);
noxRaw = measure.get(Measurements::NOxRaw);
}
if (config.hasSensorS8) {
co2 = measure.get(Measurements::CO2);
}
if (config.hasSensorPMS1) {
if (utils::isValidPm(pm01)) {
add_metric("pm1",
"PM1.0 concentration as measured by the AirGradient PMS "
"sensor, in micrograms per cubic meter",
"gauge", "ugm3");
add_metric_point("", String(pm01));
}
if (utils::isValidPm(pm25)) {
add_metric("pm2d5",
"PM2.5 concentration as measured by the AirGradient PMS "
"sensor, in micrograms per cubic meter",
"gauge", "ugm3");
add_metric_point("", String(pm25));
}
if (utils::isValidPm(pm10)) {
add_metric("pm10",
"PM10 concentration as measured by the AirGradient PMS "
"sensor, in micrograms per cubic meter",
"gauge", "ugm3");
add_metric_point("", String(pm10));
}
if (utils::isValidPm03Count(pm03PCount)) {
add_metric("pm0d3",
"PM0.3 concentration as measured by the AirGradient PMS "
"sensor, in number of particules per 100 milliliters",
"gauge", "p100ml");
add_metric_point("", String(pm03PCount));
}
}
if (config.hasSensorSGP) {
if (utils::isValidVOC(tvoc)) {
add_metric("tvoc_index",
"The processed Total Volatile Organic Compounds (TVOC) index "
"as measured by the AirGradient SGP sensor",
"gauge");
add_metric_point("", String(tvoc));
}
if (utils::isValidVOC(tvocRaw)) {
add_metric("tvoc_raw",
"The raw input value to the Total Volatile Organic Compounds "
"(TVOC) index as measured by the AirGradient SGP sensor",
"gauge");
add_metric_point("", String(tvocRaw));
}
if (utils::isValidNOx(nox)) {
add_metric("nox_index",
"The processed Nitrous Oxide (NOx) index as measured by the "
"AirGradient SGP sensor",
"gauge");
add_metric_point("", String(nox));
}
if (utils::isValidNOx(noxRaw)) {
add_metric("nox_raw",
"The raw input value to the Nitrous Oxide (NOx) index as "
"measured by the AirGradient SGP sensor",
"gauge");
add_metric_point("", String(noxRaw));
}
}
if (utils::isValidCO2(co2)) {
add_metric("co2",
"Carbon dioxide concentration as measured by the AirGradient S8 "
"sensor, in parts per million",
"gauge", "ppm");
add_metric_point("", String(co2));
}
if (utils::isValidTemperature(_temp)) {
add_metric(
"temperature",
"The ambient temperature as measured by the AirGradient SHT / PMS "
"sensor, in degrees Celsius",
"gauge", "celsius");
add_metric_point("", String(_temp));
}
if (utils::isValidTemperature(atmpCompensated)) {
add_metric("temperature_compensated",
"The compensated ambient temperature as measured by the "
"AirGradient SHT / PMS "
"sensor, in degrees Celsius",
"gauge", "celsius");
add_metric_point("", String(atmpCompensated));
}
if (utils::isValidHumidity(_hum)) {
add_metric(
"humidity",
"The relative humidity as measured by the AirGradient SHT sensor",
"gauge", "percent");
add_metric_point("", String(_hum));
}
if (utils::isValidHumidity(rhumCompensated)) {
add_metric("humidity_compensated",
"The compensated relative humidity as measured by the "
"AirGradient SHT / PMS sensor",
"gauge", "percent");
add_metric_point("", String(rhumCompensated));
}
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

@ -39,7 +39,11 @@ String LocalServer::getHostname(void) {
void LocalServer::_handle(void) { server.handleClient(); }
void LocalServer::_GET_config(void) {
server.send(200, "application/json", config.toString());
if(ag->isOne()) {
server.send(200, "application/json", config.toString());
} else {
server.send(200, "application/json", config.toString(fwMode));
}
}
void LocalServer::_PUT_config(void) {
@ -60,9 +64,8 @@ void LocalServer::_GET_metrics(void) {
}
void LocalServer::_GET_measure(void) {
server.send(
200, "application/json",
measure.toString(true, fwMode, wifiConnector.RSSI(), ag, &config));
String toSend = measure.toString(true, fwMode, wifiConnector.RSSI());
server.send(200, "application/json", toSend);
}
void LocalServer::setFwMode(AgFirmwareMode fwMode) { this->fwMode = fwMode; }

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,18 @@
#include "OpenMetrics.h"
OpenMetrics::OpenMetrics(Measurements &measure, Configuration &config,
WifiConnector &wifiConnector, AgApiClient &apiClient)
: measure(measure), config(config), wifiConnector(wifiConnector),
apiClient(apiClient) {}
WifiConnector &wifiConnector)
: measure(measure), config(config), wifiConnector(wifiConnector) {}
OpenMetrics::~OpenMetrics() {}
void OpenMetrics::setAirGradient(AirGradient *ag) { this->ag = ag; }
void OpenMetrics::setAirGradient(AirGradient *ag) {
this->ag = ag;
}
void OpenMetrics::setAirgradientClient(AirgradientClient *client) {
this->agClient = client;
}
const char *OpenMetrics::getApiContentType(void) {
return "application/openmetrics-text; version=1.0.0; charset=utf-8";
@ -43,13 +48,13 @@ String OpenMetrics::getPayload(void) {
"1 if the AirGradient device was able to successfully fetch its "
"configuration from the server",
"gauge");
add_metric_point("", apiClient.isFetchConfigureFailed() ? "0" : "1");
add_metric_point("", agClient->isLastFetchConfigSucceed() ? "1" : "0");
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_point("", agClient->isLastPostMeasureSucceed() ? "1" : "0");
add_metric(
"wifi_rssi",
@ -57,87 +62,121 @@ String OpenMetrics::getPayload(void) {
"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));
}
// Initialize default invalid value for each measurements
float _temp = utils::getInvalidTemperature();
float _hum = utils::getInvalidHumidity();
int pm01 = utils::getInvalidPmValue();
int pm25 = utils::getInvalidPmValue();
int pm10 = utils::getInvalidPmValue();
int pm03PCount = utils::getInvalidPmValue();
int co2 = utils::getInvalidCO2();
int atmpCompensated = utils::getInvalidTemperature();
int rhumCompensated = utils::getInvalidHumidity();
int tvoc = utils::getInvalidVOC();
int tvocRaw = utils::getInvalidVOC();
int nox = utils::getInvalidNOx();
int noxRaw = utils::getInvalidNOx();
float _temp = -1001;
float _hum = -1;
int pm01 = -1;
int pm25 = -1;
int pm10 = -1;
int pm03PCount = -1;
int atmpCompensated = -1;
int ahumCompensated = -1;
// Get values
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;
_temp = (measure.getFloat(Measurements::Temperature, 1) +
measure.getFloat(Measurements::Temperature, 2)) /
2.0f;
_hum = (measure.getFloat(Measurements::Humidity, 1) +
measure.getFloat(Measurements::Humidity, 2)) /
2.0f;
pm01 = (measure.get(Measurements::PM01, 1) + measure.get(Measurements::PM01, 2)) / 2.0f;
float correctedPm25_1 = measure.getCorrectedPM25(false, 1);
float correctedPm25_2 = measure.getCorrectedPM25(false, 2);
float correctedPm25 = (correctedPm25_1 + correctedPm25_2) / 2.0f;
pm25 = round(correctedPm25);
pm10 = (measure.get(Measurements::PM10, 1) + measure.get(Measurements::PM10, 2)) / 2.0f;
pm03PCount =
(measure.get(Measurements::PM03_PC, 1) + measure.get(Measurements::PM03_PC, 2)) / 2.0f;
} else {
if (ag->isOne()) {
if (config.hasSensorSHT) {
_temp = measure.Temperature;
_hum = measure.Humidity;
_temp = measure.getFloat(Measurements::Temperature);
_hum = measure.getFloat(Measurements::Humidity);
}
if (config.hasSensorPMS1) {
pm01 = measure.get(Measurements::PM01);
float correctedPm = measure.getCorrectedPM25(false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10);
pm03PCount = measure.get(Measurements::PM03_PC);
}
} 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;
_temp = measure.getFloat(Measurements::Temperature, 1);
_hum = measure.getFloat(Measurements::Humidity, 1);
pm01 = measure.get(Measurements::PM01, 1);
float correctedPm = measure.getCorrectedPM25(false, 1);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10, 1);
pm03PCount = measure.get(Measurements::PM03_PC, 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;
_temp = measure.getFloat(Measurements::Temperature, 2);
_hum = measure.getFloat(Measurements::Humidity, 2);
pm01 = measure.get(Measurements::PM01, 2);
float correctedPm = measure.getCorrectedPM25(false, 2);
pm25 = round(correctedPm);
pm10 = measure.get(Measurements::PM10, 2);
pm03PCount = measure.get(Measurements::PM03_PC, 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.hasSensorSGP) {
tvoc = measure.get(Measurements::TVOC);
tvocRaw = measure.get(Measurements::TVOCRaw);
nox = measure.get(Measurements::NOx);
noxRaw = measure.get(Measurements::NOxRaw);
}
if (config.hasSensorS8) {
co2 = measure.get(Measurements::CO2);
}
/** Get temperature and humidity compensated */
if (ag->isOne()) {
atmpCompensated = round(measure.getCorrectedTempHum(Measurements::Temperature));
rhumCompensated = round(measure.getCorrectedTempHum(Measurements::Humidity));
} else {
atmpCompensated = round((measure.getCorrectedTempHum(Measurements::Temperature, 1) +
measure.getCorrectedTempHum(Measurements::Temperature, 2)) /
2.0f);
rhumCompensated = round((measure.getCorrectedTempHum(Measurements::Humidity, 1) +
measure.getCorrectedTempHum(Measurements::Humidity, 2)) /
2.0f);
}
// Add measurements that valid to the metrics
if (config.hasSensorPMS1 || config.hasSensorPMS2) {
if (pm01 >= 0) {
if (utils::isValidPm(pm01)) {
add_metric("pm1",
"PM1.0 concentration as measured by the AirGradient PMS "
"sensor, in micrograms per cubic meter",
"gauge", "ugm3");
add_metric_point("", String(pm01));
}
if (pm25 >= 0) {
if (utils::isValidPm(pm25)) {
add_metric("pm2d5",
"PM2.5 concentration as measured by the AirGradient PMS "
"sensor, in micrograms per cubic meter",
"gauge", "ugm3");
add_metric_point("", String(pm25));
}
if (pm10 >= 0) {
if (utils::isValidPm(pm10)) {
add_metric("pm10",
"PM10 concentration as measured by the AirGradient PMS "
"sensor, in micrograms per cubic meter",
"gauge", "ugm3");
add_metric_point("", String(pm10));
}
if (pm03PCount >= 0) {
if (utils::isValidPm03Count(pm03PCount)) {
add_metric("pm0d3",
"PM0.3 concentration as measured by the AirGradient PMS "
"sensor, in number of particules per 100 milliliters",
@ -147,64 +186,68 @@ String OpenMetrics::getPayload(void) {
}
if (config.hasSensorSGP) {
if (measure.TVOC >= 0) {
if (utils::isValidVOC(tvoc)) {
add_metric("tvoc_index",
"The processed Total Volatile Organic Compounds (TVOC) index "
"as measured by the AirGradient SGP sensor",
"gauge");
add_metric_point("", String(measure.TVOC));
add_metric_point("", String(tvoc));
}
if (measure.TVOCRaw >= 0) {
if (utils::isValidVOC(tvocRaw)) {
add_metric("tvoc_raw",
"The raw input value to the Total Volatile Organic Compounds "
"(TVOC) index as measured by the AirGradient SGP sensor",
"gauge");
add_metric_point("", String(measure.TVOCRaw));
add_metric_point("", String(tvocRaw));
}
if (measure.NOx >= 0) {
if (utils::isValidNOx(nox)) {
add_metric("nox_index",
"The processed Nitrous Oxide (NOx) index as measured by the "
"AirGradient SGP sensor",
"gauge");
add_metric_point("", String(measure.NOx));
add_metric_point("", String(nox));
}
if (measure.NOxRaw >= 0) {
if (utils::isValidNOx(noxRaw)) {
add_metric("nox_raw",
"The raw input value to the Nitrous Oxide (NOx) index as "
"measured by the AirGradient SGP sensor",
"gauge");
add_metric_point("", String(measure.NOxRaw));
add_metric_point("", String(noxRaw));
}
}
if (_temp > -1001) {
if (utils::isValidCO2(co2)) {
add_metric("co2",
"Carbon dioxide concentration as measured by the AirGradient S8 "
"sensor, in parts per million",
"gauge", "ppm");
add_metric_point("", String(co2));
}
if (utils::isValidTemperature(_temp)) {
add_metric("temperature",
"The ambient temperature as measured by the AirGradient SHT / PMS "
"sensor, in degrees Celsius",
"gauge", "celsius");
add_metric_point("", String(_temp));
}
if (atmpCompensated > -1001) {
add_metric(
"temperature_compensated",
"The compensated ambient temperature as measured by the AirGradient SHT / PMS "
"sensor, in degrees Celsius",
"gauge", "celsius");
if (utils::isValidTemperature(atmpCompensated)) {
add_metric("temperature_compensated",
"The compensated ambient temperature as measured by the AirGradient SHT / PMS "
"sensor, in degrees Celsius",
"gauge", "celsius");
add_metric_point("", String(atmpCompensated));
}
if (_hum >= 0) {
add_metric(
"humidity",
"The relative humidity as measured by the AirGradient SHT sensor"
"gauge", "percent");
if (utils::isValidHumidity(_hum)) {
add_metric("humidity", "The relative humidity as measured by the AirGradient SHT sensor",
"gauge", "percent");
add_metric_point("", String(_hum));
}
if (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));
if (utils::isValidHumidity(rhumCompensated)) {
add_metric("humidity_compensated",
"The compensated relative humidity as measured by the AirGradient SHT / PMS sensor",
"gauge", "percent");
add_metric_point("", String(rhumCompensated));
}
response += "# EOF\n";

View File

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

View File

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

@ -44,7 +44,7 @@ void loop() {
if (ms >= 5000) {
lastRead = millis();
#ifdef ESP8266
if (ag.pms5003.isFailed() == false) {
if (ag.pms5003.connected()) {
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",
@ -54,12 +54,12 @@ void loop() {
}
#else
if (ag.getBoardType() == OPEN_AIR_OUTDOOR) {
if (ag.pms5003t_1.isFailed() == false) {
if (ag.pms5003t_1.connected()) {
PM2 = ag.pms5003t_1.getPm25Ae();
readResul = true;
}
} else {
if (ag.pms5003.isFailed() == false) {
if (ag.pms5003.connected()) {
PM2 = ag.pms5003.getPm25Ae();
readResul = true;
}

View File

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

View File

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

View File

@ -8,17 +8,44 @@
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:esp32-c3-devkitm-1]
[env:esp32-c3]
platform = espressif32
board = esp32-c3-devkitm-1
framework = arduino
build_flags = !echo '-D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 -D GIT_VERSION=\\"'$(git describe --tags --always --dirty)'\\"'
build_flags = !echo '-D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 -D AG_LOG_LEVEL=AG_LOG_LEVEL_INFO -D GIT_VERSION=\\"'$(git describe --tags --always --dirty)'\\"'
board_build.partitions = partitions.csv
monitor_speed = 115200
lib_deps =
aglib=symlink://../arduino
EEPROM
WebServer
ESPmDNS
FS
SPIFFS
HTTPClient
WiFiClientSecure
Update
DNSServer
[env:esp8266]
platform = espressif8266
board = d1_mini
framework = arduino
monitor_speed = 115200
lib_deps =
aglib=symlink://../arduino
EEPROM
ESP8266HTTPClient
ESP8266WebServer
DNSServer
monitor_filters = time
[platformio]
src_dir = examples/OneOpenAir
; src_dir = examples/BASIC
; src_dir = examples/DiyProIndoorV4_2
; src_dir = examples/DiyProIndoorV3_3
; src_dir = examples/TestCO2
; src_dir = examples/TestPM
; src_dir = examples/TestSht

View File

@ -22,6 +22,7 @@ AgApiClient::~AgApiClient() {}
void AgApiClient::begin(void) {
getConfigFailed = false;
postToServerFailed = false;
logInfo("Init apiRoot: " + apiRoot);
logInfo("begin");
}
@ -33,19 +34,8 @@ void AgApiClient::begin(void) {
* @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";
String uri = apiRoot + "/sensors/airgradient:" +
ag->deviceId() + "/one/config";
/** Init http client */
#ifdef ESP8266
@ -57,29 +47,50 @@ bool AgApiClient::fetchServerConfiguration(void) {
}
#else
HTTPClient client;
if (client.begin(uri) == false) {
getConfigFailed = true;
return false;
client.setConnectTimeout(timeoutMs); // Set timeout when establishing connection to server
client.setTimeout(timeoutMs); // Timeout when waiting for response from AG server
if (apiRootChanged) {
// If apiRoot is changed, assume not using https
if (client.begin(uri) == false) {
logError("Begin HTTPClient failed (GET)");
getConfigFailed = true;
return false;
}
} else {
// By default, airgradient using https
if (client.begin(uri, AG_SERVER_ROOT_CA) == false) {
logError("Begin HTTPClient using tls failed (GET)");
getConfigFailed = true;
return false;
}
}
#endif
/** Get data */
int retCode = client.GET();
logInfo(String("GET: ") + uri);
logInfo(String("Return code: ") + String(retCode));
if (retCode != 200) {
client.end();
getConfigFailed = true;
/** Return code 400 mean device not setup on cloud. */
if (retCode == 400) {
notAvailableOnDashboard = true;
}
return false;
}
/** clear failed */
getConfigFailed = false;
notAvailableOnDashboard = false;
/** Get response string */
String respContent = client.getString();
client.end();
// logInfo("Get configuration: " + respContent);
/** Parse configuration and return result */
return config.parse(respContent, false);
}
@ -93,30 +104,41 @@ bool AgApiClient::fetchServerConfiguration(void) {
* @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;
String uri = apiRoot + "/sensors/airgradient:" + ag->deviceId() + "/measures";
#ifdef ESP8266
HTTPClient client;
if (client.begin(wifiClient, uri.c_str()) == false) {
WiFiClient wifiClient;
if (client.begin(wifiClient, uri) == false) {
getConfigFailed = true;
return false;
}
#else
HTTPClient client;
client.setConnectTimeout(timeoutMs); // Set timeout when establishing connection to server
client.setTimeout(timeoutMs); // Timeout when waiting for response from AG server
if (apiRootChanged) {
// If apiRoot is changed, assume not using https
if (client.begin(uri) == false) {
logError("Begin HTTPClient failed (POST)");
getConfigFailed = true;
return false;
}
} else {
// By default, airgradient using https
if (client.begin(uri, AG_SERVER_ROOT_CA) == false) {
logError("Begin HTTPClient using tls failed (POST)");
getConfigFailed = true;
return false;
}
}
#endif
client.addHeader("content-type", "application/json");
int retCode = client.POST(data);
client.end();
logInfo(String("POST: ") + uri);
logInfo(String("Return code: ") + String(retCode));
if ((retCode == 200) || (retCode == 429)) {
postToServerFailed = false;
return true;
@ -133,7 +155,12 @@ bool AgApiClient::postToServer(String data) {
* @return true Success
* @return false Failure
*/
bool AgApiClient::isFetchConfigureFailed(void) { return getConfigFailed; }
bool AgApiClient::isFetchConfigurationFailed(void) { return getConfigFailed; }
/**
* @brief Reset status of get configuration from AirGradient cloud
*/
void AgApiClient::resetFetchConfigurationStatus(void) { getConfigFailed = false; }
/**
* @brief Get failed status when post data to AirGradient cloud
@ -143,6 +170,17 @@ bool AgApiClient::isFetchConfigureFailed(void) { return getConfigFailed; }
*/
bool AgApiClient::isPostToServerFailed(void) { return postToServerFailed; }
/**
* @brief Get status device has available on dashboard or not. should get after
* fetch configuration return failed
*
* @return true
* @return false
*/
bool AgApiClient::isNotAvailableOnDashboard(void) {
return notAvailableOnDashboard;
}
void AgApiClient::setAirGradient(AirGradient *ag) { this->ag = ag; }
/**
@ -159,3 +197,19 @@ bool AgApiClient::sendPing(int rssi, int bootCount) {
root["boot"] = bootCount;
return postToServer(JSON.stringify(root));
}
String AgApiClient::getApiRoot() const { return apiRoot; }
void AgApiClient::setApiRoot(const String &apiRoot) {
this->apiRootChanged = true;
this->apiRoot = apiRoot;
}
/**
* @brief Set http request timeout. (Default: 10s)
*
* @param timeoutMs
*/
void AgApiClient::setTimeout(uint16_t timeoutMs) {
this->timeoutMs = timeoutMs;
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -5,41 +5,50 @@
#include "Main/PrintLog.h"
#include "AirGradient.h"
#include <Arduino.h>
#include "Libraries/Arduino_JSON/src/Arduino_JSON.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;
public:
struct PMCorrection {
PMCorrectionAlgorithm algorithm;
float intercept;
float scalingFactor;
bool useEPA; // EPA 2021
bool changed;
};
struct Config config;
struct TempHumCorrection {
TempHumCorrectionAlgorithm algorithm;
float intercept;
float scalingFactor;
bool changed;
};
private:
bool co2CalibrationRequested;
bool ledBarTestRequested;
bool udpated;
bool updated;
bool commandRequested = false;
String failedMessage;
bool _noxLearnOffsetChanged;
bool _tvocLearningOffsetChanged;
bool ledBarBrightnessChanged = false;
bool displayBrightnessChanged = false;
String otaNewFirmwareVersion;
bool _offlineMode = false;
bool _ledBarModeChanged = false;
PMCorrection pmCorrection;
TempHumCorrection tempCorrection;
TempHumCorrection rhumCorrection;
AirGradient* ag;
String getLedBarModeName(LedBarMode mode);
PMCorrectionAlgorithm matchPmAlgorithm(String algorithm);
TempHumCorrectionAlgorithm matchTempHumAlgorithm(String algorithm);
bool updatePmCorrection(JSONVar &json);
bool updateTempHumCorrection(JSONVar &json, TempHumCorrection &target,
const char *correctionName);
void saveConfig(void);
void loadConfig(void);
void defaultConfig(void);
@ -49,8 +58,8 @@ private:
void jsonInvalid(void);
void configLogInfo(String name, String fromValue, String toValue);
String getPMStandardString(bool usaqi);
String getDisplayModeString(bool dispMode);
String getAbcDayString(int value);
void toConfig(const char *buf);
public:
Configuration(Stream &debugLog);
@ -62,9 +71,13 @@ public:
bool hasSensorSGP = true;
bool hasSensorSHT = true;
typedef void (*ConfigurationUpdatedCallback_t)();
void setConfigurationUpdatedCallback(ConfigurationUpdatedCallback_t callback);
bool begin(void);
bool parse(String data, bool isLocal);
String toString(void);
String toString(AgFirmwareMode fwMode);
bool isTemperatureUnitInF(void);
String getCountry(void);
bool isPmStandardInUSAQI(void);
@ -73,6 +86,7 @@ public:
String getLedBarModeName(void);
bool getDisplayMode(void);
String getMqttBrokerUri(void);
String getHttpDomain(void);
bool isPostDataToAirGradient(void);
ConfigurationControl getConfigurationControl(void);
bool isCo2CalibrationRequested(void);
@ -80,6 +94,7 @@ public:
void reset(void);
String getModel(void);
bool isUpdated(void);
bool isCommandRequested(void);
String getFailedMesage(void);
void setPostToAirGradient(bool enable);
bool noxLearnOffsetChanged(void);
@ -89,6 +104,25 @@ public:
String wifiSSID(void);
String wifiPass(void);
void setAirGradient(AirGradient *ag);
bool isLedBarBrightnessChanged(void);
int getLedBarBrightness(void);
bool isDisplayBrightnessChanged(void);
int getDisplayBrightness(void);
String newFirmwareVersion(void);
bool isOfflineMode(void);
void setOfflineMode(bool offline);
void setOfflineModeWithoutSave(bool offline);
bool isCloudConnectionDisabled(void);
void setDisableCloudConnection(bool disable);
bool isLedBarModeChanged(void);
bool isMonitorDisplayCompensatedValues(void);
bool isPMCorrectionChanged(void);
bool isPMCorrectionEnabled(void);
PMCorrection getPMCorrection(void);
TempHumCorrection getTempCorrection(void);
TempHumCorrection getHumCorrection(void);
private:
ConfigurationUpdatedCallback_t _callback;
};
#endif /** _AG_CONFIG_H_ */

View File

@ -1,33 +1,59 @@
#include "AgOledDisplay.h"
#include "Libraries/U8g2/src/U8g2lib.h"
#include "Libraries/QRCode/src/qrcode.h"
#include "Main/utils.h"
/** Cast U8G2 */
#define DISP() ((U8G2_SH1106_128X64_NONAME_F_HW_I2C *)(this->u8g2))
static const unsigned char WIFI_ISSUE_BITS[] = {
0xd8, 0xc6, 0xde, 0xde, 0xc7, 0xf8, 0xd1, 0xe2, 0xdc, 0xce, 0xcc,
0xcc, 0xc0, 0xc0, 0xd0, 0xc2, 0x00, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0};
static const unsigned char CLOUD_ISSUE_BITS[] = {
0x70, 0xc0, 0x88, 0xc0, 0x04, 0xc1, 0x04, 0xcf, 0x02, 0xd0, 0x01,
0xe0, 0x01, 0xe0, 0x01, 0xe0, 0xa2, 0xd0, 0x4c, 0xce, 0xa0, 0xc0};
// Offline mode icon
static unsigned char OFFLINE_BITS[] = {
0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x30, 0x00, 0x62, 0x00,
0xE6, 0x00, 0xFE, 0x1F, 0xFE, 0x1F, 0xE6, 0x00, 0x62, 0x00,
0x30, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
};
// {
// 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x60, 0x00, 0x62, 0x00, 0xE2, 0x00,
// 0xFE, 0x1F, 0xFE, 0x1F, 0xE2, 0x00, 0x62, 0x00, 0x60, 0x00, 0x30, 0x00,
// 0x00, 0x00, 0x00, 0x00, };
/**
* @brief Show dashboard temperature and humdity
*
* @param hasStatus
*
* @param hasStatus
*/
void OledDisplay::showTempHum(bool hasStatus) {
char buf[10];
if (value.Temperature > -1001) {
/** Temperature */
float temp = value.getCorrectedTempHum(Measurements::Temperature, 1);
if (utils::isValidTemperature(temp)) {
float t = 0.0f;
if (config.isTemperatureUnitInF()) {
t = utils::degreeC_To_F(temp);
} else {
t = temp;
}
if (config.isTemperatureUnitInF()) {
float tempF = (value.Temperature * 9) / 5 + 32;
if (hasStatus) {
snprintf(buf, sizeof(buf), "%0.1f", tempF);
snprintf(buf, sizeof(buf), "%0.1f", t);
} else {
snprintf(buf, sizeof(buf), "%0.1f°F", tempF);
snprintf(buf, sizeof(buf), "%0.1f°F", t);
}
} else {
if (hasStatus) {
snprintf(buf, sizeof(buf), "%.1f", value.Temperature);
snprintf(buf, sizeof(buf), "%.1f", t);
} else {
snprintf(buf, sizeof(buf), "%.1f°C", value.Temperature);
snprintf(buf, sizeof(buf), "%.1f°C", t);
}
}
} else {
} else { /** Show invalid value */
if (config.isTemperatureUnitInF()) {
snprintf(buf, sizeof(buf), "-°F");
} else {
@ -36,33 +62,47 @@ void OledDisplay::showTempHum(bool hasStatus) {
}
DISP()->drawUTF8(1, 10, buf);
/** Show humidty */
if (value.Humidity >= 0) {
snprintf(buf, sizeof(buf), "%d%%", value.Humidity);
/** Show humidity */
int rhum = round(value.getCorrectedTempHum(Measurements::Humidity, 1));
if (utils::isValidHumidity(rhum)) {
snprintf(buf, sizeof(buf), "%d%%", rhum);
} else {
snprintf(buf, sizeof(buf), "%-%%");
snprintf(buf, sizeof(buf), "-%%");
}
if (value.Humidity > 99) {
if (rhum > 99.0) {
DISP()->drawStr(97, 10, buf);
} else {
DISP()->drawStr(105, 10, buf);
}
}
void OledDisplay::setCentralText(int y, String text) {
setCentralText(y, text.c_str());
}
void OledDisplay::setCentralText(int y, const char *text) {
int x = (DISP()->getWidth() - DISP()->getStrWidth(text)) / 2;
DISP()->drawStr(x, y, text);
}
void OledDisplay::showIcon(int x, int y, xbm_icon_t *icon) {
DISP()->drawXBM(x, y, icon->width, icon->height, icon->icon);
}
/**
* @brief Construct a new Ag Oled Display:: Ag Oled Display object
*
*
* @param config AgConfiguration
* @param value Measurements
* @param log Serial Stream
*/
OledDisplay::OledDisplay(Configuration &config, Measurements &value, Stream &log)
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; }
@ -81,17 +121,32 @@ bool OledDisplay::begin(void) {
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;
if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) {
/** Create u8g2 instance */
u8g2 = new U8G2_SH1106_128X64_NONAME_F_HW_I2C(U8G2_R0, U8X8_PIN_NONE);
if (u8g2 == NULL) {
logError("Create 'U8G2' failed");
return false;
}
/** Init u8g2 */
if (DISP()->begin() == false) {
logError("U8G2 'begin' failed");
return false;
}
} else if (ag->isBasic()) {
logInfo("DIY_BASIC init");
ag->display.begin(Wire);
ag->display.setTextColor(1);
ag->display.clear();
ag->display.show();
}
/** Init u8g2 */
if (DISP()->begin() == false) {
logError("U8G2 'begin' failed");
return false;
/** Show low brightness on startup. then it's completely turn off on main
* application */
int brightness = config.getDisplayBrightness();
if (brightness == 0) {
setBrightness(1);
}
isBegin = true;
@ -109,9 +164,13 @@ void OledDisplay::end(void) {
return;
}
/** Free u8g2 */
delete DISP();
u8g2 = NULL;
if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) {
/** Free u8g2 */
delete DISP();
u8g2 = NULL;
} else if (ag->isBasic()) {
ag->display.end();
}
isBegin = false;
logInfo("end");
@ -119,10 +178,10 @@ void OledDisplay::end(void) {
/**
* @brief Show text on 3 line of display
*
* @param line1
* @param line2
* @param line3
*
* @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());
@ -130,181 +189,413 @@ void OledDisplay::setText(String &line1, String &line2, String &line3) {
/**
* @brief Show text on 3 line of display
*
* @param line1
* @param line2
* @param line3
*
* @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());
const char *line3) {
if (isDisplayOff) {
return;
}
if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) {
DISP()->firstPage();
do {
DISP()->setFont(u8g2_font_t0_16_tf);
DISP()->drawStr(1, 10, line1);
DISP()->drawStr(1, 30, line2);
DISP()->drawStr(1, 50, line3);
} while (DISP()->nextPage());
} else if (ag->isBasic()) {
ag->display.clear();
ag->display.setCursor(1, 1);
ag->display.setText(line1);
ag->display.setCursor(1, 17);
ag->display.setText(line2);
ag->display.setCursor(1, 33);
ag->display.setText(line3);
ag->display.show();
}
}
/**
* @brief Set Text on 4 line
*
* @param line1
* @param line2
* @param line3
* @param line4
*
* @param line1
* @param line2
* @param line3
* @param line4
*/
void OledDisplay::setText(String &line1, String &line2, String &line3,
String &line4) {
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
*
* @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());
const char *line3, const char *line4) {
if (isDisplayOff) {
return;
}
if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) {
DISP()->firstPage();
do {
DISP()->setFont(u8g2_font_t0_16_tf);
DISP()->drawStr(1, 10, line1);
DISP()->drawStr(1, 25, line2);
DISP()->drawStr(1, 40, line3);
DISP()->drawStr(1, 55, line4);
} while (DISP()->nextPage());
} else if (ag->isBasic()) {
ag->display.clear();
ag->display.setCursor(0, 0);
ag->display.setText(line1);
ag->display.setCursor(0, 10);
ag->display.setText(line2);
ag->display.setCursor(0, 20);
ag->display.setText(line3);
ag->display.show();
}
}
/**
* @brief Update dashboard content
*
*
*/
void OledDisplay::showDashboard(void) { showDashboard(NULL); }
void OledDisplay::showDashboard(void) { showDashboard(DashBoardStatusNone); }
/**
* @brief Update dashboard content and error status
*
*
*/
void OledDisplay::showDashboard(const char *status) {
char strBuf[10];
void OledDisplay::showDashboard(DashboardStatus status) {
if (isDisplayOff) {
return;
}
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);
char strBuf[16];
const int icon_pos_x = 64;
xbm_icon_t xbm_icon = {
.width = 0,
.height = 0,
.icon = nullptr,
};
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);
if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) {
DISP()->firstPage();
do {
DISP()->setFont(u8g2_font_t0_16_tf);
switch (status) {
case DashBoardStatusNone: {
// Maybe show signal strength?
showTempHum(false);
break;
}
}
/** 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;
case DashBoardStatusWiFiIssue: {
DISP()->drawXBM(icon_pos_x, 0, 14, 11, WIFI_ISSUE_BITS);
showTempHum(false);
break;
}
case DashBoardStatusServerIssue: {
DISP()->drawXBM(icon_pos_x, 0, 14, 11, CLOUD_ISSUE_BITS);
showTempHum(false);
break;
}
case DashBoardStatusAddToDashboard: {
setCentralText(10, "Add To Dashboard");
break;
}
case DashBoardStatusDeviceId: {
setCentralText(10, ag->deviceId().c_str());
break;
}
case DashBoardStatusOfflineMode: {
DISP()->drawXBM(icon_pos_x, 0, 14, 14, OFFLINE_BITS);
showTempHum(false); // First true
break;
}
default:
break;
}
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 horizonal line */
DISP()->drawLine(1, 13, 128, 13);
/** Draw vertical line */
DISP()->drawLine(45, 14, 45, 64);
DISP()->drawLine(82, 14, 82, 64);
/** Show CO2 label */
DISP()->setFont(u8g2_font_t0_12_tf);
DISP()->drawUTF8(1, 27, "CO2");
/** 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));
DISP()->setFont(u8g2_font_t0_22b_tf);
int co2 = round(value.getAverage(Measurements::CO2));
if (utils::isValidCO2(co2)) {
sprintf(strBuf, "%d", co2);
} else {
sprintf(strBuf, "%s", "-");
}
DISP()->drawStr(48, 48, strBuf);
DISP()->drawStr(1, 48, strBuf);
/** Show CO2 value index */
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()->drawStr(1, 61, "ppm");
/** Draw vertical line */
DISP()->drawLine(52, 14, 52, 64);
DISP()->drawLine(97, 14, 97, 64);
/** Draw PM2.5 label */
DISP()->setFont(u8g2_font_t0_12_tf);
DISP()->drawUTF8(48, 61, "ug/m³");
}
DISP()->drawStr(55, 27, "PM2.5");
/** 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);
/** Draw PM2.5 value */
int pm25 = round(value.getAverage(Measurements::PM25));
if (utils::isValidPm(pm25)) {
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
pm25 = round(value.getCorrectedPM25(true));
}
if (config.isPmStandardInUSAQI()) {
sprintf(strBuf, "%d", ag->pms5003.convertPm25ToUsAqi(pm25));
} else {
sprintf(strBuf, "%d", pm25);
}
} else { /** Show invalid value. */
sprintf(strBuf, "%s", "-");
}
DISP()->setFont(u8g2_font_t0_22b_tf);
DISP()->drawStr(55, 48, strBuf);
/** Draw PM2.5 unit */
DISP()->setFont(u8g2_font_t0_12_tf);
if (config.isPmStandardInUSAQI()) {
DISP()->drawUTF8(55, 61, "AQI");
} else {
DISP()->drawUTF8(55, 61, "ug/m³");
}
/** Draw tvocIndexlabel */
DISP()->setFont(u8g2_font_t0_12_tf);
DISP()->drawStr(100, 27, "VOC:");
/** Draw tvocIndexvalue */
int tvoc = round(value.getAverage(Measurements::TVOC));
if (utils::isValidVOC(tvoc)) {
sprintf(strBuf, "%d", tvoc);
} else {
sprintf(strBuf, "%s", "-");
}
DISP()->drawStr(100, 39, strBuf);
/** Draw NOx label */
int nox = round(value.getAverage(Measurements::NOx));
DISP()->drawStr(100, 53, "NOx:");
if (utils::isValidNOx(nox)) {
sprintf(strBuf, "%d", nox);
} else {
sprintf(strBuf, "%s", "-");
}
DISP()->drawStr(100, 63, strBuf);
} while (DISP()->nextPage());
} else if (ag->isBasic()) {
ag->display.clear();
/** Set CO2 */
int co2 = round(value.getAverage(Measurements::CO2));
if (utils::isValidCO2(co2)) {
snprintf(strBuf, sizeof(strBuf), "CO2:%d", co2);
} else {
snprintf(strBuf, sizeof(strBuf), "CO2:-");
}
ag->display.setCursor(0, 0);
ag->display.setText(strBuf);
/** Set PM */
int pm25 = round(value.getAverage(Measurements::PM25));
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
pm25 = round(value.getCorrectedPM25(true));
}
ag->display.setCursor(0, 12);
if (utils::isValidPm(pm25)) {
snprintf(strBuf, sizeof(strBuf), "PM2.5:%d", pm25);
} else {
snprintf(strBuf, sizeof(strBuf), "PM2.5:-");
}
ag->display.setText(strBuf);
/** Set temperature and humidity */
float temp = value.getCorrectedTempHum(Measurements::Temperature, 1);
if (utils::isValidTemperature(temp)) {
if (config.isTemperatureUnitInF()) {
snprintf(strBuf, sizeof(strBuf), "T:%0.1f F",
utils::degreeC_To_F(temp));
} else {
snprintf(strBuf, sizeof(strBuf), "T:%0.1f C", temp);
}
} else {
if (config.isTemperatureUnitInF()) {
snprintf(strBuf, sizeof(strBuf), "T:-F");
} else {
snprintf(strBuf, sizeof(strBuf), "T:-C");
}
}
ag->display.setCursor(0, 24);
ag->display.setText(strBuf);
int rhum = round(value.getCorrectedTempHum(Measurements::Humidity, 1));
if (utils::isValidHumidity(rhum)) {
snprintf(strBuf, sizeof(strBuf), "H:%d %%", rhum);
} else {
snprintf(strBuf, sizeof(strBuf), "H:- %%");
}
ag->display.setCursor(0, 36);
ag->display.setText(strBuf);
ag->display.show();
}
}
void OledDisplay::setBrightness(int percent) {
if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) {
if (percent == 0) {
isDisplayOff = true;
// Clear display.
DISP()->firstPage();
do {
} while (DISP()->nextPage());
} else {
isDisplayOff = false;
DISP()->setContrast((127 * percent) / 100);
}
} else if (ag->isBasic()) {
if (percent == 0) {
isDisplayOff = true;
// Clear display.
ag->display.clear();
ag->display.show();
} else {
isDisplayOff = false;
ag->display.setContrast((255 * percent) / 100);
}
}
}
#ifdef ESP32
void OledDisplay::showFirmwareUpdateVersion(String version) {
if (isDisplayOff) {
return;
}
DISP()->firstPage();
do {
DISP()->setFont(u8g2_font_t0_16_tf);
x_start = (DISP()->getWidth() - DISP()->getStrWidth(label.c_str()))/2;
DISP()->drawStr(x_start, 60, label.c_str());
setCentralText(20, "Firmware Update");
setCentralText(40, "New version");
setCentralText(60, version.c_str());
} while (DISP()->nextPage());
}
void OledDisplay::showFirmwareUpdateProgress(int percent) {
if (isDisplayOff) {
return;
}
DISP()->firstPage();
do {
DISP()->setFont(u8g2_font_t0_16_tf);
setCentralText(20, "Firmware Update");
setCentralText(50, String("Updating... ") + String(percent) + String("%"));
} while (DISP()->nextPage());
}
void OledDisplay::showFirmwareUpdateSuccess(int count) {
if (isDisplayOff) {
return;
}
DISP()->firstPage();
do {
DISP()->setFont(u8g2_font_t0_16_tf);
setCentralText(20, "Firmware Update");
setCentralText(40, "Success");
setCentralText(60, String("Rebooting... ") + String(count));
} while (DISP()->nextPage());
}
void OledDisplay::showFirmwareUpdateFailed(void) {
if (isDisplayOff) {
return;
}
DISP()->firstPage();
do {
DISP()->setFont(u8g2_font_t0_16_tf);
setCentralText(20, "Firmware Update");
setCentralText(40, "fail, will retry");
// setCentralText(60, "will retry");
} while (DISP()->nextPage());
}
void OledDisplay::showFirmwareUpdateSkipped(void) {
if (isDisplayOff) {
return;
}
DISP()->firstPage();
do {
DISP()->setFont(u8g2_font_t0_16_tf);
setCentralText(20, "Firmware Update");
setCentralText(40, "skipped");
} while (DISP()->nextPage());
}
void OledDisplay::showFirmwareUpdateUpToDate(void) {
if (isDisplayOff) {
return;
}
DISP()->firstPage();
do {
DISP()->setFont(u8g2_font_t0_16_tf);
setCentralText(20, "Firmware Update");
setCentralText(40, "up to date");
} while (DISP()->nextPage());
}
#else
#endif
void OledDisplay::showRebooting(void) {
if (ag->isOne() || ag->isPro3_3() || ag->isPro4_2()) {
DISP()->firstPage();
do {
DISP()->setFont(u8g2_font_t0_16_tf);
// setCentralText(20, "Firmware Update");
setCentralText(40, "Rebooting...");
// setCentralText(60, String("Retry after 24h"));
} while (DISP()->nextPage());
} else if (ag->isBasic()) {
ag->display.clear();
ag->display.setCursor(0, 20);
ag->display.setText("Rebooting...");
ag->display.show();
}
}

View File

@ -14,15 +14,34 @@ private:
bool isBegin = false;
void *u8g2 = NULL;
Measurements &value;
bool isDisplayOff = false;
typedef struct {
int width;
int height;
unsigned char *icon;
} xbm_icon_t;
void showTempHum(bool hasStatus);
void setCentralText(int y, String text);
void setCentralText(int y, const char *text);
void showIcon(int x, int y, xbm_icon_t *icon);
public:
OledDisplay(Configuration &config, Measurements &value,
Stream &log);
OledDisplay(Configuration &config, Measurements &value, Stream &log);
~OledDisplay();
enum DashboardStatus {
DashBoardStatusNone,
DashBoardStatusWiFiIssue,
DashBoardStatusServerIssue,
DashBoardStatusAddToDashboard,
DashBoardStatusDeviceId,
DashBoardStatusOfflineMode,
};
void setAirGradient(AirGradient *ag);
bool begin(void);
bool begin(void);
void end(void);
void setText(String &line1, String &line2, String &line3);
void setText(const char *line1, const char *line2, const char *line3);
@ -30,8 +49,19 @@ public:
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);
void showDashboard(DashboardStatus status);
void setBrightness(int percent);
#ifdef ESP32
void showFirmwareUpdateVersion(String version);
void showFirmwareUpdateProgress(int percent);
void showFirmwareUpdateSuccess(int count);
void showFirmwareUpdateFailed(void);
void showFirmwareUpdateSkipped(void);
void showFirmwareUpdateUpToDate(void);
#else
#endif
void showRebooting(void);
};
#endif /** _AG_OLED_DISPLAY_H_ */

View File

@ -1,5 +1,7 @@
#include "AgStateMachine.h"
#include "AgOledDisplay.h"
#define LED_TEST_BLINK_DELAY 50 /** ms */
#define LED_FAST_BLINK_DELAY 250 /** ms */
#define LED_SLOW_BLINK_DELAY 1000 /** ms */
#define LED_SHORT_BLINK_DELAY 500 /** ms */
@ -7,6 +9,13 @@
#define SENSOR_CO2_CALIB_COUNTDOWN_MAX 5 /** sec */
#define RGB_COLOR_R 255, 0, 0 /** Red */
#define RGB_COLOR_G 0, 255, 0 /** Green */
#define RGB_COLOR_Y 255, 150, 0 /** Yellow */
#define RGB_COLOR_O 255, 40, 0 /** Orange */
#define RGB_COLOR_P 180, 0, 255 /** Purple */
#define RGB_COLOR_CLEAR 0, 0, 0 /** No color */
/**
* @brief Animation LED bar with color
*
@ -40,187 +49,222 @@ void StateMachine::ledStatusBlinkDelay(uint32_t ms) {
}
/**
* @brief Led bar show led color status
* @brief Led bar show PM or CO2 led color status
*
* @return true if all led bar are used, false othwerwise
*/
void StateMachine::sensorhandleLeds(void) {
bool StateMachine::sensorhandleLeds(void) {
int totalLedUsed = 0;
switch (config.getLedBarMode()) {
case LedBarMode::LedBarModeCO2:
co2handleLeds();
totalLedUsed = co2handleLeds();
break;
case LedBarMode::LedBarModePm:
pm25handleLeds();
totalLedUsed = pm25handleLeds();
break;
default:
ag->ledBar.clear();
break;
}
if (totalLedUsed == ag->ledBar.getNumberOfLeds()) {
return true;
}
// Clear the rest of unused led
int startIndex = totalLedUsed + 1;
for (int i = startIndex; i <= ag->ledBar.getNumberOfLeds(); i++) {
ag->ledBar.setColor(RGB_COLOR_CLEAR, ag->ledBar.getNumberOfLeds() - i);
}
return false;
}
/**
* @brief Show CO2 LED status
*
* @return return total number of led that are used on the monitor
*/
void StateMachine::co2handleLeds(void) {
int co2Value = value.CO2;
if (co2Value <= 400) {
int StateMachine::co2handleLeds(void) {
int totalUsed = ag->ledBar.getNumberOfLeds();
int co2Value = round(value.getAverage(Measurements::CO2));
if (co2Value <= 600) {
/** G; 1 */
ag->ledBar.setColor(0, 255, 0, ag->ledBar.getNumberOfLeds() - 1);
} else if (co2Value <= 700) {
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1);
totalUsed = 1;
} else if (co2Value <= 800) {
/** GG; 2 */
ag->ledBar.setColor(0, 255, 0, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(0, 255, 0, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 2);
totalUsed = 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) {
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 3);
totalUsed = 3;
} else if (co2Value <= 1250) {
/** OOOO; 4 */
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 4);
totalUsed = 4;
} else if (co2Value <= 1500) {
/** OOOOO; 5 */
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 5);
totalUsed = 5;
} else if (co2Value <= 1750) {
/** 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) {
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 6);
totalUsed = 6;
} else if (co2Value <= 2000) {
/** 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 */
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 6);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 7);
totalUsed = 7;
} else if (co2Value <= 3000) {
/** PPPPPPPP; 8 */
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 6);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 7);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 8);
totalUsed = 8;
} else { /** > 3000 */
/* 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);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 6);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 7);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 8);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 9);
totalUsed = 9;
}
return totalUsed;
}
/**
* @brief Show PM2.5 LED status
*
* @return return total number of led that are used on the monitor
*/
void StateMachine::pm25handleLeds(void) {
int pm25Value = value.pm25_1;
int StateMachine::pm25handleLeds(void) {
int totalUsed = ag->ledBar.getNumberOfLeds();
int pm25Value = round(value.getAverage(Measurements::PM25));
if (config.hasSensorSHT && config.isPMCorrectionEnabled()) {
pm25Value = round(value.getCorrectedPM25(true));
}
if (pm25Value <= 5) {
/** G; 1 */
ag->ledBar.setColor(0, 255, 0, ag->ledBar.getNumberOfLeds() - 1);
} else if (pm25Value <= 10) {
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1);
totalUsed = 1;
} else if (pm25Value <= 9) {
/** GG; 2 */
ag->ledBar.setColor(0, 255, 0, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(0, 255, 0, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_G, ag->ledBar.getNumberOfLeds() - 2);
totalUsed = 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);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 3);
totalUsed = 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);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_Y, ag->ledBar.getNumberOfLeds() - 4);
totalUsed = 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);
/** OOOOO; 5 */
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 5);
totalUsed = 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) {
/** OOOOOO; 6 */
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(RGB_COLOR_O, ag->ledBar.getNumberOfLeds() - 6);
totalUsed = 6;
} else if (pm25Value <= 100) {
/** 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) {
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 6);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 7);
totalUsed = 7;
} else if (pm25Value <= 125) {
/** 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 */
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 6);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 7);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 8);
totalUsed = 8;
} else if (pm25Value <= 225) {
/** PPPPPPPPP; 9 */
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 6);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 7);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 8);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 9);
totalUsed = 9;
} else { /** > 225 */
/* 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);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 1);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 2);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 3);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 4);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 5);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 6);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 7);
ag->ledBar.setColor(RGB_COLOR_R, ag->ledBar.getNumberOfLeds() - 8);
ag->ledBar.setColor(RGB_COLOR_P, ag->ledBar.getNumberOfLeds() - 9);
totalUsed = 9;
}
return totalUsed;
}
void StateMachine::co2Calibration(void) {
@ -229,10 +273,13 @@ void StateMachine::co2Calibration(void) {
/** Count down to 0 then start */
for (int i = 0; i < SENSOR_CO2_CALIB_COUNTDOWN_MAX; i++) {
if (ag->isOne()) {
if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3()) {
String str =
"after " + String(SENSOR_CO2_CALIB_COUNTDOWN_MAX - i) + " sec";
disp.setText("Start CO2 calib", str.c_str(), "");
} else if (ag->isBasic()) {
String str = String(SENSOR_CO2_CALIB_COUNTDOWN_MAX - i) + " sec";
disp.setText("CO2 Calib", "after", str.c_str());
} else {
logInfo("Start CO2 calib after " +
String(SENSOR_CO2_CALIB_COUNTDOWN_MAX - i) + " sec");
@ -241,14 +288,16 @@ void StateMachine::co2Calibration(void) {
}
if (ag->s8.setBaselineCalibration()) {
if (ag->isOne()) {
if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3()) {
disp.setText("Calibration", "success", "");
} else if (ag->isBasic()) {
disp.setText("CO2 Calib", "success", "");
} else {
logInfo("CO2 Calibration: success");
}
delay(1000);
if (ag->isOne()) {
disp.setText("Wait for", "calib finish", "...");
if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic()) {
disp.setText("Wait for", "calib done", "...");
} else {
logInfo("CO2 Calibration: Wait for calibration finish...");
}
@ -259,16 +308,18 @@ void StateMachine::co2Calibration(void) {
delay(1000);
count++;
}
if (ag->isOne()) {
if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic()) {
String str = "after " + String(count);
disp.setText("Calib finish", str.c_str(), "sec");
disp.setText("Calib done", str.c_str(), "sec");
} else {
logInfo("CO2 Calibration: finish after " + String(count) + " sec");
}
delay(2000);
} else {
if (ag->isOne()) {
if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3()) {
disp.setText("Calibration", "failure!!!", "");
} else if (ag->isBasic()) {
disp.setText("CO2 calib", "failure!!!", "");
} else {
logInfo("CO2 Calibration: failure!!!");
}
@ -284,15 +335,16 @@ void StateMachine::co2Calibration(void) {
if (ag->s8.setAbcPeriod(config.getCO2CalibrationAbcDays() * 24)) {
resultStr = "successful";
}
String fromStr = String(curHour/24) + " days";
if(curHour == 0){
String fromStr = String(curHour / 24) + " days";
if (curHour == 0) {
fromStr = "off";
}
String toStr = String(config.getCO2CalibrationAbcDays()) + " days";
if(config.getCO2CalibrationAbcDays() == 0) {
if (config.getCO2CalibrationAbcDays() == 0) {
toStr = "off";
}
String msg = "Setting S8 from " + fromStr + " to " + toStr + " " + resultStr;
String msg =
"Setting S8 from " + fromStr + " to " + toStr + " " + resultStr;
logInfo(msg);
}
} else {
@ -302,40 +354,61 @@ void StateMachine::co2Calibration(void) {
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;
if (ag->isOne()) {
ag->ledBar.clear();
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();
}
} else {
} else if (ag->isOpenAir()) {
ledBarRunTest();
}
}
}
void StateMachine::ledBarPowerUpTest(void) {
if (ag->isOne()) {
ag->ledBar.clear();
}
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);
if (ag->isOne()) {
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);
} else if (ag->isOpenAir()) {
for (int i = 0; i < 100; i++) {
ag->statusLed.setOn();
delay(LED_TEST_BLINK_DELAY);
ag->statusLed.setOff();
delay(LED_TEST_BLINK_DELAY);
}
}
}
void StateMachine::runLedTest(char color) {
@ -399,8 +472,11 @@ StateMachine::~StateMachine() {}
* @param state
*/
void StateMachine::displayHandle(AgStateMachineState state) {
// Ignore handle if not ONE_INDOOR board
if (!ag->isOne()) {
// Ignore handle if not support display
if (!(ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic())) {
if (state == AgStateMachineCo2Calibration) {
co2Calibration();
}
return;
}
@ -414,19 +490,18 @@ void StateMachine::displayHandle(AgStateMachineState 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);
if (ag->isBasic()) {
String ssid = "\"airgradient-" + ag->deviceId() + "\" " +
String(wifiConnectCountDown) + String("s");
disp.setText("Connect tohotspot:", ssid.c_str(), "");
} else {
String line1 = String(wifiConnectCountDown) + "s to connect";
String line2 = "to WiFi hotspot:";
String line3 = "\"airgradient-";
String line4 = ag->deviceId() + "\"";
disp.setText(line1, line2, line3, line4);
}
wifiConnectCountDown--;
}
break;
@ -440,7 +515,12 @@ void StateMachine::displayHandle(AgStateMachineState state) {
break;
}
case AgStateMachineWiFiOkServerConnecting: {
disp.setText("Connecting to", "Server", "...");
if (ag->isBasic()) {
disp.setText("Connecting", "to", "Server...");
} else {
disp.setText("Connecting to", "Server", "...");
}
break;
}
case AgStateMachineWiFiOkServerConnected: {
@ -456,32 +536,45 @@ void StateMachine::displayHandle(AgStateMachineState state) {
break;
}
case AgStateMachineWiFiOkServerOkSensorConfigFailed: {
disp.setText("Monitor not", "setup on", "dashboard");
if (ag->isBasic()) {
disp.setText("Monitor", "not on", "dashboard");
} else {
disp.setText("Monitor not", "setup on", "dashboard");
}
break;
}
case AgStateMachineWiFiLost: {
disp.showDashboard("WiFi N/A");
disp.showDashboard(OledDisplay::DashBoardStatusWiFiIssue);
break;
}
case AgStateMachineServerLost: {
disp.showDashboard("Server N/A");
disp.showDashboard(OledDisplay::DashBoardStatusServerIssue);
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());
if (addToDashBoard) {
uint32_t ms = (uint32_t)(millis() - addToDashboardTime);
if (ms >= 5000) {
addToDashboardTime = millis();
if (addToDashBoardToggle) {
disp.showDashboard(OledDisplay::DashBoardStatusAddToDashboard);
} else {
disp.showDashboard(OledDisplay::DashBoardStatusDeviceId);
}
addToDashBoardToggle = !addToDashBoardToggle;
}
addToDashBoard = !addToDashBoard;
} else {
disp.showDashboard();
}
break;
}
case AgStateMachineNormal: {
disp.showDashboard();
if (config.isOfflineMode()) {
disp.showDashboard(
OledDisplay::DashBoardStatusOfflineMode);
} else {
disp.showDashboard();
}
break;
}
case AgStateMachineCo2Calibration:
@ -503,8 +596,11 @@ void StateMachine::displayHandle(void) { displayHandle(dispState); }
*
*/
void StateMachine::displaySetAddToDashBoard(void) {
if (addToDashBoard == false) {
addToDashboardTime = 0;
addToDashBoardToggle = true;
}
addToDashBoard = true;
addToDashboardTime = millis();
}
void StateMachine::displayClearAddToDashBoard(void) { addToDashBoard = false; }
@ -525,26 +621,31 @@ void StateMachine::displayWiFiConnectCountDown(int count) {
void StateMachine::ledAnimationInit(void) { ledBarAnimationCount = -1; }
/**
* @brief Handle LED from state
* @brief Handle LED from state, only handle LED if board type is: One Indoor or
* Open Air
*
* @param state
*/
void StateMachine::handleLeds(AgStateMachineState state) {
/** Ignore if board type if not ONE_INDOOR or OPEN_AIR_OUTDOOR */
if ((ag->getBoardType() != BoardType::ONE_INDOOR) &&
(ag->getBoardType() != BoardType::OPEN_AIR_OUTDOOR)) {
return;
}
if (state > AgStateMachineNormal) {
logError("ledHandle: state invalid");
return;
}
ledState = state;
if (ag->isOne()) {
ag->ledBar.clear(); // Set all LED OFF
}
switch (state) {
case AgStateMachineWiFiManagerMode: {
/** In WiFi Manager Mode */
/** Turn LED OFF */
/** Turn midle LED Color */
/** Turn middle LED Color */
if (ag->isOne()) {
ag->ledBar.clear();
ag->ledBar.setColor(0, 0, 255, ag->ledBar.getNumberOfLeds() / 2);
} else {
ag->statusLed.setToggle();
@ -554,6 +655,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
case AgStateMachineWiFiManagerPortalActive: {
/** WiFi Manager has connected to mobile phone */
if (ag->isOne()) {
ag->ledBar.clear();
ag->ledBar.setColor(0, 0, 255);
} else {
ag->statusLed.setOn();
@ -564,6 +666,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
/** after SSID and PW entered and OK clicked, connection to WiFI network is
* attempted */
if (ag->isOne()) {
ag->ledBar.clear();
ledBarSingleLedAnimation(255, 255, 255);
} else {
ag->statusLed.setOff();
@ -573,6 +676,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
case AgStateMachineWiFiManagerStaConnected: {
/** Connecting to WiFi worked */
if (ag->isOne()) {
ag->ledBar.clear();
ag->ledBar.setColor(255, 255, 255);
} else {
ag->statusLed.setOff();
@ -582,6 +686,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
case AgStateMachineWiFiOkServerConnecting: {
/** once connected to WiFi an attempt to reach the server is performed */
if (ag->isOne()) {
ag->ledBar.clear();
ledBarSingleLedAnimation(0, 255, 0);
} else {
ag->statusLed.setOff();
@ -591,6 +696,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
case AgStateMachineWiFiOkServerConnected: {
/** Server is reachable, all fine */
if (ag->isOne()) {
ag->ledBar.clear();
ag->ledBar.setColor(0, 255, 0);
} else {
ag->statusLed.setOff();
@ -607,6 +713,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
case AgStateMachineWiFiManagerConnectFailed: {
/** Cannot connect to WiFi (e.g. wrong password, WPA Enterprise etc.) */
if (ag->isOne()) {
ag->ledBar.clear();
ag->ledBar.setColor(255, 0, 0);
} else {
ag->statusLed.setOff();
@ -625,6 +732,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
/** Connected to WiFi but server not reachable, e.g. firewall block/
* whitelisting needed etc. */
if (ag->isOne()) {
ag->ledBar.clear();
ag->ledBar.setColor(233, 183, 54); /** orange */
} else {
ag->statusLed.setOff();
@ -641,6 +749,7 @@ void StateMachine::handleLeds(AgStateMachineState state) {
case AgStateMachineWiFiOkServerOkSensorConfigFailed: {
/** Server reachable but sensor not configured correctly */
if (ag->isOne()) {
ag->ledBar.clear();
ag->ledBar.setColor(139, 24, 248); /** violet */
} else {
ag->statusLed.setOff();
@ -658,11 +767,10 @@ void StateMachine::handleLeds(AgStateMachineState state) {
/** 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();
bool allUsed = sensorhandleLeds();
if (allUsed == false) {
ag->ledBar.setColor(255, 0, 0, 0);
}
} else {
ag->statusLed.setOff();
}
@ -672,24 +780,23 @@ void StateMachine::handleLeds(AgStateMachineState state) {
/** 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();
bool allUsed = sensorhandleLeds();
if (allUsed == false) {
ag->ledBar.setColor(233, 183, 54, 0);
}
} else {
ag->statusLed.setOff();
}
break;
}
case AgStateMachineSensorConfigFailed: {
/** Server is reachable but there is some conguration issue to be fixed on
/** 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();
bool allUsed = sensorhandleLeds();
if (allUsed == false) {
ag->ledBar.setColor(139, 24, 248, 0);
}
} else {
ag->statusLed.setOff();
}
@ -706,6 +813,8 @@ void StateMachine::handleLeds(AgStateMachineState state) {
case AgStateMachineLedBarTest:
ledBarTest();
break;
case AgStateMachineLedBarPowerUpTest:
ledBarPowerUpTest();
default:
break;
}
@ -759,3 +868,7 @@ void StateMachine::executeCo2Calibration(void) {
void StateMachine::executeLedBarTest(void) {
handleLeds(AgStateMachineLedBarTest);
}
void StateMachine::executeLedBarPowerUpTest(void) {
handleLeds(AgStateMachineLedBarPowerUpTest);
}

View File

@ -17,17 +17,19 @@ private:
Measurements &value;
Configuration &config;
bool addToDashBoard = false;
bool addToDashBoardToggle = false;
uint32_t addToDashboardTime;
int wifiConnectCountDown;
int ledBarAnimationCount;
void ledBarSingleLedAnimation(uint8_t r, uint8_t g, uint8_t b);
void ledStatusBlinkDelay(uint32_t delay);
void sensorhandleLeds(void);
void co2handleLeds(void);
void pm25handleLeds(void);
bool sensorhandleLeds(void);
int co2handleLeds(void);
int pm25handleLeds(void);
void co2Calibration(void);
void ledBarTest(void);
void ledBarPowerUpTest(void);
void ledBarRunTest(void);
void runLedTest(char color);
@ -49,6 +51,7 @@ public:
AgStateMachineState getLedState(void);
void executeCo2Calibration(void);
void executeLedBarTest(void);
void executeLedBarPowerUpTest(void);
};
#endif /** _AG_STATE_MACHINE_H_ */

File diff suppressed because it is too large Load Diff

View File

@ -1,76 +1,270 @@
#ifndef _AG_VALUE_H_
#define _AG_VALUE_H_
#include <Arduino.h>
#include "AgConfigure.h"
#include "AirGradient.h"
#include "App/AppDef.h"
#include "Libraries/Arduino_JSON/src/Arduino_JSON.h"
#include "Main/utils.h"
#include <Arduino.h>
#include <cstdint>
#include <vector>
class Measurements {
private:
// Generic struct for update indication for respective value
struct Update {
int invalidCounter; // Counting on how many invalid value that are passed to update function
int max; // Maximum length of the period of the moving average
float avg; // Moving average value, updated every update function called
};
// Reading type for sensor value that outputs float
struct FloatValue {
float sumValues; // Total value from each update
std::vector<float> listValues; // List of update value that are kept
Update update;
};
// Reading type for sensor value that outputs integer
struct IntegerValue {
unsigned long sumValues; // Total value from each update; unsigned long to accomodate TVOx and
// NOx raw data
std::vector<int> listValues; // List of update value that are kept
Update update;
};
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(Configuration &config);
~Measurements() {}
float Temperature;
int Humidity;
int CO2;
int TVOC;
int TVOCRaw;
int NOx;
int NOxRaw;
struct Measures {
float temperature[2];
float humidity[2];
float co2;
float tvoc; // Index value
float tvoc_raw;
float nox; // Index value
float nox_raw;
float pm_01[2]; // pm 1.0 atmospheric environment
float pm_25[2]; // pm 2.5 atmospheric environment
float pm_10[2]; // pm 10 atmospheric environment
float pm_01_sp[2]; // pm 1.0 standard particle
float pm_25_sp[2]; // pm 2.5 standard particle
float pm_10_sp[2]; // pm 10 standard particle
float pm_03_pc[2]; // particle count 0.3
float pm_05_pc[2]; // particle count 0.5
float pm_01_pc[2]; // particle count 1.0
float pm_25_pc[2]; // particle count 2.5
float pm_5_pc[2]; // particle count 5.0
float pm_10_pc[2]; // particle count 10
int bootCount;
int signal;
uint32_t freeHeap;
};
int pm25_1;
int pm01_1;
int pm10_1;
int pm03PCount_1;
float temp_1;
int hum_1;
void setAirGradient(AirGradient *ag);
int pm25_2;
int pm01_2;
int pm10_2;
int pm03PCount_2;
float temp_2;
int hum_2;
// Enumeration for every AG measurements
enum MeasurementType {
Temperature,
Humidity,
CO2,
TVOC, // index value
TVOCRaw,
NOx, // index value
NOxRaw,
PM01, // PM1.0 under atmospheric environment
PM25, // PM2.5 under athompheric environment
PM10, // PM10 under atmospheric environment
PM01_SP, // PM1.0 standard particle
PM25_SP, // PM2.5 standard particle
PM10_SP, // PM10 standard particle
PM03_PC, // Particle 0.3 count
PM05_PC, // Particle 0.5 count
PM01_PC, // Particle 1.0 count
PM25_PC, // Particle 2.5 count
PM5_PC, // Particle 5.0 count
PM10_PC, // Particle 10 count
};
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;
void printCurrentAverage();
String toString(bool isLocal, AgFirmwareMode fwMode, int rssi, void* _ag, void* _config);
/**
* @brief Set each MeasurementType maximum period length for moving average
*
* @param type the target measurement type to set
* @param max the maximum period length
*/
void maxPeriod(MeasurementType, int max);
/**
* @brief update target measurement type with new value.
* Each MeasurementType has last raw value and moving average value based on max period
* This function is for MeasurementType that use INT as the data type
*
* @param type measurement type that will be updated
* @param val (int) the new value
* @param ch (int) the MeasurementType channel, not every MeasurementType has more than 1 channel.
* Currently maximum channel is 2. Default: 1 (channel 1)
* @return false if new value invalid consecutively reach threshold (max period)
* @return true otherwise
*/
bool update(MeasurementType type, int val, int ch = 1);
/**
* @brief update target measurement type with new value.
* Each MeasurementType has last raw value and moving average value based on max period
* This function is for MeasurementType that use FLOAT as the data type
*
* @param type measurement type that will be updated
* @param val (float) the new value
* @param ch (int) the MeasurementType channel, not every MeasurementType has more than 1 channel.
* Currently maximum channel is 2. Default: 1 (channel 1)
* @return false if new value invalid consecutively reach threshold (max period)
* @return true otherwise
*/
bool update(MeasurementType type, float val, int ch = 1);
/**
* @brief Get the target measurement latest value
*
* @param type measurement type that will be retrieve
* @param ch target type value channel
* @return int measurement type value
*/
int get(MeasurementType type, int ch = 1);
/**
* @brief Get the target measurement latest value
*
* @param type measurement type that will be retrieve
* @param ch target type value channel
* @return float measurement type value
*/
float getFloat(MeasurementType type, int ch = 1);
/**
* @brief Get the target measurement average value
*
* @param type measurement type that will be retrieve
* @param ch target type value channel
* @return moving average value of target measurements type
*/
float getAverage(MeasurementType type, int ch = 1);
/**
* @brief Get Temperature or Humidity correction value
* Only if correction is applied from configuration or forceCorrection is True
*
* @param type measurement type either Temperature or Humidity
* @param ch target type value channel
* @param forceCorrection force using correction even though config correction is not applied, but
* not for CUSTOM
* @return correction value
*/
float getCorrectedTempHum(MeasurementType type, int ch = 1, bool forceCorrection = false);
/**
* @brief Get the Corrected PM25 object based on the correction algorithm from configuration
*
* If correction is not enabled, then will return the raw value (either average or last value)
*
* @param useAvg Use moving average value if true, otherwise use latest value
* @param ch MeasurementType channel
* @param forceCorrection force using correction even though config correction is not applied, default to EPA
* @return float Corrected PM2.5 value
*/
float getCorrectedPM25(bool useAvg = false, int ch = 1, bool forceCorrection = false);
/**
* build json payload for every measurements
*/
String toString(bool localServer, AgFirmwareMode fwMode, int rssi);
Measures getMeasures();
std::string buildMeasuresPayload(Measures &measures);
/**
* Set to true if want to debug every update value
*/
void setDebug(bool debug);
int bootCount();
void setBootCount(int bootCount);
#ifndef ESP8266
void setResetReason(esp_reset_reason_t reason);
#endif
private:
Configuration &config;
AirGradient *ag;
// Some declared as an array (channel), because FW_MODE_O_1PPx has two PMS5003T
FloatValue _temperature[2];
FloatValue _humidity[2];
IntegerValue _co2;
IntegerValue _tvoc; // Index value
IntegerValue _tvoc_raw;
IntegerValue _nox; // Index value
IntegerValue _nox_raw;
IntegerValue _pm_01[2]; // pm 1.0 atmospheric environment
IntegerValue _pm_25[2]; // pm 2.5 atmospheric environment
IntegerValue _pm_10[2]; // pm 10 atmospheric environment
IntegerValue _pm_01_sp[2]; // pm 1.0 standard particle
IntegerValue _pm_25_sp[2]; // pm 2.5 standard particle
IntegerValue _pm_10_sp[2]; // pm 10 standard particle
IntegerValue _pm_03_pc[2]; // particle count 0.3
IntegerValue _pm_05_pc[2]; // particle count 0.5
IntegerValue _pm_01_pc[2]; // particle count 1.0
IntegerValue _pm_25_pc[2]; // particle count 2.5
IntegerValue _pm_5_pc[2]; // particle count 5.0
IntegerValue _pm_10_pc[2]; // particle count 10
int _bootCount;
int _resetReason;
bool _debug = false;
/**
* @brief Get PMS5003 firmware version string
*
* @param fwCode
* @return String
*/
String pms5003FirmwareVersion(int fwCode);
/**
* @brief Get PMS5003T firmware version string
*
* @param fwCode
* @return String
*/
String pms5003TFirmwareVersion(int fwCode);
/**
* @brief Get firmware version string
*
* @param prefix Prefix firmware string
* @param fwCode Version code
* @return string
*/
String pms5003FirmwareVersionBase(String prefix, int fwCode);
/**
* Convert AgValue Type to string representation of the value
*/
String measurementTypeStr(MeasurementType type);
/**
* @brief check if provided channel is a valid channel or not
* abort program if invalid
*/
void validateChannel(int ch);
void printCurrentPMAverage(int ch);
JSONVar buildOutdoor(bool localServer, AgFirmwareMode fwMode);
JSONVar buildIndoor(bool localServer);
JSONVar buildPMS(int ch, bool allCh, bool withTempHum, bool compensate);
};
#endif /** _AG_VALUE_H_ */

View File

@ -13,7 +13,6 @@
*/
void WifiConnector::setAirGradient(AirGradient *ag) { this->ag = ag; }
#ifdef ESP32
/**
* @brief Construct a new Ag Wi Fi Connector:: Ag Wi Fi Connector object
*
@ -24,9 +23,6 @@ void WifiConnector::setAirGradient(AirGradient *ag) { this->ag = ag; }
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() {}
@ -45,38 +41,60 @@ bool WifiConnector::connect(void) {
}
}
WiFi.begin();
String wifiSSID = WIFI()->getWiFiSSID(true);
if (wifiSSID.isEmpty()) {
logInfo("Connected WiFi is empty, connect to default wifi \"" +
String(this->defaultSsid) + String("\""));
/** Set wifi connect */
WiFi.begin(this->defaultSsid, this->defaultPassword);
/** Wait for wifi connect to AP */
int count = 0;
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
count++;
if (count >= 15) {
logError("Try connect to default wifi \"" + String(this->defaultSsid) +
String("\" failed"));
break;
}
}
}
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()) {
WIFI()->setConfigPortalTimeoutCallback([this]() {_wifiTimeoutCallback();});
if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic()) {
disp.setText("Connecting to", "WiFi", "...");
} else {
logInfo("Connecting to WiFi...");
}
ssid = "airgradient-" + ag->deviceId();
#else
ssid = "AG-" + String(ESP.getChipId(), HEX);
#endif
// ssid = "AG-" + String(ESP.getChipId(), HEX);
WIFI()->setConfigPortalTimeout(WIFI_CONNECT_COUNTDOWN_MAX);
WiFiManagerParameter postToAg("chbPostToAg",
"Prevent Connection to AirGradient Server", "T",
2, "type=\"checkbox\" ", WFM_LABEL_AFTER);
WIFI()->addParameter(&postToAg);
WiFiManagerParameter postToAgInfo(
WiFiManagerParameter disableCloud("chbPostToAg", "Prevent Connection to AirGradient Server", "T",
2, "type=\"checkbox\" ", WFM_LABEL_AFTER);
WIFI()->addParameter(&disableCloud);
WiFiManagerParameter disableCloudInfo(
"<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);
"features. As a result you will not receive automatic firmware updates, "
"configuration settings from cloud and the measure data will not reach the AirGradient dashboard.</p>");
WIFI()->addParameter(&disableCloudInfo);
WIFI()->autoConnect(ssid.c_str(), WIFI_HOTSPOT_PASSWORD_DEFAULT);
logInfo("Wait for configure portal");
#ifdef ESP32
// Task handle WiFi connection.
xTaskCreate(
@ -84,6 +102,7 @@ bool WifiConnector::connect(void) {
WifiConnector *connector = (WifiConnector *)obj;
while (connector->_wifiConfigPortalActive()) {
connector->_wifiProcess();
vTaskDelay(1);
}
vTaskDelete(NULL);
},
@ -138,10 +157,14 @@ bool WifiConnector::connect(void) {
delay(1); // avoid watchdog timer reset.
}
#else
_wifiProcess();
#endif
/** Show display wifi connect result failed */
if (WiFi.isConnected() == false) {
sm.handleLeds(AgStateMachineWiFiManagerConnectFailed);
if (ag->isOne()) {
if (ag->isOne() || ag->isPro4_2() || ag->isPro3_3() || ag->isBasic()) {
sm.displayHandle(AgStateMachineWiFiManagerConnectFailed);
}
delay(6000);
@ -150,18 +173,15 @@ bool WifiConnector::connect(void) {
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");
String result = String(disableCloud.getValue());
logInfo("Setting disableCloudConnection set from " +
String(config.isCloudConnectionDisabled() ? "True" : "False") + String(" to ") +
String(result == "T" ? "True" : "False") + String(" successful"));
config.setDisableCloudConnection(result == "T");
}
hasPortalConfig = false;
}
#else
_wifiProcess();
#endif
return true;
}
@ -176,24 +196,6 @@ void WifiConnector::disconnect(void) {
}
}
#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)
*
@ -204,7 +206,6 @@ bool WifiConnector::wifiClientConnected(void) {
return WiFi.softAPgetStationNum() ? true : false;
}
#ifdef ESP32
/**
* @brief Handle WiFiManage softAP setup completed callback
*
@ -245,7 +246,8 @@ void WifiConnector::_wifiSaveParamCallback(void) {
bool WifiConnector::_wifiConfigPortalActive(void) {
return WIFI()->getConfigPortalActive();
}
#endif
void WifiConnector::_wifiTimeoutCallback(void) { connectorTimeout = true; }
/**
* @brief Process WiFiManager connection
*
@ -254,33 +256,67 @@ void WifiConnector::_wifiProcess() {
#ifdef ESP32
WIFI()->process();
#else
int count = WIFI_CONNECT_COUNTDOWN_MAX;
displayShowText(String(WIFI_CONNECT_COUNTDOWN_MAX) + " sec", "SSID:", ssid);
/** Wait for WiFi connect and show LED, display status */
uint32_t dispPeriod = millis();
uint32_t ledPeriod = millis();
bool clientConnectChanged = false;
AgStateMachineState stateOld = sm.getDisplayState();
while (WIFI()->getConfigPortalActive()) {
WIFI()->process();
uint32_t lastTime = millis();
uint32_t ms = (uint32_t)(millis() - lastTime);
if (ms >= 1000) {
lastTime = millis();
if (WiFi.isConnected() == false) {
/** Display countdown */
uint32_t ms;
if (ag->isOne() || (ag->isPro4_2()) || ag->isPro3_3() || ag->isBasic()) {
ms = (uint32_t)(millis() - dispPeriod);
if (ms >= 1000) {
dispPeriod = millis();
sm.displayHandle();
logInfo("displayHandle state: " + String(sm.getDisplayState()));
} else {
if (stateOld != sm.getDisplayState()) {
stateOld = sm.getDisplayState();
sm.displayHandle();
}
}
}
displayShowText(String(count) + " sec", "SSID:", ssid);
/** LED animations */
ms = (uint32_t)(millis() - ledPeriod);
if (ms >= 100) {
ledPeriod = millis();
sm.handleLeds();
}
count--;
// Timeout
if (count == 0) {
break;
/** 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);
}
if (!WiFi.isConnected()) {
displayShowText("Booting", "offline", "mode");
Serial.println("failed to connect and hit timeout");
delay(2500);
} else {
hasConfig = true;
// TODO This is for basic
if (ag->getBoardType() == DIY_BASIC) {
if (!WiFi.isConnected()) {
// disp.setText("Booting", "offline", "mode");
Serial.println("failed to connect and hit timeout");
delay(2500);
} else {
hasConfig = true;
}
}
#endif
}
@ -305,8 +341,6 @@ void WifiConnector::handle(void) {
if (ms >= 10000) {
lastRetry = millis();
WiFi.reconnect();
// Serial.printf("Re-Connect WiFi\r\n");
logInfo("Re-Connect WiFi");
}
}
@ -324,7 +358,16 @@ bool WifiConnector::isConnected(void) { return WiFi.isConnected(); }
* this method
*
*/
void WifiConnector::reset(void) { WIFI()->resetSettings(); }
void WifiConnector::reset(void) {
if(this->wifi == NULL) {
this->wifi = new WiFiManager();
if(this->wifi == NULL){
logInfo("reset failed");
return;
}
}
WIFI()->resetSettings();
}
/**
* @brief Get wifi RSSI
@ -339,3 +382,32 @@ int WifiConnector::RSSI(void) { return WiFi.RSSI(); }
* @return String
*/
String WifiConnector::localIpStr(void) { return WiFi.localIP().toString(); }
/**
* @brief Get status that wifi has configurated
*
* @return true Configurated
* @return false Not Configurated
*/
bool WifiConnector::hasConfigurated(void) {
if (WiFi.SSID().isEmpty()) {
return false;
}
return true;
}
/**
* @brief Get WiFi connection porttal timeout.
*
* @return true
* @return false
*/
bool WifiConnector::isConfigurePorttalTimeout(void) { return connectorTimeout; }
/**
* @brief Set wifi connect to default WiFi
*
*/
void WifiConnector::setDefault(void) {
WiFi.begin("airgradient", "cleanair");
}

View File

@ -12,44 +12,44 @@
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 connectorTimeout = 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 _wifiTimeoutCallback(void);
void _wifiProcess();
bool isConnected(void);
void reset(void);
int RSSI(void);
String localIpStr(void);
bool hasConfigurated(void);
bool isConfigurePorttalTimeout(void);
const char* defaultSsid = "airgradient";
const char* defaultPassword = "cleanair";
void setDefault(void);
};
#endif /** _AG_WIFI_CONNECTOR_H_ */

View File

@ -41,7 +41,14 @@ 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;
double ret;
if (value >= 0) {
ret = (int)(value * 100 + 0.5f);
} else {
ret = (int)(value * 100 - 0.5f);
}
return ret / 100;
}
String AirGradient::getBoardName(void) {
@ -58,6 +65,20 @@ bool AirGradient::isOne(void) {
return boardType == BoardType::ONE_INDOOR;
}
bool AirGradient::isOpenAir(void) {
return boardType == BoardType::OPEN_AIR_OUTDOOR;
}
bool AirGradient::isPro4_2(void) {
return boardType == BoardType::DIY_PRO_INDOOR_V4_2;
}
bool AirGradient::isPro3_3(void) {
return boardType == BoardType::DIY_PRO_INDOOR_V3_3;
}
bool AirGradient::isBasic(void) { return boardType == BoardType::DIY_BASIC; }
String AirGradient::deviceId(void) {
String mac = WiFi.macAddress();
mac.replace(":", "");

View File

@ -12,9 +12,50 @@
#include "S8/S8.h"
#include "Sgp41/Sgp41.h"
#include "Sht/Sht.h"
#include "Main/utils.h"
#ifndef GIT_VERSION
#define GIT_VERSION "snapshot"
#define GIT_VERSION "3.3.9-snap"
#endif
#ifndef ESP8266
// Airgradient server root ca certificate
const char *const AG_SERVER_ROOT_CA =
"-----BEGIN CERTIFICATE-----\n"
"MIIF4jCCA8oCCQD7MgvcaVWxkTANBgkqhkiG9w0BAQsFADCBsjELMAkGA1UEBhMC\n"
"VEgxEzARBgNVBAgMCkNoaWFuZyBNYWkxEDAOBgNVBAcMB01hZSBSaW0xGTAXBgNV\n"
"BAoMEEFpckdyYWRpZW50IEx0ZC4xFDASBgNVBAsMC1NlbnNvciBMYWJzMSgwJgYD\n"
"VQQDDB9BaXJHcmFkaWVudCBTZW5zb3IgTGFicyBSb290IENBMSEwHwYJKoZIhvcN\n"
"AQkBFhJjYUBhaXJncmFkaWVudC5jb20wHhcNMjEwOTE3MTE0NDE3WhcNNDEwOTEy\n"
"MTE0NDE3WjCBsjELMAkGA1UEBhMCVEgxEzARBgNVBAgMCkNoaWFuZyBNYWkxEDAO\n"
"BgNVBAcMB01hZSBSaW0xGTAXBgNVBAoMEEFpckdyYWRpZW50IEx0ZC4xFDASBgNV\n"
"BAsMC1NlbnNvciBMYWJzMSgwJgYDVQQDDB9BaXJHcmFkaWVudCBTZW5zb3IgTGFi\n"
"cyBSb290IENBMSEwHwYJKoZIhvcNAQkBFhJjYUBhaXJncmFkaWVudC5jb20wggIi\n"
"MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC6XkVQ4O9d5GcUjPYRgF/uaY6O\n"
"5ry1xCGvotxkEeKkBk99lB1oNUUfNsP5bwuDci4XKfY9Ro6/jmkfHSVcPAwUnjAt\n"
"BcHqZtA/cMXykaynf9yXPxPQN7XLu/Rk32RIfb90sIGS318xgNziCYvzWZmlxpxc\n"
"3gUcAgGtamlgZ6wD3yOHVo8B9aFNvmP16QwkUm8fKDHunJG+iX2Bxa4ka5FJovhG\n"
"TnUwtso6Vrn0JaWF9qWcPZE0JZMjFW8PYRriyJmHwr/nAXfPPKphD1oRO+oA7/jq\n"
"dYkrJw6+OHfFXnPB1xkeh4OPBzcCZHT5XWNfwBYazYpjcJa9ngGFSmg8lX1ac23C\n"
"zea1XJmSrPwbZbWxoQznnf7Y78mRjruYKgSP8rf74KYvBe/HGPL5NQyXQ3l6kwmu\n"
"CCUqfcC0wCWEtWESxwSdFE2qQii8CZ12kQExzvR2PrOIyKQYSdkGx9/RBZtAVPXP\n"
"hmLuRBQYHrF5Cxf1oIbBK8OMoNVgBm6ftt15t9Sq9dH5Aup2YR6WEJkVaYkYzZzK\n"
"X7M+SQcdbXp+hAO8PFpABJxkaDAO2kiB5Ov7pDYPAcmNFqnJT48AY0TZJeVeCa5W\n"
"sIv3lPvB/XcFjP0+aZxxNSEEwpGPUYgvKUYUUmb0NammlYQwZHKaShPEmZ3UZ0bp\n"
"VNt4p6374nzO376sSwIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQB/LfBPgTx7xKQB\n"
"JNMUhah17AFAn050NiviGJOHdPQely6u3DmJGg+ijEVlPWO1FEW3it+LOuNP5zOu\n"
"bhq8paTYIxPxtALIxw5ksykX9woDuX3H6FF9mPdQIbL7ft+3ZtZ4FWPui9dUtaPe\n"
"ZBmDFDi4U29nhWZK68JSp5QkWjfaYLV/vtag7120eVyGEPFZ0UAuTUNqpw+stOt9\n"
"gJ2ZxNx13xJ8ZnLK7qz1crPe8/8IVAdxbVLoY7JaWPLc//+VF+ceKicy8+4gV7zN\n"
"Gnq2IyM+CHFz8VYMLbW+3eVp4iJjTa72vae116kozboEIUVN9rgLqIKyVqQXiuoN\n"
"g3xY+yfncPB2+H/+lfyy6mepPIfgksd3+KeNxFADSc5EVY2JKEdorRodnAh7a8K6\n"
"WjTYgq+GjWXU2uQW2SyPt6Tu33OT8nBnu3NB80eT8WXgdVCkgsuyCuLvNRf1Xmze\n"
"igvurpU6JmQ1GlLgLJo8omJHTh1zIbkR9injPYne2v9ciHCoP6+LDEqe+rOsvPCB\n"
"C/o/iZ4svmYX4fWGuU7GgqZE8hhrC3+GdOTf2ADC752cYCZxBidXGtkrGNoHQKmQ\n"
"KCOMFBxZIvWteB3tUo3BKYz1D2CvKWz1wV4moc5JHkOgS+jqxhvOkQ/vfQBQ1pUY\n"
"TMui9BSwU7B1G2XjdLbfF3Dc67zaSg==\n"
"-----END CERTIFICATE-----\n";
#endif
/**
@ -134,6 +175,37 @@ public:
*/
bool isOne(void);
/**
* @brief Check that Airgradient object is OPEN_AIR
*
* @return true
* @return false
*/
bool isOpenAir(void);
/**
* @brief Check that Airgradient object is DIY_PRO 4.2 indoor
*
* @return true Yes
* @return false No
*/
bool isPro4_2(void);
/**
* @brief Check that Airgradient object is DIY_PRO 3.7 indoor
*
* @return true Yes
* @return false No
*/
bool isPro3_3(void);
/**
* @brief Check that Airgradient object is DIY_BASIC
*
* @return true Yes
* @return false No
*/
bool isBasic(void);
/**
* @brief Get device Id
*

View File

@ -14,6 +14,12 @@ const char *AgFirmwareModeName(AgFirmwareMode mode) {
return "0-1PS";
case FW_MODE_O_1P:
return "O-1P";
case FW_MODE_I_42PS:
return "DIY-PRO-I-4.2PS";
case FW_MODE_I_33PS:
return "DIY-PRO-I-3.3PS";
case FW_MODE_I_BASIC_40PS:
return "DIY-BASIC-I-4.0PS";
default:
break;
}

View File

@ -59,6 +59,10 @@ enum AgStateMachineState {
/* LED bar testing */
AgStateMachineLedBarTest,
AgStateMachineLedBarPowerUpTest,
/** OTA perform, show display status */
AgStateMachineOtaPerform,
/** LED: Show working state.
* Display: Show dashboard */
@ -90,13 +94,31 @@ enum ConfigurationControl {
ConfigurationControlBoth
};
enum PMCorrectionAlgorithm {
COR_ALGO_PM_UNKNOWN, // Unknown algorithm
COR_ALGO_PM_NONE, // No PM correction
COR_ALGO_PM_EPA_2021,
COR_ALGO_PM_SLR_CUSTOM,
};
// Don't change the order of the enum
enum TempHumCorrectionAlgorithm {
COR_ALGO_TEMP_HUM_UNKNOWN, // Unknown algorithm
COR_ALGO_TEMP_HUM_NONE, // No PM correction
COR_ALGO_TEMP_HUM_AG_PMS5003T_2024,
COR_ALGO_TEMP_HUM_SLR_CUSTOM
};
enum AgFirmwareMode {
FW_MODE_I_9PSL, /** ONE_INDOOR */
FW_MODE_O_1PST, /** PMS5003T, S8 and SGP41 */
FW_MODE_O_1PPT, /** PMS5003T_1, PMS5003T_2, SGP41 */
FW_MODE_O_1PP, /** PMS5003T_1, PMS5003T_2 */
FW_MODE_O_1PS, /** PMS5003T, S8 */
FW_MODE_O_1P, /** PMS5003T */
FW_MODE_I_9PSL, /** ONE_INDOOR */
FW_MODE_O_1PST, /** PMS5003T, S8 and SGP41 */
FW_MODE_O_1PPT, /** PMS5003T_1, PMS5003T_2, SGP41 */
FW_MODE_O_1PP, /** PMS5003T_1, PMS5003T_2 */
FW_MODE_O_1PS, /** PMS5003T, S8 */
FW_MODE_O_1P, /** PMS5003T */
FW_MODE_I_42PS, /** DIY_PRO 4.2 */
FW_MODE_I_33PS, /** DIY_PRO 3.3 */
FW_MODE_I_BASIC_40PS, /** DIY_BASIC 4.0 */
};
const char *AgFirmwareModeName(AgFirmwareMode mode);

View File

@ -1 +0,0 @@
.DS_Store

View File

@ -1,26 +0,0 @@
The MIT License (MIT)
This library is written and maintained by Richard Moore.
Major parts were derived from Project Nayuki's library.
Copyright (c) 2017 Richard Moore (https://github.com/ricmoo/QRCode)
Copyright (c) 2017 Project Nayuki (https://www.nayuki.io/page/qr-code-generator-library)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,677 +0,0 @@
QRCode
======
A simple library for generating [QR codes](https://en.wikipedia.org/wiki/QR_code) in C,
optimized for processing and memory constrained systems.
**Features:**
- Stack-based (no heap necessary; but you can use heap if you want)
- Low-memory foot print (relatively)
- Compile-time stripping of unecessary logic and constants
- MIT License; do with this as you please
Installing
----------
To install this library, download and save it to your Arduino libraries directory.
Rename the directory to QRCode (if downloaded from GitHub, the filename may be
qrcode-master; library names may not contain the hyphen, so it must be renamed)
API
---
**Generate a QR Code**
```c
// The structure to manage the QR code
QRCode qrcode;
// Allocate a chunk of memory to store the QR code
uint8_t qrcodeBytes[qrcode_getBufferSize()];
qrcode_initText(&qrcode, qrcodeBytes, 3, ECC_LOW, "HELLO WORLD");
```
**Draw a QR Code**
How a QR code is used will vary greatly from project to project. For example:
- Display on an OLED screen (128x64 nicely supports 2 side-by-side version 3 QR codes)
- Print as a bitmap on a thermal printer
- Store as a BMP (or with a some extra work, possibly a PNG) on an SD card
The following example prints a QR code to the Serial Monitor (it likely will
not be scannable, but is just for demonstration purposes).
```c
for (uint8 y = 0; y < qrcode.size; y++) {
for (uint8 x = 0; x < qrcode.size; x++) {
if (qrcode_getModule(&qrcode, x, y) {
Serial.print("**");
} else {
Serial.print(" ");
}
}
Serial.print("\n");
}
```
What is Version, Error Correction and Mode?
-------------------------------------------
A QR code is composed of many little squares, called **modules**, which represent
encoded data, with additional error correction (allowing partially damaged QR
codes to still be read).
The **version** of a QR code is a number between 1 and 40 (inclusive), which indicates
the size of the QR code. The width and height of a QR code are always equal (it is
square) and are equal to `4 * version + 17`.
The level of **error correction** is a number between 0 and 3 (inclusive), or can be
one of the symbolic names ECC_LOW, ECC_MEDIUM, ECC_QUARTILE and ECC_HIGH. Higher
levels of error correction sacrifice data capacity, but allow a larger portion of
the QR code to be damaged or unreadable.
The **mode** of a QR code is determined by the data being encoded. Each mode is encoded
internally using a compact representation, so lower modes can contain more data.
- **NUMERIC:** numbers (`0-9`)
- **ALPHANUMERIC:** uppercase letters (`A-Z`), numbers (`0-9`), the space (` `), dollar sign (`$`), percent sign (`%`), asterisk (`*`), plus (`+`), minus (`-`), decimal point (`.`), slash (`/`) and colon (`:`).
- **BYTE:** any character
Data Capacities
---------------
<table>
<tr>
<th rowspan="2">Version</th>
<th rowspan="2">Size</th>
<th rowspan="2">Error Correction</th>
<th colspan="3">Mode</th>
</tr>
<tr>
<th>Numeric</th>
<th>Alphanumeric</th>
<th>Byte</th>
</tr>
<tr>
<td rowspan="4">1</td>
<td rowspan="4">21 x 21</td>
<td>LOW</td><td>41</td><td>25</td><td>17</td>
</tr>
<tr>
<td>MEDIUM</td><td>34</td><td>20</td><td>14</td>
</tr>
<tr>
<td>QUARTILE</td><td>27</td><td>16</td><td>11</td>
</tr>
<tr>
<td>HIGH</td><td>17</td><td>10</td><td>7</td>
</tr>
<tr>
<td rowspan="4">2</td>
<td rowspan="4">25 x 25</td>
<td>LOW</td><td>77</td><td>47</td><td>32</td>
</tr>
<tr>
<td>MEDIUM</td><td>63</td><td>38</td><td>26</td>
</tr>
<tr>
<td>QUARTILE</td><td>48</td><td>29</td><td>20</td>
</tr>
<tr>
<td>HIGH</td><td>34</td><td>20</td><td>14</td>
</tr>
<tr>
<td rowspan="4">3</td>
<td rowspan="4">29 x 29</td>
<td>LOW</td><td>127</td><td>77</td><td>53</td>
</tr>
<tr>
<td>MEDIUM</td><td>101</td><td>61</td><td>42</td>
</tr>
<tr>
<td>QUARTILE</td><td>77</td><td>47</td><td>32</td>
</tr>
<tr>
<td>HIGH</td><td>58</td><td>35</td><td>24</td>
</tr>
<tr>
<td rowspan="4">4</td>
<td rowspan="4">33 x 33</td>
<td>LOW</td><td>187</td><td>114</td><td>78</td>
</tr>
<tr>
<td>MEDIUM</td><td>149</td><td>90</td><td>62</td>
</tr>
<tr>
<td>QUARTILE</td><td>111</td><td>67</td><td>46</td>
</tr>
<tr>
<td>HIGH</td><td>82</td><td>50</td><td>34</td>
</tr>
<tr>
<td rowspan="4">5</td>
<td rowspan="4">37 x 37</td>
<td>LOW</td><td>255</td><td>154</td><td>106</td>
</tr>
<tr>
<td>MEDIUM</td><td>202</td><td>122</td><td>84</td>
</tr>
<tr>
<td>QUARTILE</td><td>144</td><td>87</td><td>60</td>
</tr>
<tr>
<td>HIGH</td><td>106</td><td>64</td><td>44</td>
</tr>
<tr>
<td rowspan="4">6</td>
<td rowspan="4">41 x 41</td>
<td>LOW</td><td>322</td><td>195</td><td>134</td>
</tr>
<tr>
<td>MEDIUM</td><td>255</td><td>154</td><td>106</td>
</tr>
<tr>
<td>QUARTILE</td><td>178</td><td>108</td><td>74</td>
</tr>
<tr>
<td>HIGH</td><td>139</td><td>84</td><td>58</td>
</tr>
<tr>
<td rowspan="4">7</td>
<td rowspan="4">45 x 45</td>
<td>LOW</td><td>370</td><td>224</td><td>154</td>
</tr>
<tr>
<td>MEDIUM</td><td>293</td><td>178</td><td>122</td>
</tr>
<tr>
<td>QUARTILE</td><td>207</td><td>125</td><td>86</td>
</tr>
<tr>
<td>HIGH</td><td>154</td><td>93</td><td>64</td>
</tr>
<tr>
<td rowspan="4">8</td>
<td rowspan="4">49 x 49</td>
<td>LOW</td><td>461</td><td>279</td><td>192</td>
</tr>
<tr>
<td>MEDIUM</td><td>365</td><td>221</td><td>152</td>
</tr>
<tr>
<td>QUARTILE</td><td>259</td><td>157</td><td>108</td>
</tr>
<tr>
<td>HIGH</td><td>202</td><td>122</td><td>84</td>
</tr>
<tr>
<td rowspan="4">9</td>
<td rowspan="4">53 x 53</td>
<td>LOW</td><td>552</td><td>335</td><td>230</td>
</tr>
<tr>
<td>MEDIUM</td><td>432</td><td>262</td><td>180</td>
</tr>
<tr>
<td>QUARTILE</td><td>312</td><td>189</td><td>130</td>
</tr>
<tr>
<td>HIGH</td><td>235</td><td>143</td><td>98</td>
</tr>
<tr>
<td rowspan="4">10</td>
<td rowspan="4">57 x 57</td>
<td>LOW</td><td>652</td><td>395</td><td>271</td>
</tr>
<tr>
<td>MEDIUM</td><td>513</td><td>311</td><td>213</td>
</tr>
<tr>
<td>QUARTILE</td><td>364</td><td>221</td><td>151</td>
</tr>
<tr>
<td>HIGH</td><td>288</td><td>174</td><td>119</td>
</tr>
<tr>
<td rowspan="4">11</td>
<td rowspan="4">61 x 61</td>
<td>LOW</td><td>772</td><td>468</td><td>321</td>
</tr>
<tr>
<td>MEDIUM</td><td>604</td><td>366</td><td>251</td>
</tr>
<tr>
<td>QUARTILE</td><td>427</td><td>259</td><td>177</td>
</tr>
<tr>
<td>HIGH</td><td>331</td><td>200</td><td>137</td>
</tr>
<tr>
<td rowspan="4">12</td>
<td rowspan="4">65 x 65</td>
<td>LOW</td><td>883</td><td>535</td><td>367</td>
</tr>
<tr>
<td>MEDIUM</td><td>691</td><td>419</td><td>287</td>
</tr>
<tr>
<td>QUARTILE</td><td>489</td><td>296</td><td>203</td>
</tr>
<tr>
<td>HIGH</td><td>374</td><td>227</td><td>155</td>
</tr>
<tr>
<td rowspan="4">13</td>
<td rowspan="4">69 x 69</td>
<td>LOW</td><td>1022</td><td>619</td><td>425</td>
</tr>
<tr>
<td>MEDIUM</td><td>796</td><td>483</td><td>331</td>
</tr>
<tr>
<td>QUARTILE</td><td>580</td><td>352</td><td>241</td>
</tr>
<tr>
<td>HIGH</td><td>427</td><td>259</td><td>177</td>
</tr>
<tr>
<td rowspan="4">14</td>
<td rowspan="4">73 x 73</td>
<td>LOW</td><td>1101</td><td>667</td><td>458</td>
</tr>
<tr>
<td>MEDIUM</td><td>871</td><td>528</td><td>362</td>
</tr>
<tr>
<td>QUARTILE</td><td>621</td><td>376</td><td>258</td>
</tr>
<tr>
<td>HIGH</td><td>468</td><td>283</td><td>194</td>
</tr>
<tr>
<td rowspan="4">15</td>
<td rowspan="4">77 x 77</td>
<td>LOW</td><td>1250</td><td>758</td><td>520</td>
</tr>
<tr>
<td>MEDIUM</td><td>991</td><td>600</td><td>412</td>
</tr>
<tr>
<td>QUARTILE</td><td>703</td><td>426</td><td>292</td>
</tr>
<tr>
<td>HIGH</td><td>530</td><td>321</td><td>220</td>
</tr>
<tr>
<td rowspan="4">16</td>
<td rowspan="4">81 x 81</td>
<td>LOW</td><td>1408</td><td>854</td><td>586</td>
</tr>
<tr>
<td>MEDIUM</td><td>1082</td><td>656</td><td>450</td>
</tr>
<tr>
<td>QUARTILE</td><td>775</td><td>470</td><td>322</td>
</tr>
<tr>
<td>HIGH</td><td>602</td><td>365</td><td>250</td>
</tr>
<tr>
<td rowspan="4">17</td>
<td rowspan="4">85 x 85</td>
<td>LOW</td><td>1548</td><td>938</td><td>644</td>
</tr>
<tr>
<td>MEDIUM</td><td>1212</td><td>734</td><td>504</td>
</tr>
<tr>
<td>QUARTILE</td><td>876</td><td>531</td><td>364</td>
</tr>
<tr>
<td>HIGH</td><td>674</td><td>408</td><td>280</td>
</tr>
<tr>
<td rowspan="4">18</td>
<td rowspan="4">89 x 89</td>
<td>LOW</td><td>1725</td><td>1046</td><td>718</td>
</tr>
<tr>
<td>MEDIUM</td><td>1346</td><td>816</td><td>560</td>
</tr>
<tr>
<td>QUARTILE</td><td>948</td><td>574</td><td>394</td>
</tr>
<tr>
<td>HIGH</td><td>746</td><td>452</td><td>310</td>
</tr>
<tr>
<td rowspan="4">19</td>
<td rowspan="4">93 x 93</td>
<td>LOW</td><td>1903</td><td>1153</td><td>792</td>
</tr>
<tr>
<td>MEDIUM</td><td>1500</td><td>909</td><td>624</td>
</tr>
<tr>
<td>QUARTILE</td><td>1063</td><td>644</td><td>442</td>
</tr>
<tr>
<td>HIGH</td><td>813</td><td>493</td><td>338</td>
</tr>
<tr>
<td rowspan="4">20</td>
<td rowspan="4">97 x 97</td>
<td>LOW</td><td>2061</td><td>1249</td><td>858</td>
</tr>
<tr>
<td>MEDIUM</td><td>1600</td><td>970</td><td>666</td>
</tr>
<tr>
<td>QUARTILE</td><td>1159</td><td>702</td><td>482</td>
</tr>
<tr>
<td>HIGH</td><td>919</td><td>557</td><td>382</td>
</tr>
<tr>
<td rowspan="4">21</td>
<td rowspan="4">101 x 101</td>
<td>LOW</td><td>2232</td><td>1352</td><td>929</td>
</tr>
<tr>
<td>MEDIUM</td><td>1708</td><td>1035</td><td>711</td>
</tr>
<tr>
<td>QUARTILE</td><td>1224</td><td>742</td><td>509</td>
</tr>
<tr>
<td>HIGH</td><td>969</td><td>587</td><td>403</td>
</tr>
<tr>
<td rowspan="4">22</td>
<td rowspan="4">105 x 105</td>
<td>LOW</td><td>2409</td><td>1460</td><td>1003</td>
</tr>
<tr>
<td>MEDIUM</td><td>1872</td><td>1134</td><td>779</td>
</tr>
<tr>
<td>QUARTILE</td><td>1358</td><td>823</td><td>565</td>
</tr>
<tr>
<td>HIGH</td><td>1056</td><td>640</td><td>439</td>
</tr>
<tr>
<td rowspan="4">23</td>
<td rowspan="4">109 x 109</td>
<td>LOW</td><td>2620</td><td>1588</td><td>1091</td>
</tr>
<tr>
<td>MEDIUM</td><td>2059</td><td>1248</td><td>857</td>
</tr>
<tr>
<td>QUARTILE</td><td>1468</td><td>890</td><td>611</td>
</tr>
<tr>
<td>HIGH</td><td>1108</td><td>672</td><td>461</td>
</tr>
<tr>
<td rowspan="4">24</td>
<td rowspan="4">113 x 113</td>
<td>LOW</td><td>2812</td><td>1704</td><td>1171</td>
</tr>
<tr>
<td>MEDIUM</td><td>2188</td><td>1326</td><td>911</td>
</tr>
<tr>
<td>QUARTILE</td><td>1588</td><td>963</td><td>661</td>
</tr>
<tr>
<td>HIGH</td><td>1228</td><td>744</td><td>511</td>
</tr>
<tr>
<td rowspan="4">25</td>
<td rowspan="4">117 x 117</td>
<td>LOW</td><td>3057</td><td>1853</td><td>1273</td>
</tr>
<tr>
<td>MEDIUM</td><td>2395</td><td>1451</td><td>997</td>
</tr>
<tr>
<td>QUARTILE</td><td>1718</td><td>1041</td><td>715</td>
</tr>
<tr>
<td>HIGH</td><td>1286</td><td>779</td><td>535</td>
</tr>
<tr>
<td rowspan="4">26</td>
<td rowspan="4">121 x 121</td>
<td>LOW</td><td>3283</td><td>1990</td><td>1367</td>
</tr>
<tr>
<td>MEDIUM</td><td>2544</td><td>1542</td><td>1059</td>
</tr>
<tr>
<td>QUARTILE</td><td>1804</td><td>1094</td><td>751</td>
</tr>
<tr>
<td>HIGH</td><td>1425</td><td>864</td><td>593</td>
</tr>
<tr>
<td rowspan="4">27</td>
<td rowspan="4">125 x 125</td>
<td>LOW</td><td>3517</td><td>2132</td><td>1465</td>
</tr>
<tr>
<td>MEDIUM</td><td>2701</td><td>1637</td><td>1125</td>
</tr>
<tr>
<td>QUARTILE</td><td>1933</td><td>1172</td><td>805</td>
</tr>
<tr>
<td>HIGH</td><td>1501</td><td>910</td><td>625</td>
</tr>
<tr>
<td rowspan="4">28</td>
<td rowspan="4">129 x 129</td>
<td>LOW</td><td>3669</td><td>2223</td><td>1528</td>
</tr>
<tr>
<td>MEDIUM</td><td>2857</td><td>1732</td><td>1190</td>
</tr>
<tr>
<td>QUARTILE</td><td>2085</td><td>1263</td><td>868</td>
</tr>
<tr>
<td>HIGH</td><td>1581</td><td>958</td><td>658</td>
</tr>
<tr>
<td rowspan="4">29</td>
<td rowspan="4">133 x 133</td>
<td>LOW</td><td>3909</td><td>2369</td><td>1628</td>
</tr>
<tr>
<td>MEDIUM</td><td>3035</td><td>1839</td><td>1264</td>
</tr>
<tr>
<td>QUARTILE</td><td>2181</td><td>1322</td><td>908</td>
</tr>
<tr>
<td>HIGH</td><td>1677</td><td>1016</td><td>698</td>
</tr>
<tr>
<td rowspan="4">30</td>
<td rowspan="4">137 x 137</td>
<td>LOW</td><td>4158</td><td>2520</td><td>1732</td>
</tr>
<tr>
<td>MEDIUM</td><td>3289</td><td>1994</td><td>1370</td>
</tr>
<tr>
<td>QUARTILE</td><td>2358</td><td>1429</td><td>982</td>
</tr>
<tr>
<td>HIGH</td><td>1782</td><td>1080</td><td>742</td>
</tr>
<tr>
<td rowspan="4">31</td>
<td rowspan="4">141 x 141</td>
<td>LOW</td><td>4417</td><td>2677</td><td>1840</td>
</tr>
<tr>
<td>MEDIUM</td><td>3486</td><td>2113</td><td>1452</td>
</tr>
<tr>
<td>QUARTILE</td><td>2473</td><td>1499</td><td>1030</td>
</tr>
<tr>
<td>HIGH</td><td>1897</td><td>1150</td><td>790</td>
</tr>
<tr>
<td rowspan="4">32</td>
<td rowspan="4">145 x 145</td>
<td>LOW</td><td>4686</td><td>2840</td><td>1952</td>
</tr>
<tr>
<td>MEDIUM</td><td>3693</td><td>2238</td><td>1538</td>
</tr>
<tr>
<td>QUARTILE</td><td>2670</td><td>1618</td><td>1112</td>
</tr>
<tr>
<td>HIGH</td><td>2022</td><td>1226</td><td>842</td>
</tr>
<tr>
<td rowspan="4">33</td>
<td rowspan="4">149 x 149</td>
<td>LOW</td><td>4965</td><td>3009</td><td>2068</td>
</tr>
<tr>
<td>MEDIUM</td><td>3909</td><td>2369</td><td>1628</td>
</tr>
<tr>
<td>QUARTILE</td><td>2805</td><td>1700</td><td>1168</td>
</tr>
<tr>
<td>HIGH</td><td>2157</td><td>1307</td><td>898</td>
</tr>
<tr>
<td rowspan="4">34</td>
<td rowspan="4">153 x 153</td>
<td>LOW</td><td>5253</td><td>3183</td><td>2188</td>
</tr>
<tr>
<td>MEDIUM</td><td>4134</td><td>2506</td><td>1722</td>
</tr>
<tr>
<td>QUARTILE</td><td>2949</td><td>1787</td><td>1228</td>
</tr>
<tr>
<td>HIGH</td><td>2301</td><td>1394</td><td>958</td>
</tr>
<tr>
<td rowspan="4">35</td>
<td rowspan="4">157 x 157</td>
<td>LOW</td><td>5529</td><td>3351</td><td>2303</td>
</tr>
<tr>
<td>MEDIUM</td><td>4343</td><td>2632</td><td>1809</td>
</tr>
<tr>
<td>QUARTILE</td><td>3081</td><td>1867</td><td>1283</td>
</tr>
<tr>
<td>HIGH</td><td>2361</td><td>1431</td><td>983</td>
</tr>
<tr>
<td rowspan="4">36</td>
<td rowspan="4">161 x 161</td>
<td>LOW</td><td>5836</td><td>3537</td><td>2431</td>
</tr>
<tr>
<td>MEDIUM</td><td>4588</td><td>2780</td><td>1911</td>
</tr>
<tr>
<td>QUARTILE</td><td>3244</td><td>1966</td><td>1351</td>
</tr>
<tr>
<td>HIGH</td><td>2524</td><td>1530</td><td>1051</td>
</tr>
<tr>
<td rowspan="4">37</td>
<td rowspan="4">165 x 165</td>
<td>LOW</td><td>6153</td><td>3729</td><td>2563</td>
</tr>
<tr>
<td>MEDIUM</td><td>4775</td><td>2894</td><td>1989</td>
</tr>
<tr>
<td>QUARTILE</td><td>3417</td><td>2071</td><td>1423</td>
</tr>
<tr>
<td>HIGH</td><td>2625</td><td>1591</td><td>1093</td>
</tr>
<tr>
<td rowspan="4">38</td>
<td rowspan="4">169 x 169</td>
<td>LOW</td><td>6479</td><td>3927</td><td>2699</td>
</tr>
<tr>
<td>MEDIUM</td><td>5039</td><td>3054</td><td>2099</td>
</tr>
<tr>
<td>QUARTILE</td><td>3599</td><td>2181</td><td>1499</td>
</tr>
<tr>
<td>HIGH</td><td>2735</td><td>1658</td><td>1139</td>
</tr>
<tr>
<td rowspan="4">39</td>
<td rowspan="4">173 x 173</td>
<td>LOW</td><td>6743</td><td>4087</td><td>2809</td>
</tr>
<tr>
<td>MEDIUM</td><td>5313</td><td>3220</td><td>2213</td>
</tr>
<tr>
<td>QUARTILE</td><td>3791</td><td>2298</td><td>1579</td>
</tr>
<tr>
<td>HIGH</td><td>2927</td><td>1774</td><td>1219</td>
</tr>
<tr>
<td rowspan="4">40</td>
<td rowspan="4">177 x 177</td>
<td>LOW</td><td>7089</td><td>4296</td><td>2953</td>
</tr>
<tr>
<td>MEDIUM</td><td>5596</td><td>3391</td><td>2331</td>
</tr>
<tr>
<td>QUARTILE</td><td>3993</td><td>2420</td><td>1663</td>
</tr>
<tr>
<td>HIGH</td><td>3057</td><td>1852</td><td>1273</td>
</tr>
</table>
Special Thanks
--------------
A HUGE thank you to [Project Nayuki](https://www.nayuki.io/) for the
[QR code C++ library](https://github.com/nayuki/QR-Code-generator/tree/master/cpp)
which was critical in development of this library.
License
-------
MIT License.

View File

@ -1,56 +0,0 @@
/**
* QRCode
*
* A quick example of generating a QR code.
*
* This prints the QR code to the serial monitor as solid blocks. Each module
* is two characters wide, since the monospace font used in the serial monitor
* is approximately twice as tall as wide.
*
*/
#include "qrcode.h"
void setup() {
Serial.begin(115200);
// Start time
uint32_t dt = millis();
// Create the QR code
QRCode qrcode;
uint8_t qrcodeData[qrcode_getBufferSize(3)];
qrcode_initText(&qrcode, qrcodeData, 3, 0, "HELLO WORLD");
// Delta time
dt = millis() - dt;
Serial.print("QR Code Generation Time: ");
Serial.print(dt);
Serial.print("\n");
// Top quiet zone
Serial.print("\n\n\n\n");
for (uint8_t y = 0; y < qrcode.size; y++) {
// Left quiet zone
Serial.print(" ");
// Each horizontal module
for (uint8_t x = 0; x < qrcode.size; x++) {
// Print each module (UTF-8 \u2588 is a solid block)
Serial.print(qrcode_getModule(&qrcode, x, y) ? "\u2588\u2588": " ");
}
Serial.print("\n");
}
// Bottom quiet zone
Serial.print("\n\n\n\n");
}
void loop() {
}

View File

@ -1,62 +0,0 @@
Data = [
["1", "41", "25", "17", "34", "20", "14","27", "16", "11","17", "10", "7"],
["2", "77", "47", "32", "63", "38", "26", "48", "29", "20", "34", "20", "14"],
["3", "127", "77", "53", "101", "61", "42", "77", "47", "32", "58", "35", "24"],
["4", "187", "114", "78", "149", "90", "62", "111", "67", "46", "82", "50", "34"],
["5", "255", "154", "106", "202", "122", "84", "144", "87", "60", "106", "64", "44"],
["6", "322", "195", "134", "255", "154", "106", "178", "108", "74", "139", "84", "58"],
["7", "370", "224", "154", "293", "178", "122", "207", "125", "86", "154", "93", "64"],
["8", "461", "279", "192", "365", "221", "152", "259", "157", "108", "202", "122", "84"],
["9", "552", "335", "230", "432", "262", "180", "312", "189", "130", "235", "143", "98"],
["10", "652", "395", "271", "513", "311", "213", "364", "221", "151", "288", "174", "119"],
["11", "772", "468", "321", "604", "366", "251", "427", "259", "177", "331", "200", "137"],
["12", "883", "535", "367", "691", "419", "287", "489", "296", "203", "374", "227", "155"],
["13", "1022", "619", "425", "796", "483", "331", "580", "352", "241", "427", "259", "177"],
["14", "1101", "667", "458", "871", "528", "362", "621", "376", "258", "468", "283", "194"],
["15", "1250", "758", "520", "991", "600", "412", "703", "426", "292", "530", "321", "220"],
["16", "1408", "854", "586", "1082", "656", "450", "775", "470", "322", "602", "365", "250"],
["17", "1548", "938", "644", "1212", "734", "504", "876", "531", "364", "674", "408", "280"],
["18", "1725", "1046", "718", "1346", "816", "560", "948", "574", "394", "746", "452", "310"],
["19", "1903", "1153", "792", "1500", "909", "624", "1063", "644", "442", "813", "493", "338"],
["20", "2061", "1249", "858", "1600", "970", "666", "1159", "702", "482", "919", "557", "382"],
["21", "2232", "1352", "929", "1708", "1035", "711", "1224", "742", "509", "969", "587", "403"],
["22", "2409", "1460", "1003", "1872", "1134", "779", "1358", "823", "565", "1056", "640", "439"],
["23", "2620", "1588", "1091", "2059", "1248", "857", "1468", "890", "611", "1108", "672", "461"],
["24", "2812", "1704", "1171", "2188", "1326", "911", "1588", "963", "661", "1228", "744", "511"],
["25", "3057", "1853", "1273", "2395", "1451", "997", "1718", "1041", "715", "1286", "779", "535"],
["26", "3283", "1990", "1367", "2544", "1542", "1059", "1804", "1094", "751", "1425", "864", "593"],
["27", "3517", "2132", "1465", "2701", "1637", "1125", "1933", "1172", "805", "1501", "910", "625"],
["28", "3669", "2223", "1528", "2857", "1732", "1190", "2085", "1263", "868", "1581", "958", "658"],
["29", "3909", "2369", "1628", "3035", "1839", "1264", "2181", "1322", "908", "1677", "1016", "698"],
["30", "4158", "2520", "1732", "3289", "1994", "1370", "2358", "1429", "982", "1782", "1080", "742"],
["31", "4417", "2677", "1840", "3486", "2113", "1452", "2473", "1499", "1030", "1897", "1150", "790"],
["32", "4686", "2840", "1952", "3693", "2238", "1538", "2670", "1618", "1112", "2022", "1226", "842"],
["33", "4965", "3009", "2068", "3909", "2369", "1628", "2805", "1700", "1168", "2157", "1307", "898"],
["34", "5253", "3183", "2188", "4134", "2506", "1722", "2949", "1787", "1228", "2301", "1394", "958"],
["35", "5529", "3351", "2303", "4343", "2632", "1809", "3081", "1867", "1283", "2361", "1431", "983"],
["36", "5836", "3537", "2431", "4588", "2780", "1911", "3244", "1966", "1351", "2524", "1530", "1051"],
["37", "6153", "3729", "2563", "4775", "2894", "1989", "3417", "2071", "1423", "2625", "1591", "1093"],
["38", "6479", "3927", "2699", "5039", "3054", "2099", "3599", "2181", "1499", "2735", "1658", "1139"],
["39", "6743", "4087", "2809", "5313", "3220", "2213", "3791", "2298", "1579", "2927", "1774", "1219"],
["40", "7089", "4296", "2953", "5596", "3391", "2331", "3993", "2420", "1663", "3057", "1852", "1273"],
]
Template = ''' <tr>
<td rowspan="4">%s</td>
<td rowspan="4">%s</td>
<td>LOW</td><td>%s</td><td>%s</td><td>%s</td>
</tr>
<tr>
<td>MEDIUM</td><td>%s</td><td>%s</td><td>%s</td>
</tr>
<tr>
<td>QUARTILE</td><td>%s</td><td>%s</td><td>%s</td>
</tr>
<tr>
<td>HIGH</td><td>%s</td><td>%s</td><td>%s</td>
</tr>'''
for data in Data:
data = data[:]
size = 4 * int(data[0]) + 17
data.insert(1, "%d x %d" % (size, size))
print Template % tuple(data)

View File

@ -1,31 +0,0 @@
# Datatypes (KEYWORD1)
bool KEYWORD1
uint8_t KEYWORD1
QRCode KEYWORD1
# Methods and Functions (KEYWORD2)
qrcode_getBufferSize KEYWORD2
qrcode_initText KEYWORD2
qrcode_initBytes KEYWORD2
qrcode_getModule KEYWORD2
# Instances (KEYWORD2)
# Constants (LITERAL1)
false LITERAL1
true LITERAL1
ECC_LOW LITERAL1
ECC_MEDIUM LITERAL1
ECC_QUARTILE LITERAL1
ECC_HIGH LITERAL1
MODE_NUMERIC LITERAL1
MODE_ALPHANUMERIC LITERAL1
MODE_BYTE LITERAL1

View File

@ -1,10 +0,0 @@
name=QRCode
version=0.0.1
author=Richard Moore <me@ricmoo.com>
maintainer=Richard Moore <me@ricmoo.com>
sentence=A simple QR code generation library.
paragraph=A simple QR code generation library.
category=Other
url=https://github.com/ricmoo/qrcode/
architectures=*
includes=qrcode.h

View File

@ -1,876 +0,0 @@
/**
* The MIT License (MIT)
*
* This library is written and maintained by Richard Moore.
* Major parts were derived from Project Nayuki's library.
*
* Copyright (c) 2017 Richard Moore (https://github.com/ricmoo/QRCode)
* Copyright (c) 2017 Project Nayuki (https://www.nayuki.io/page/qr-code-generator-library)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Special thanks to Nayuki (https://www.nayuki.io/) from which this library was
* heavily inspired and compared against.
*
* See: https://github.com/nayuki/QR-Code-generator/tree/master/cpp
*/
#include "qrcode.h"
#include <stdlib.h>
#include <string.h>
#pragma mark - Error Correction Lookup tables
#if LOCK_VERSION == 0
static const uint16_t NUM_ERROR_CORRECTION_CODEWORDS[4][40] = {
// 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level
{ 10, 16, 26, 36, 48, 64, 72, 88, 110, 130, 150, 176, 198, 216, 240, 280, 308, 338, 364, 416, 442, 476, 504, 560, 588, 644, 700, 728, 784, 812, 868, 924, 980, 1036, 1064, 1120, 1204, 1260, 1316, 1372}, // Medium
{ 7, 10, 15, 20, 26, 36, 40, 48, 60, 72, 80, 96, 104, 120, 132, 144, 168, 180, 196, 224, 224, 252, 270, 300, 312, 336, 360, 390, 420, 450, 480, 510, 540, 570, 570, 600, 630, 660, 720, 750}, // Low
{ 17, 28, 44, 64, 88, 112, 130, 156, 192, 224, 264, 308, 352, 384, 432, 480, 532, 588, 650, 700, 750, 816, 900, 960, 1050, 1110, 1200, 1260, 1350, 1440, 1530, 1620, 1710, 1800, 1890, 1980, 2100, 2220, 2310, 2430}, // High
{ 13, 22, 36, 52, 72, 96, 108, 132, 160, 192, 224, 260, 288, 320, 360, 408, 448, 504, 546, 600, 644, 690, 750, 810, 870, 952, 1020, 1050, 1140, 1200, 1290, 1350, 1440, 1530, 1590, 1680, 1770, 1860, 1950, 2040}, // Quartile
};
static const uint8_t NUM_ERROR_CORRECTION_BLOCKS[4][40] = {
// Version: (note that index 0 is for padding, and is set to an illegal value)
// 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level
{ 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49}, // Medium
{ 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25}, // Low
{ 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81}, // High
{ 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68}, // Quartile
};
static const uint16_t NUM_RAW_DATA_MODULES[40] = {
// 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
208, 359, 567, 807, 1079, 1383, 1568, 1936, 2336, 2768, 3232, 3728, 4256, 4651, 5243, 5867, 6523,
// 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
7211, 7931, 8683, 9252, 10068, 10916, 11796, 12708, 13652, 14628, 15371, 16411, 17483, 18587,
// 32, 33, 34, 35, 36, 37, 38, 39, 40
19723, 20891, 22091, 23008, 24272, 25568, 26896, 28256, 29648
};
// @TODO: Put other LOCK_VERSIONS here
#elif LOCK_VERSION == 3
static const int16_t NUM_ERROR_CORRECTION_CODEWORDS[4] = {
26, 15, 44, 36
};
static const int8_t NUM_ERROR_CORRECTION_BLOCKS[4] = {
1, 1, 2, 2
};
static const uint16_t NUM_RAW_DATA_MODULES = 567;
#else
#error Unsupported LOCK_VERSION (add it...)
#endif
static int max(int a, int b) {
if (a > b) { return a; }
return b;
}
/*
static int abs(int value) {
if (value < 0) { return -value; }
return value;
}
*/
#pragma mark - Mode testing and conversion
static int8_t getAlphanumeric(char c) {
if (c >= '0' && c <= '9') { return (c - '0'); }
if (c >= 'A' && c <= 'Z') { return (c - 'A' + 10); }
switch (c) {
case ' ': return 36;
case '$': return 37;
case '%': return 38;
case '*': return 39;
case '+': return 40;
case '-': return 41;
case '.': return 42;
case '/': return 43;
case ':': return 44;
}
return -1;
}
static bool isAlphanumeric(const char *text, uint16_t length) {
while (length != 0) {
if (getAlphanumeric(text[--length]) == -1) { return false; }
}
return true;
}
static bool isNumeric(const char *text, uint16_t length) {
while (length != 0) {
char c = text[--length];
if (c < '0' || c > '9') { return false; }
}
return true;
}
#pragma mark - Counting
// We store the following tightly packed (less 8) in modeInfo
// <=9 <=26 <= 40
// NUMERIC ( 10, 12, 14);
// ALPHANUMERIC ( 9, 11, 13);
// BYTE ( 8, 16, 16);
static char getModeBits(uint8_t version, uint8_t mode) {
// Note: We use 15 instead of 16; since 15 doesn't exist and we cannot store 16 (8 + 8) in 3 bits
// hex(int("".join(reversed([('00' + bin(x - 8)[2:])[-3:] for x in [10, 9, 8, 12, 11, 15, 14, 13, 15]])), 2))
unsigned int modeInfo = 0x7bbb80a;
#if LOCK_VERSION == 0 || LOCK_VERSION > 9
if (version > 9) { modeInfo >>= 9; }
#endif
#if LOCK_VERSION == 0 || LOCK_VERSION > 26
if (version > 26) { modeInfo >>= 9; }
#endif
char result = 8 + ((modeInfo >> (3 * mode)) & 0x07);
if (result == 15) { result = 16; }
return result;
}
#pragma mark - BitBucket
typedef struct BitBucket {
uint32_t bitOffsetOrWidth;
uint16_t capacityBytes;
uint8_t *data;
} BitBucket;
/*
void bb_dump(BitBucket *bitBuffer) {
printf("Buffer: ");
for (uint32_t i = 0; i < bitBuffer->capacityBytes; i++) {
printf("%02x", bitBuffer->data[i]);
if ((i % 4) == 3) { printf(" "); }
}
printf("\n");
}
*/
static uint16_t bb_getGridSizeBytes(uint8_t size) {
return (((size * size) + 7) / 8);
}
static uint16_t bb_getBufferSizeBytes(uint32_t bits) {
return ((bits + 7) / 8);
}
static void bb_initBuffer(BitBucket *bitBuffer, uint8_t *data, int32_t capacityBytes) {
bitBuffer->bitOffsetOrWidth = 0;
bitBuffer->capacityBytes = capacityBytes;
bitBuffer->data = data;
memset(data, 0, bitBuffer->capacityBytes);
}
static void bb_initGrid(BitBucket *bitGrid, uint8_t *data, uint8_t size) {
bitGrid->bitOffsetOrWidth = size;
bitGrid->capacityBytes = bb_getGridSizeBytes(size);
bitGrid->data = data;
memset(data, 0, bitGrid->capacityBytes);
}
static void bb_appendBits(BitBucket *bitBuffer, uint32_t val, uint8_t length) {
uint32_t offset = bitBuffer->bitOffsetOrWidth;
for (int8_t i = length - 1; i >= 0; i--, offset++) {
bitBuffer->data[offset >> 3] |= ((val >> i) & 1) << (7 - (offset & 7));
}
bitBuffer->bitOffsetOrWidth = offset;
}
/*
void bb_setBits(BitBucket *bitBuffer, uint32_t val, int offset, uint8_t length) {
for (int8_t i = length - 1; i >= 0; i--, offset++) {
bitBuffer->data[offset >> 3] |= ((val >> i) & 1) << (7 - (offset & 7));
}
}
*/
static void bb_setBit(BitBucket *bitGrid, uint8_t x, uint8_t y, bool on) {
uint32_t offset = y * bitGrid->bitOffsetOrWidth + x;
uint8_t mask = 1 << (7 - (offset & 0x07));
if (on) {
bitGrid->data[offset >> 3] |= mask;
} else {
bitGrid->data[offset >> 3] &= ~mask;
}
}
static void bb_invertBit(BitBucket *bitGrid, uint8_t x, uint8_t y, bool invert) {
uint32_t offset = y * bitGrid->bitOffsetOrWidth + x;
uint8_t mask = 1 << (7 - (offset & 0x07));
bool on = ((bitGrid->data[offset >> 3] & (1 << (7 - (offset & 0x07)))) != 0);
if (on ^ invert) {
bitGrid->data[offset >> 3] |= mask;
} else {
bitGrid->data[offset >> 3] &= ~mask;
}
}
static bool bb_getBit(BitBucket *bitGrid, uint8_t x, uint8_t y) {
uint32_t offset = y * bitGrid->bitOffsetOrWidth + x;
return (bitGrid->data[offset >> 3] & (1 << (7 - (offset & 0x07)))) != 0;
}
#pragma mark - Drawing Patterns
// XORs the data modules in this QR Code with the given mask pattern. Due to XOR's mathematical
// properties, calling applyMask(m) twice with the same value is equivalent to no change at all.
// This means it is possible to apply a mask, undo it, and try another mask. Note that a final
// well-formed QR Code symbol needs exactly one mask applied (not zero, not two, etc.).
static void applyMask(BitBucket *modules, BitBucket *isFunction, uint8_t mask) {
uint8_t size = modules->bitOffsetOrWidth;
for (uint8_t y = 0; y < size; y++) {
for (uint8_t x = 0; x < size; x++) {
if (bb_getBit(isFunction, x, y)) { continue; }
bool invert = 0;
switch (mask) {
case 0: invert = (x + y) % 2 == 0; break;
case 1: invert = y % 2 == 0; break;
case 2: invert = x % 3 == 0; break;
case 3: invert = (x + y) % 3 == 0; break;
case 4: invert = (x / 3 + y / 2) % 2 == 0; break;
case 5: invert = x * y % 2 + x * y % 3 == 0; break;
case 6: invert = (x * y % 2 + x * y % 3) % 2 == 0; break;
case 7: invert = ((x + y) % 2 + x * y % 3) % 2 == 0; break;
}
bb_invertBit(modules, x, y, invert);
}
}
}
static void setFunctionModule(BitBucket *modules, BitBucket *isFunction, uint8_t x, uint8_t y, bool on) {
bb_setBit(modules, x, y, on);
bb_setBit(isFunction, x, y, true);
}
// Draws a 9*9 finder pattern including the border separator, with the center module at (x, y).
static void drawFinderPattern(BitBucket *modules, BitBucket *isFunction, uint8_t x, uint8_t y) {
uint8_t size = modules->bitOffsetOrWidth;
for (int8_t i = -4; i <= 4; i++) {
for (int8_t j = -4; j <= 4; j++) {
uint8_t dist = max(abs(i), abs(j)); // Chebyshev/infinity norm
int16_t xx = x + j, yy = y + i;
if (0 <= xx && xx < size && 0 <= yy && yy < size) {
setFunctionModule(modules, isFunction, xx, yy, dist != 2 && dist != 4);
}
}
}
}
// Draws a 5*5 alignment pattern, with the center module at (x, y).
static void drawAlignmentPattern(BitBucket *modules, BitBucket *isFunction, uint8_t x, uint8_t y) {
for (int8_t i = -2; i <= 2; i++) {
for (int8_t j = -2; j <= 2; j++) {
setFunctionModule(modules, isFunction, x + j, y + i, max(abs(i), abs(j)) != 1);
}
}
}
// Draws two copies of the format bits (with its own error correction code)
// based on the given mask and this object's error correction level field.
static void drawFormatBits(BitBucket *modules, BitBucket *isFunction, uint8_t ecc, uint8_t mask) {
uint8_t size = modules->bitOffsetOrWidth;
// Calculate error correction code and pack bits
uint32_t data = ecc << 3 | mask; // errCorrLvl is uint2, mask is uint3
uint32_t rem = data;
for (int i = 0; i < 10; i++) {
rem = (rem << 1) ^ ((rem >> 9) * 0x537);
}
data = data << 10 | rem;
data ^= 0x5412; // uint15
// Draw first copy
for (uint8_t i = 0; i <= 5; i++) {
setFunctionModule(modules, isFunction, 8, i, ((data >> i) & 1) != 0);
}
setFunctionModule(modules, isFunction, 8, 7, ((data >> 6) & 1) != 0);
setFunctionModule(modules, isFunction, 8, 8, ((data >> 7) & 1) != 0);
setFunctionModule(modules, isFunction, 7, 8, ((data >> 8) & 1) != 0);
for (int8_t i = 9; i < 15; i++) {
setFunctionModule(modules, isFunction, 14 - i, 8, ((data >> i) & 1) != 0);
}
// Draw second copy
for (int8_t i = 0; i <= 7; i++) {
setFunctionModule(modules, isFunction, size - 1 - i, 8, ((data >> i) & 1) != 0);
}
for (int8_t i = 8; i < 15; i++) {
setFunctionModule(modules, isFunction, 8, size - 15 + i, ((data >> i) & 1) != 0);
}
setFunctionModule(modules, isFunction, 8, size - 8, true);
}
// Draws two copies of the version bits (with its own error correction code),
// based on this object's version field (which only has an effect for 7 <= version <= 40).
static void drawVersion(BitBucket *modules, BitBucket *isFunction, uint8_t version) {
int8_t size = modules->bitOffsetOrWidth;
#if LOCK_VERSION != 0 && LOCK_VERSION < 7
return;
#else
if (version < 7) { return; }
// Calculate error correction code and pack bits
uint32_t rem = version; // version is uint6, in the range [7, 40]
for (uint8_t i = 0; i < 12; i++) {
rem = (rem << 1) ^ ((rem >> 11) * 0x1F25);
}
uint32_t data = version << 12 | rem; // uint18
// Draw two copies
for (uint8_t i = 0; i < 18; i++) {
bool bit = ((data >> i) & 1) != 0;
uint8_t a = size - 11 + i % 3, b = i / 3;
setFunctionModule(modules, isFunction, a, b, bit);
setFunctionModule(modules, isFunction, b, a, bit);
}
#endif
}
static void drawFunctionPatterns(BitBucket *modules, BitBucket *isFunction, uint8_t version, uint8_t ecc) {
uint8_t size = modules->bitOffsetOrWidth;
// Draw the horizontal and vertical timing patterns
for (uint8_t i = 0; i < size; i++) {
setFunctionModule(modules, isFunction, 6, i, i % 2 == 0);
setFunctionModule(modules, isFunction, i, 6, i % 2 == 0);
}
// Draw 3 finder patterns (all corners except bottom right; overwrites some timing modules)
drawFinderPattern(modules, isFunction, 3, 3);
drawFinderPattern(modules, isFunction, size - 4, 3);
drawFinderPattern(modules, isFunction, 3, size - 4);
#if LOCK_VERSION == 0 || LOCK_VERSION > 1
if (version > 1) {
// Draw the numerous alignment patterns
uint8_t alignCount = version / 7 + 2;
uint8_t step;
if (version != 32) {
step = (version * 4 + alignCount * 2 + 1) / (2 * alignCount - 2) * 2; // ceil((size - 13) / (2*numAlign - 2)) * 2
} else { // C-C-C-Combo breaker!
step = 26;
}
uint8_t alignPositionIndex = alignCount - 1;
uint8_t alignPosition[alignCount];
alignPosition[0] = 6;
uint8_t size = version * 4 + 17;
for (uint8_t i = 0, pos = size - 7; i < alignCount - 1; i++, pos -= step) {
alignPosition[alignPositionIndex--] = pos;
}
for (uint8_t i = 0; i < alignCount; i++) {
for (uint8_t j = 0; j < alignCount; j++) {
if ((i == 0 && j == 0) || (i == 0 && j == alignCount - 1) || (i == alignCount - 1 && j == 0)) {
continue; // Skip the three finder corners
} else {
drawAlignmentPattern(modules, isFunction, alignPosition[i], alignPosition[j]);
}
}
}
}
#endif
// Draw configuration data
drawFormatBits(modules, isFunction, ecc, 0); // Dummy mask value; overwritten later in the constructor
drawVersion(modules, isFunction, version);
}
// Draws the given sequence of 8-bit codewords (data and error correction) onto the entire
// data area of this QR Code symbol. Function modules need to be marked off before this is called.
static void drawCodewords(BitBucket *modules, BitBucket *isFunction, BitBucket *codewords) {
uint32_t bitLength = codewords->bitOffsetOrWidth;
uint8_t *data = codewords->data;
uint8_t size = modules->bitOffsetOrWidth;
// Bit index into the data
uint32_t i = 0;
// Do the funny zigzag scan
for (int16_t right = size - 1; right >= 1; right -= 2) { // Index of right column in each column pair
if (right == 6) { right = 5; }
for (uint8_t vert = 0; vert < size; vert++) { // Vertical counter
for (int j = 0; j < 2; j++) {
uint8_t x = right - j; // Actual x coordinate
bool upwards = ((right & 2) == 0) ^ (x < 6);
uint8_t y = upwards ? size - 1 - vert : vert; // Actual y coordinate
if (!bb_getBit(isFunction, x, y) && i < bitLength) {
bb_setBit(modules, x, y, ((data[i >> 3] >> (7 - (i & 7))) & 1) != 0);
i++;
}
// If there are any remainder bits (0 to 7), they are already
// set to 0/false/white when the grid of modules was initialized
}
}
}
}
#pragma mark - Penalty Calculation
#define PENALTY_N1 3
#define PENALTY_N2 3
#define PENALTY_N3 40
#define PENALTY_N4 10
// Calculates and returns the penalty score based on state of this QR Code's current modules.
// This is used by the automatic mask choice algorithm to find the mask pattern that yields the lowest score.
// @TODO: This can be optimized by working with the bytes instead of bits.
static uint32_t getPenaltyScore(BitBucket *modules) {
uint32_t result = 0;
uint8_t size = modules->bitOffsetOrWidth;
// Adjacent modules in row having same color
for (uint8_t y = 0; y < size; y++) {
bool colorX = bb_getBit(modules, 0, y);
for (uint8_t x = 1, runX = 1; x < size; x++) {
bool cx = bb_getBit(modules, x, y);
if (cx != colorX) {
colorX = cx;
runX = 1;
} else {
runX++;
if (runX == 5) {
result += PENALTY_N1;
} else if (runX > 5) {
result++;
}
}
}
}
// Adjacent modules in column having same color
for (uint8_t x = 0; x < size; x++) {
bool colorY = bb_getBit(modules, x, 0);
for (uint8_t y = 1, runY = 1; y < size; y++) {
bool cy = bb_getBit(modules, x, y);
if (cy != colorY) {
colorY = cy;
runY = 1;
} else {
runY++;
if (runY == 5) {
result += PENALTY_N1;
} else if (runY > 5) {
result++;
}
}
}
}
uint16_t black = 0;
for (uint8_t y = 0; y < size; y++) {
uint16_t bitsRow = 0, bitsCol = 0;
for (uint8_t x = 0; x < size; x++) {
bool color = bb_getBit(modules, x, y);
// 2*2 blocks of modules having same color
if (x > 0 && y > 0) {
bool colorUL = bb_getBit(modules, x - 1, y - 1);
bool colorUR = bb_getBit(modules, x, y - 1);
bool colorL = bb_getBit(modules, x - 1, y);
if (color == colorUL && color == colorUR && color == colorL) {
result += PENALTY_N2;
}
}
// Finder-like pattern in rows and columns
bitsRow = ((bitsRow << 1) & 0x7FF) | color;
bitsCol = ((bitsCol << 1) & 0x7FF) | bb_getBit(modules, y, x);
// Needs 11 bits accumulated
if (x >= 10) {
if (bitsRow == 0x05D || bitsRow == 0x5D0) {
result += PENALTY_N3;
}
if (bitsCol == 0x05D || bitsCol == 0x5D0) {
result += PENALTY_N3;
}
}
// Balance of black and white modules
if (color) { black++; }
}
}
// Find smallest k such that (45-5k)% <= dark/total <= (55+5k)%
uint16_t total = size * size;
for (uint16_t k = 0; black * 20 < (9 - k) * total || black * 20 > (11 + k) * total; k++) {
result += PENALTY_N4;
}
return result;
}
#pragma mark - Reed-Solomon Generator
static uint8_t rs_multiply(uint8_t x, uint8_t y) {
// Russian peasant multiplication
// See: https://en.wikipedia.org/wiki/Ancient_Egyptian_multiplication
uint16_t z = 0;
for (int8_t i = 7; i >= 0; i--) {
z = (z << 1) ^ ((z >> 7) * 0x11D);
z ^= ((y >> i) & 1) * x;
}
return z;
}
static void rs_init(uint8_t degree, uint8_t *coeff) {
memset(coeff, 0, degree);
coeff[degree - 1] = 1;
// Compute the product polynomial (x - r^0) * (x - r^1) * (x - r^2) * ... * (x - r^{degree-1}),
// drop the highest term, and store the rest of the coefficients in order of descending powers.
// Note that r = 0x02, which is a generator element of this field GF(2^8/0x11D).
uint16_t root = 1;
for (uint8_t i = 0; i < degree; i++) {
// Multiply the current product by (x - r^i)
for (uint8_t j = 0; j < degree; j++) {
coeff[j] = rs_multiply(coeff[j], root);
if (j + 1 < degree) {
coeff[j] ^= coeff[j + 1];
}
}
root = (root << 1) ^ ((root >> 7) * 0x11D); // Multiply by 0x02 mod GF(2^8/0x11D)
}
}
static void rs_getRemainder(uint8_t degree, uint8_t *coeff, uint8_t *data, uint8_t length, uint8_t *result, uint8_t stride) {
// Compute the remainder by performing polynomial division
//for (uint8_t i = 0; i < degree; i++) { result[] = 0; }
//memset(result, 0, degree);
for (uint8_t i = 0; i < length; i++) {
uint8_t factor = data[i] ^ result[0];
for (uint8_t j = 1; j < degree; j++) {
result[(j - 1) * stride] = result[j * stride];
}
result[(degree - 1) * stride] = 0;
for (uint8_t j = 0; j < degree; j++) {
result[j * stride] ^= rs_multiply(coeff[j], factor);
}
}
}
#pragma mark - QrCode
static int8_t encodeDataCodewords(BitBucket *dataCodewords, const uint8_t *text, uint16_t length, uint8_t version) {
int8_t mode = MODE_BYTE;
if (isNumeric((char*)text, length)) {
mode = MODE_NUMERIC;
bb_appendBits(dataCodewords, 1 << MODE_NUMERIC, 4);
bb_appendBits(dataCodewords, length, getModeBits(version, MODE_NUMERIC));
uint16_t accumData = 0;
uint8_t accumCount = 0;
for (uint16_t i = 0; i < length; i++) {
accumData = accumData * 10 + ((char)(text[i]) - '0');
accumCount++;
if (accumCount == 3) {
bb_appendBits(dataCodewords, accumData, 10);
accumData = 0;
accumCount = 0;
}
}
// 1 or 2 digits remaining
if (accumCount > 0) {
bb_appendBits(dataCodewords, accumData, accumCount * 3 + 1);
}
} else if (isAlphanumeric((char*)text, length)) {
mode = MODE_ALPHANUMERIC;
bb_appendBits(dataCodewords, 1 << MODE_ALPHANUMERIC, 4);
bb_appendBits(dataCodewords, length, getModeBits(version, MODE_ALPHANUMERIC));
uint16_t accumData = 0;
uint8_t accumCount = 0;
for (uint16_t i = 0; i < length; i++) {
accumData = accumData * 45 + getAlphanumeric((char)(text[i]));
accumCount++;
if (accumCount == 2) {
bb_appendBits(dataCodewords, accumData, 11);
accumData = 0;
accumCount = 0;
}
}
// 1 character remaining
if (accumCount > 0) {
bb_appendBits(dataCodewords, accumData, 6);
}
} else {
bb_appendBits(dataCodewords, 1 << MODE_BYTE, 4);
bb_appendBits(dataCodewords, length, getModeBits(version, MODE_BYTE));
for (uint16_t i = 0; i < length; i++) {
bb_appendBits(dataCodewords, (char)(text[i]), 8);
}
}
//bb_setBits(dataCodewords, length, 4, getModeBits(version, mode));
return mode;
}
static void performErrorCorrection(uint8_t version, uint8_t ecc, BitBucket *data) {
// See: http://www.thonky.com/qr-code-tutorial/structure-final-message
#if LOCK_VERSION == 0
uint8_t numBlocks = NUM_ERROR_CORRECTION_BLOCKS[ecc][version - 1];
uint16_t totalEcc = NUM_ERROR_CORRECTION_CODEWORDS[ecc][version - 1];
uint16_t moduleCount = NUM_RAW_DATA_MODULES[version - 1];
#else
uint8_t numBlocks = NUM_ERROR_CORRECTION_BLOCKS[ecc];
uint16_t totalEcc = NUM_ERROR_CORRECTION_CODEWORDS[ecc];
uint16_t moduleCount = NUM_RAW_DATA_MODULES;
#endif
uint8_t blockEccLen = totalEcc / numBlocks;
uint8_t numShortBlocks = numBlocks - moduleCount / 8 % numBlocks;
uint8_t shortBlockLen = moduleCount / 8 / numBlocks;
uint8_t shortDataBlockLen = shortBlockLen - blockEccLen;
uint8_t result[data->capacityBytes];
memset(result, 0, sizeof(result));
uint8_t coeff[blockEccLen];
rs_init(blockEccLen, coeff);
uint16_t offset = 0;
uint8_t *dataBytes = data->data;
// Interleave all short blocks
for (uint8_t i = 0; i < shortDataBlockLen; i++) {
uint16_t index = i;
uint8_t stride = shortDataBlockLen;
for (uint8_t blockNum = 0; blockNum < numBlocks; blockNum++) {
result[offset++] = dataBytes[index];
#if LOCK_VERSION == 0 || LOCK_VERSION >= 5
if (blockNum == numShortBlocks) { stride++; }
#endif
index += stride;
}
}
// Version less than 5 only have short blocks
#if LOCK_VERSION == 0 || LOCK_VERSION >= 5
{
// Interleave long blocks
uint16_t index = shortDataBlockLen * (numShortBlocks + 1);
uint8_t stride = shortDataBlockLen;
for (uint8_t blockNum = 0; blockNum < numBlocks - numShortBlocks; blockNum++) {
result[offset++] = dataBytes[index];
if (blockNum == 0) { stride++; }
index += stride;
}
}
#endif
// Add all ecc blocks, interleaved
uint8_t blockSize = shortDataBlockLen;
for (uint8_t blockNum = 0; blockNum < numBlocks; blockNum++) {
#if LOCK_VERSION == 0 || LOCK_VERSION >= 5
if (blockNum == numShortBlocks) { blockSize++; }
#endif
rs_getRemainder(blockEccLen, coeff, dataBytes, blockSize, &result[offset + blockNum], numBlocks);
dataBytes += blockSize;
}
memcpy(data->data, result, data->capacityBytes);
data->bitOffsetOrWidth = moduleCount;
}
// We store the Format bits tightly packed into a single byte (each of the 4 modes is 2 bits)
// The format bits can be determined by ECC_FORMAT_BITS >> (2 * ecc)
static const uint8_t ECC_FORMAT_BITS = (0x02 << 6) | (0x03 << 4) | (0x00 << 2) | (0x01 << 0);
#pragma mark - Public QRCode functions
uint16_t qrcode_getBufferSize(uint8_t version) {
return bb_getGridSizeBytes(4 * version + 17);
}
// @TODO: Return error if data is too big.
int8_t qrcode_initBytes(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, uint8_t *data, uint16_t length) {
uint8_t size = version * 4 + 17;
qrcode->version = version;
qrcode->size = size;
qrcode->ecc = ecc;
qrcode->modules = modules;
uint8_t eccFormatBits = (ECC_FORMAT_BITS >> (2 * ecc)) & 0x03;
#if LOCK_VERSION == 0
uint16_t moduleCount = NUM_RAW_DATA_MODULES[version - 1];
uint16_t dataCapacity = moduleCount / 8 - NUM_ERROR_CORRECTION_CODEWORDS[eccFormatBits][version - 1];
#else
version = LOCK_VERSION;
uint16_t moduleCount = NUM_RAW_DATA_MODULES;
uint16_t dataCapacity = moduleCount / 8 - NUM_ERROR_CORRECTION_CODEWORDS[eccFormatBits];
#endif
struct BitBucket codewords;
uint8_t codewordBytes[bb_getBufferSizeBytes(moduleCount)];
bb_initBuffer(&codewords, codewordBytes, (int32_t)sizeof(codewordBytes));
// Place the data code words into the buffer
int8_t mode = encodeDataCodewords(&codewords, data, length, version);
if (mode < 0) { return -1; }
qrcode->mode = mode;
// Add terminator and pad up to a byte if applicable
uint32_t padding = (dataCapacity * 8) - codewords.bitOffsetOrWidth;
if (padding > 4) { padding = 4; }
bb_appendBits(&codewords, 0, padding);
bb_appendBits(&codewords, 0, (8 - codewords.bitOffsetOrWidth % 8) % 8);
// Pad with alternate bytes until data capacity is reached
for (uint8_t padByte = 0xEC; codewords.bitOffsetOrWidth < (dataCapacity * 8); padByte ^= 0xEC ^ 0x11) {
bb_appendBits(&codewords, padByte, 8);
}
BitBucket modulesGrid;
bb_initGrid(&modulesGrid, modules, size);
BitBucket isFunctionGrid;
uint8_t isFunctionGridBytes[bb_getGridSizeBytes(size)];
bb_initGrid(&isFunctionGrid, isFunctionGridBytes, size);
// Draw function patterns, draw all codewords, do masking
drawFunctionPatterns(&modulesGrid, &isFunctionGrid, version, eccFormatBits);
performErrorCorrection(version, eccFormatBits, &codewords);
drawCodewords(&modulesGrid, &isFunctionGrid, &codewords);
// Find the best (lowest penalty) mask
uint8_t mask = 0;
int32_t minPenalty = INT32_MAX;
for (uint8_t i = 0; i < 8; i++) {
drawFormatBits(&modulesGrid, &isFunctionGrid, eccFormatBits, i);
applyMask(&modulesGrid, &isFunctionGrid, i);
int penalty = getPenaltyScore(&modulesGrid);
if (penalty < minPenalty) {
mask = i;
minPenalty = penalty;
}
applyMask(&modulesGrid, &isFunctionGrid, i); // Undoes the mask due to XOR
}
qrcode->mask = mask;
// Overwrite old format bits
drawFormatBits(&modulesGrid, &isFunctionGrid, eccFormatBits, mask);
// Apply the final choice of mask
applyMask(&modulesGrid, &isFunctionGrid, mask);
return 0;
}
int8_t qrcode_initText(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, const char *data) {
return qrcode_initBytes(qrcode, modules, version, ecc, (uint8_t*)data, strlen(data));
}
bool qrcode_getModule(QRCode *qrcode, uint8_t x, uint8_t y) {
if (x < 0 || x >= qrcode->size || y < 0 || y >= qrcode->size) {
return false;
}
uint32_t offset = y * qrcode->size + x;
return (qrcode->modules[offset >> 3] & (1 << (7 - (offset & 0x07)))) != 0;
}
/*
uint8_t qrcode_getHexLength(QRCode *qrcode) {
return ((qrcode->size * qrcode->size) + 7) / 4;
}
void qrcode_getHex(QRCode *qrcode, char *result) {
}
*/

View File

@ -1,99 +0,0 @@
/**
* The MIT License (MIT)
*
* This library is written and maintained by Richard Moore.
* Major parts were derived from Project Nayuki's library.
*
* Copyright (c) 2017 Richard Moore (https://github.com/ricmoo/QRCode)
* Copyright (c) 2017 Project Nayuki (https://www.nayuki.io/page/qr-code-generator-library)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Special thanks to Nayuki (https://www.nayuki.io/) from which this library was
* heavily inspired and compared against.
*
* See: https://github.com/nayuki/QR-Code-generator/tree/master/cpp
*/
#ifndef __QRCODE_H_
#define __QRCODE_H_
#ifndef __cplusplus
typedef unsigned char bool;
static const bool false = 0;
static const bool true = 1;
#endif
#include <stdint.h>
// QR Code Format Encoding
#define MODE_NUMERIC 0
#define MODE_ALPHANUMERIC 1
#define MODE_BYTE 2
// Error Correction Code Levels
#define ECC_LOW 0
#define ECC_MEDIUM 1
#define ECC_QUARTILE 2
#define ECC_HIGH 3
// If set to non-zero, this library can ONLY produce QR codes at that version
// This saves a lot of dynamic memory, as the codeword tables are skipped
#ifndef LOCK_VERSION
#define LOCK_VERSION 0
#endif
typedef struct QRCode {
uint8_t version;
uint8_t size;
uint8_t ecc;
uint8_t mode;
uint8_t mask;
uint8_t *modules;
} QRCode;
#ifdef __cplusplus
extern "C"{
#endif /* __cplusplus */
uint16_t qrcode_getBufferSize(uint8_t version);
int8_t qrcode_initText(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, const char *data);
int8_t qrcode_initBytes(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, uint8_t *data, uint16_t length);
bool qrcode_getModule(QRCode *qrcode, uint8_t x, uint8_t y);
#ifdef __cplusplus
}
#endif /* __cplusplus */
#endif /* __QRCODE_H_ */

View File

@ -0,0 +1,5 @@
tests/bin
.pioenvs
.piolibdeps
.clang_complete
.gcc-flags.json

View File

@ -0,0 +1,7 @@
sudo: false
language: cpp
compiler:
- g++
script: cd tests && make && make test
os:
- linux

View File

@ -0,0 +1,85 @@
2.8
* Add setBufferSize() to override MQTT_MAX_PACKET_SIZE
* Add setKeepAlive() to override MQTT_KEEPALIVE
* Add setSocketTimeout() to overide MQTT_SOCKET_TIMEOUT
* Added check to prevent subscribe/unsubscribe to empty topics
* Declare wifi mode prior to connect in ESP example
* Use `strnlen` to avoid overruns
* Support pre-connected Client objects
2.7
* Fix remaining-length handling to prevent buffer overrun
* Add large-payload API - beginPublish/write/publish/endPublish
* Add yield call to improve reliability on ESP
* Add Clean Session flag to connect options
* Add ESP32 support for functional callback signature
* Various other fixes
2.4
* Add MQTT_SOCKET_TIMEOUT to prevent it blocking indefinitely
whilst waiting for inbound data
* Fixed return code when publishing >256 bytes
2.3
* Add publish(topic,payload,retained) function
2.2
* Change code layout to match Arduino Library reqs
2.1
* Add MAX_TRANSFER_SIZE def to chunk messages if needed
* Reject topic/payloads that exceed MQTT_MAX_PACKET_SIZE
2.0
* Add (and default to) MQTT 3.1.1 support
* Fix PROGMEM handling for Intel Galileo/ESP8266
* Add overloaded constructors for convenience
* Add chainable setters for server/callback/client/stream
* Add state function to return connack return code
1.9
* Do not split MQTT packets over multiple calls to _client->write()
* API change: All constructors now require an instance of Client
to be passed in.
* Fixed example to match 1.8 api changes - dpslwk
* Added username/password support - WilHall
* Added publish_P - publishes messages from PROGMEM - jobytaffey
1.8
* KeepAlive interval is configurable in PubSubClient.h
* Maximum packet size is configurable in PubSubClient.h
* API change: Return boolean rather than int from various functions
* API change: Length parameter in message callback changed
from int to unsigned int
* Various internal tidy-ups around types
1.7
* Improved keepalive handling
* Updated to the Arduino-1.0 API
1.6
* Added the ability to publish a retained message
1.5
* Added default constructor
* Fixed compile error when used with arduino-0021 or later
1.4
* Fixed connection lost handling
1.3
* Fixed packet reading bug in PubSubClient.readPacket
1.2
* Fixed compile error when used with arduino-0016 or later
1.1
* Reduced size of library
* Added support for Will messages
* Clarified licensing - see LICENSE.txt
1.0
* Only Quality of Service (QOS) 0 messaging is supported
* The maximum message size, including header, is 128 bytes
* The keepalive interval is set to 30 seconds
* No support for Will messages

View File

@ -0,0 +1,20 @@
Copyright (c) 2008-2020 Nicholas O'Leary
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,50 @@
# Arduino Client for MQTT
This library provides a client for doing simple publish/subscribe messaging with
a server that supports MQTT.
## Examples
The library comes with a number of example sketches. See File > Examples > PubSubClient
within the Arduino application.
Full API documentation is available here: https://pubsubclient.knolleary.net
## Limitations
- It can only publish QoS 0 messages. It can subscribe at QoS 0 or QoS 1.
- The maximum message size, including header, is **256 bytes** by default. This
is configurable via `MQTT_MAX_PACKET_SIZE` in `PubSubClient.h` or can be changed
by calling `PubSubClient::setBufferSize(size)`.
- The keepalive interval is set to 15 seconds by default. This is configurable
via `MQTT_KEEPALIVE` in `PubSubClient.h` or can be changed by calling
`PubSubClient::setKeepAlive(keepAlive)`.
- The client uses MQTT 3.1.1 by default. It can be changed to use MQTT 3.1 by
changing value of `MQTT_VERSION` in `PubSubClient.h`.
## Compatible Hardware
The library uses the Arduino Ethernet Client api for interacting with the
underlying network hardware. This means it Just Works with a growing number of
boards and shields, including:
- Arduino Ethernet
- Arduino Ethernet Shield
- Arduino YUN use the included `YunClient` in place of `EthernetClient`, and
be sure to do a `Bridge.begin()` first
- Arduino WiFi Shield - if you want to send packets > 90 bytes with this shield,
enable the `MQTT_MAX_TRANSFER_SIZE` define in `PubSubClient.h`.
- Sparkfun WiFly Shield [library](https://github.com/dpslwk/WiFly)
- TI CC3000 WiFi - [library](https://github.com/sparkfun/SFE_CC3000_Library)
- Intel Galileo/Edison
- ESP8266
- ESP32
The library cannot currently be used with hardware based on the ENC28J60 chip
such as the Nanode or the Nuelectronics Ethernet Shield. For those, there is an
[alternative library](https://github.com/njh/NanodeMQTT) available.
## License
This code is released under the MIT License.

View File

@ -0,0 +1,43 @@
/*
Basic MQTT example with Authentication
- connects to an MQTT server, providing username
and password
- publishes "hello world" to the topic "outTopic"
- subscribes to the topic "inTopic"
*/
#include <SPI.h>
#include <Ethernet.h>
#include <PubSubClient.h>
// Update these with values suitable for your network.
byte mac[] = { 0xDE, 0xED, 0xBA, 0xFE, 0xFE, 0xED };
IPAddress ip(172, 16, 0, 100);
IPAddress server(172, 16, 0, 2);
void callback(char* topic, byte* payload, unsigned int length) {
// handle message arrived
}
EthernetClient ethClient;
PubSubClient client(server, 1883, callback, ethClient);
void setup()
{
Ethernet.begin(mac, ip);
// Note - the default maximum packet size is 128 bytes. If the
// combined length of clientId, username and password exceed this use the
// following to increase the buffer size:
// client.setBufferSize(255);
if (client.connect("arduinoClient", "testuser", "testpass")) {
client.publish("outTopic","hello world");
client.subscribe("inTopic");
}
}
void loop()
{
client.loop();
}

View File

@ -0,0 +1,77 @@
/*
Basic MQTT example
This sketch demonstrates the basic capabilities of the library.
It connects to an MQTT server then:
- publishes "hello world" to the topic "outTopic"
- subscribes to the topic "inTopic", printing out any messages
it receives. NB - it assumes the received payloads are strings not binary
It will reconnect to the server if the connection is lost using a blocking
reconnect function. See the 'mqtt_reconnect_nonblocking' example for how to
achieve the same result without blocking the main loop.
*/
#include <SPI.h>
#include <Ethernet.h>
#include <PubSubClient.h>
// Update these with values suitable for your network.
byte mac[] = { 0xDE, 0xED, 0xBA, 0xFE, 0xFE, 0xED };
IPAddress ip(172, 16, 0, 100);
IPAddress server(172, 16, 0, 2);
void callback(char* topic, byte* payload, unsigned int length) {
Serial.print("Message arrived [");
Serial.print(topic);
Serial.print("] ");
for (int i=0;i<length;i++) {
Serial.print((char)payload[i]);
}
Serial.println();
}
EthernetClient ethClient;
PubSubClient client(ethClient);
void reconnect() {
// Loop until we're reconnected
while (!client.connected()) {
Serial.print("Attempting MQTT connection...");
// Attempt to connect
if (client.connect("arduinoClient")) {
Serial.println("connected");
// Once connected, publish an announcement...
client.publish("outTopic","hello world");
// ... and resubscribe
client.subscribe("inTopic");
} else {
Serial.print("failed, rc=");
Serial.print(client.state());
Serial.println(" try again in 5 seconds");
// Wait 5 seconds before retrying
delay(5000);
}
}
}
void setup()
{
Serial.begin(57600);
client.setServer(server, 1883);
client.setCallback(callback);
Ethernet.begin(mac, ip);
// Allow the hardware to sort itself out
delay(1500);
}
void loop()
{
if (!client.connected()) {
reconnect();
}
client.loop();
}

View File

@ -0,0 +1,129 @@
/*
Basic ESP8266 MQTT example
This sketch demonstrates the capabilities of the pubsub library in combination
with the ESP8266 board/library.
It connects to an MQTT server then:
- publishes "hello world" to the topic "outTopic" every two seconds
- subscribes to the topic "inTopic", printing out any messages
it receives. NB - it assumes the received payloads are strings not binary
- If the first character of the topic "inTopic" is an 1, switch ON the ESP Led,
else switch it off
It will reconnect to the server if the connection is lost using a blocking
reconnect function. See the 'mqtt_reconnect_nonblocking' example for how to
achieve the same result without blocking the main loop.
To install the ESP8266 board, (using Arduino 1.6.4+):
- Add the following 3rd party board manager under "File -> Preferences -> Additional Boards Manager URLs":
http://arduino.esp8266.com/stable/package_esp8266com_index.json
- Open the "Tools -> Board -> Board Manager" and click install for the ESP8266"
- Select your ESP8266 in "Tools -> Board"
*/
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
// Update these with values suitable for your network.
const char* ssid = "........";
const char* password = "........";
const char* mqtt_server = "broker.mqtt-dashboard.com";
WiFiClient espClient;
PubSubClient client(espClient);
unsigned long lastMsg = 0;
#define MSG_BUFFER_SIZE (50)
char msg[MSG_BUFFER_SIZE];
int value = 0;
void setup_wifi() {
delay(10);
// We start by connecting to a WiFi network
Serial.println();
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
randomSeed(micros());
Serial.println("");
Serial.println("WiFi connected");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
}
void callback(char* topic, byte* payload, unsigned int length) {
Serial.print("Message arrived [");
Serial.print(topic);
Serial.print("] ");
for (int i = 0; i < length; i++) {
Serial.print((char)payload[i]);
}
Serial.println();
// Switch on the LED if an 1 was received as first character
if ((char)payload[0] == '1') {
digitalWrite(BUILTIN_LED, LOW); // Turn the LED on (Note that LOW is the voltage level
// but actually the LED is on; this is because
// it is active low on the ESP-01)
} else {
digitalWrite(BUILTIN_LED, HIGH); // Turn the LED off by making the voltage HIGH
}
}
void reconnect() {
// Loop until we're reconnected
while (!client.connected()) {
Serial.print("Attempting MQTT connection...");
// Create a random client ID
String clientId = "ESP8266Client-";
clientId += String(random(0xffff), HEX);
// Attempt to connect
if (client.connect(clientId.c_str())) {
Serial.println("connected");
// Once connected, publish an announcement...
client.publish("outTopic", "hello world");
// ... and resubscribe
client.subscribe("inTopic");
} else {
Serial.print("failed, rc=");
Serial.print(client.state());
Serial.println(" try again in 5 seconds");
// Wait 5 seconds before retrying
delay(5000);
}
}
}
void setup() {
pinMode(BUILTIN_LED, OUTPUT); // Initialize the BUILTIN_LED pin as an output
Serial.begin(115200);
setup_wifi();
client.setServer(mqtt_server, 1883);
client.setCallback(callback);
}
void loop() {
if (!client.connected()) {
reconnect();
}
client.loop();
unsigned long now = millis();
if (now - lastMsg > 2000) {
lastMsg = now;
++value;
snprintf (msg, MSG_BUFFER_SIZE, "hello world #%ld", value);
Serial.print("Publish message: ");
Serial.println(msg);
client.publish("outTopic", msg);
}
}

View File

@ -0,0 +1,179 @@
/*
Long message ESP8266 MQTT example
This sketch demonstrates sending arbitrarily large messages in combination
with the ESP8266 board/library.
It connects to an MQTT server then:
- publishes "hello world" to the topic "outTopic"
- subscribes to the topic "greenBottles/#", printing out any messages
it receives. NB - it assumes the received payloads are strings not binary
- If the sub-topic is a number, it publishes a "greenBottles/lyrics" message
with a payload consisting of the lyrics to "10 green bottles", replacing
10 with the number given in the sub-topic.
It will reconnect to the server if the connection is lost using a blocking
reconnect function. See the 'mqtt_reconnect_nonblocking' example for how to
achieve the same result without blocking the main loop.
To install the ESP8266 board, (using Arduino 1.6.4+):
- Add the following 3rd party board manager under "File -> Preferences -> Additional Boards Manager URLs":
http://arduino.esp8266.com/stable/package_esp8266com_index.json
- Open the "Tools -> Board -> Board Manager" and click install for the ESP8266"
- Select your ESP8266 in "Tools -> Board"
*/
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
// Update these with values suitable for your network.
const char* ssid = "........";
const char* password = "........";
const char* mqtt_server = "broker.mqtt-dashboard.com";
WiFiClient espClient;
PubSubClient client(espClient);
long lastMsg = 0;
char msg[50];
int value = 0;
void setup_wifi() {
delay(10);
// We start by connecting to a WiFi network
Serial.println();
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
randomSeed(micros());
Serial.println("");
Serial.println("WiFi connected");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
}
void callback(char* topic, byte* payload, unsigned int length) {
Serial.print("Message arrived [");
Serial.print(topic);
Serial.print("] ");
for (int i = 0; i < length; i++) {
Serial.print((char)payload[i]);
}
Serial.println();
// Find out how many bottles we should generate lyrics for
String topicStr(topic);
int bottleCount = 0; // assume no bottles unless we correctly parse a value from the topic
if (topicStr.indexOf('/') >= 0) {
// The topic includes a '/', we'll try to read the number of bottles from just after that
topicStr.remove(0, topicStr.indexOf('/')+1);
// Now see if there's a number of bottles after the '/'
bottleCount = topicStr.toInt();
}
if (bottleCount > 0) {
// Work out how big our resulting message will be
int msgLen = 0;
for (int i = bottleCount; i > 0; i--) {
String numBottles(i);
msgLen += 2*numBottles.length();
if (i == 1) {
msgLen += 2*String(" green bottle, standing on the wall\n").length();
} else {
msgLen += 2*String(" green bottles, standing on the wall\n").length();
}
msgLen += String("And if one green bottle should accidentally fall\nThere'll be ").length();
switch (i) {
case 1:
msgLen += String("no green bottles, standing on the wall\n\n").length();
break;
case 2:
msgLen += String("1 green bottle, standing on the wall\n\n").length();
break;
default:
numBottles = i-1;
msgLen += numBottles.length();
msgLen += String(" green bottles, standing on the wall\n\n").length();
break;
};
}
// Now we can start to publish the message
client.beginPublish("greenBottles/lyrics", msgLen, false);
for (int i = bottleCount; i > 0; i--) {
for (int j = 0; j < 2; j++) {
client.print(i);
if (i == 1) {
client.print(" green bottle, standing on the wall\n");
} else {
client.print(" green bottles, standing on the wall\n");
}
}
client.print("And if one green bottle should accidentally fall\nThere'll be ");
switch (i) {
case 1:
client.print("no green bottles, standing on the wall\n\n");
break;
case 2:
client.print("1 green bottle, standing on the wall\n\n");
break;
default:
client.print(i-1);
client.print(" green bottles, standing on the wall\n\n");
break;
};
}
// Now we're done!
client.endPublish();
}
}
void reconnect() {
// Loop until we're reconnected
while (!client.connected()) {
Serial.print("Attempting MQTT connection...");
// Create a random client ID
String clientId = "ESP8266Client-";
clientId += String(random(0xffff), HEX);
// Attempt to connect
if (client.connect(clientId.c_str())) {
Serial.println("connected");
// Once connected, publish an announcement...
client.publish("outTopic", "hello world");
// ... and resubscribe
client.subscribe("greenBottles/#");
} else {
Serial.print("failed, rc=");
Serial.print(client.state());
Serial.println(" try again in 5 seconds");
// Wait 5 seconds before retrying
delay(5000);
}
}
}
void setup() {
pinMode(BUILTIN_LED, OUTPUT); // Initialize the BUILTIN_LED pin as an output
Serial.begin(115200);
setup_wifi();
client.setServer(mqtt_server, 1883);
client.setCallback(callback);
}
void loop() {
if (!client.connected()) {
reconnect();
}
client.loop();
}

View File

@ -0,0 +1,60 @@
/*
Publishing in the callback
- connects to an MQTT server
- subscribes to the topic "inTopic"
- when a message is received, republishes it to "outTopic"
This example shows how to publish messages within the
callback function. The callback function header needs to
be declared before the PubSubClient constructor and the
actual callback defined afterwards.
This ensures the client reference in the callback function
is valid.
*/
#include <SPI.h>
#include <Ethernet.h>
#include <PubSubClient.h>
// Update these with values suitable for your network.
byte mac[] = { 0xDE, 0xED, 0xBA, 0xFE, 0xFE, 0xED };
IPAddress ip(172, 16, 0, 100);
IPAddress server(172, 16, 0, 2);
// Callback function header
void callback(char* topic, byte* payload, unsigned int length);
EthernetClient ethClient;
PubSubClient client(server, 1883, callback, ethClient);
// Callback function
void callback(char* topic, byte* payload, unsigned int length) {
// In order to republish this payload, a copy must be made
// as the orignal payload buffer will be overwritten whilst
// constructing the PUBLISH packet.
// Allocate the correct amount of memory for the payload copy
byte* p = (byte*)malloc(length);
// Copy the payload to the new buffer
memcpy(p,payload,length);
client.publish("outTopic", p, length);
// Free the memory
free(p);
}
void setup()
{
Ethernet.begin(mac, ip);
if (client.connect("arduinoClient")) {
client.publish("outTopic","hello world");
client.subscribe("inTopic");
}
}
void loop()
{
client.loop();
}

View File

@ -0,0 +1,67 @@
/*
Reconnecting MQTT example - non-blocking
This sketch demonstrates how to keep the client connected
using a non-blocking reconnect function. If the client loses
its connection, it attempts to reconnect every 5 seconds
without blocking the main loop.
*/
#include <SPI.h>
#include <Ethernet.h>
#include <PubSubClient.h>
// Update these with values suitable for your hardware/network.
byte mac[] = { 0xDE, 0xED, 0xBA, 0xFE, 0xFE, 0xED };
IPAddress ip(172, 16, 0, 100);
IPAddress server(172, 16, 0, 2);
void callback(char* topic, byte* payload, unsigned int length) {
// handle message arrived
}
EthernetClient ethClient;
PubSubClient client(ethClient);
long lastReconnectAttempt = 0;
boolean reconnect() {
if (client.connect("arduinoClient")) {
// Once connected, publish an announcement...
client.publish("outTopic","hello world");
// ... and resubscribe
client.subscribe("inTopic");
}
return client.connected();
}
void setup()
{
client.setServer(server, 1883);
client.setCallback(callback);
Ethernet.begin(mac, ip);
delay(1500);
lastReconnectAttempt = 0;
}
void loop()
{
if (!client.connected()) {
long now = millis();
if (now - lastReconnectAttempt > 5000) {
lastReconnectAttempt = now;
// Attempt to reconnect
if (reconnect()) {
lastReconnectAttempt = 0;
}
}
} else {
// Client connected
client.loop();
}
}

View File

@ -0,0 +1,57 @@
/*
Example of using a Stream object to store the message payload
Uses SRAM library: https://github.com/ennui2342/arduino-sram
but could use any Stream based class such as SD
- connects to an MQTT server
- publishes "hello world" to the topic "outTopic"
- subscribes to the topic "inTopic"
*/
#include <SPI.h>
#include <Ethernet.h>
#include <PubSubClient.h>
#include <SRAM.h>
// Update these with values suitable for your network.
byte mac[] = { 0xDE, 0xED, 0xBA, 0xFE, 0xFE, 0xED };
IPAddress ip(172, 16, 0, 100);
IPAddress server(172, 16, 0, 2);
SRAM sram(4, SRAM_1024);
void callback(char* topic, byte* payload, unsigned int length) {
sram.seek(1);
// do something with the message
for(uint8_t i=0; i<length; i++) {
Serial.write(sram.read());
}
Serial.println();
// Reset position for the next message to be stored
sram.seek(1);
}
EthernetClient ethClient;
PubSubClient client(server, 1883, callback, ethClient, sram);
void setup()
{
Ethernet.begin(mac, ip);
if (client.connect("arduinoClient")) {
client.publish("outTopic","hello world");
client.subscribe("inTopic");
}
sram.begin();
sram.seek(1);
Serial.begin(9600);
}
void loop()
{
client.loop();
}

View File

@ -0,0 +1,36 @@
#######################################
# Syntax Coloring Map For PubSubClient
#######################################
#######################################
# Datatypes (KEYWORD1)
#######################################
PubSubClient KEYWORD1
#######################################
# Methods and Functions (KEYWORD2)
#######################################
connect KEYWORD2
disconnect KEYWORD2
publish KEYWORD2
publish_P KEYWORD2
beginPublish KEYWORD2
endPublish KEYWORD2
write KEYWORD2
subscribe KEYWORD2
unsubscribe KEYWORD2
loop KEYWORD2
connected KEYWORD2
setServer KEYWORD2
setCallback KEYWORD2
setClient KEYWORD2
setStream KEYWORD2
setKeepAlive KEYWORD2
setBufferSize KEYWORD2
setSocketTimeout KEYWORD2
#######################################
# Constants (LITERAL1)
#######################################

View File

@ -0,0 +1,18 @@
{
"name": "PubSubClient",
"keywords": "ethernet, mqtt, m2m, iot",
"description": "A client library for MQTT messaging. MQTT is a lightweight messaging protocol ideal for small devices. This library allows you to send and receive MQTT messages. It supports the latest MQTT 3.1.1 protocol and can be configured to use the older MQTT 3.1 if needed. It supports all Arduino Ethernet Client compatible hardware, including the Intel Galileo/Edison, ESP8266 and TI CC3000.",
"repository": {
"type": "git",
"url": "https://github.com/knolleary/pubsubclient.git"
},
"version": "2.8",
"exclude": "tests",
"examples": "examples/*/*.ino",
"frameworks": "arduino",
"platforms": [
"atmelavr",
"espressif8266",
"espressif32"
]
}

View File

@ -0,0 +1,9 @@
name=PubSubClient
version=2.8
author=Nick O'Leary <nick.oleary@gmail.com>
maintainer=Nick O'Leary <nick.oleary@gmail.com>
sentence=A client library for MQTT messaging.
paragraph=MQTT is a lightweight messaging protocol ideal for small devices. This library allows you to send and receive MQTT messages. It supports the latest MQTT 3.1.1 protocol and can be configured to use the older MQTT 3.1 if needed. It supports all Arduino Ethernet Client compatible hardware, including the Intel Galileo/Edison, ESP8266 and TI CC3000.
category=Communication
url=http://pubsubclient.knolleary.net
architectures=*

View File

@ -0,0 +1,769 @@
/*
PubSubClient.cpp - A simple client for MQTT.
Nick O'Leary
http://knolleary.net
*/
#include "PubSubClient.h"
#include "Arduino.h"
PubSubClient::PubSubClient() {
this->_state = MQTT_DISCONNECTED;
this->_client = NULL;
this->stream = NULL;
setCallback(NULL);
this->bufferSize = 0;
setBufferSize(MQTT_MAX_PACKET_SIZE);
setKeepAlive(MQTT_KEEPALIVE);
setSocketTimeout(MQTT_SOCKET_TIMEOUT);
}
PubSubClient::PubSubClient(Client& client) {
this->_state = MQTT_DISCONNECTED;
setClient(client);
this->stream = NULL;
this->bufferSize = 0;
setBufferSize(MQTT_MAX_PACKET_SIZE);
setKeepAlive(MQTT_KEEPALIVE);
setSocketTimeout(MQTT_SOCKET_TIMEOUT);
}
PubSubClient::PubSubClient(IPAddress addr, uint16_t port, Client& client) {
this->_state = MQTT_DISCONNECTED;
setServer(addr, port);
setClient(client);
this->stream = NULL;
this->bufferSize = 0;
setBufferSize(MQTT_MAX_PACKET_SIZE);
setKeepAlive(MQTT_KEEPALIVE);
setSocketTimeout(MQTT_SOCKET_TIMEOUT);
}
PubSubClient::PubSubClient(IPAddress addr, uint16_t port, Client& client, Stream& stream) {
this->_state = MQTT_DISCONNECTED;
setServer(addr,port);
setClient(client);
setStream(stream);
this->bufferSize = 0;
setBufferSize(MQTT_MAX_PACKET_SIZE);
setKeepAlive(MQTT_KEEPALIVE);
setSocketTimeout(MQTT_SOCKET_TIMEOUT);
}
PubSubClient::PubSubClient(IPAddress addr, uint16_t port, MQTT_CALLBACK_SIGNATURE, Client& client) {
this->_state = MQTT_DISCONNECTED;
setServer(addr, port);
setCallback(callback);
setClient(client);
this->stream = NULL;
this->bufferSize = 0;
setBufferSize(MQTT_MAX_PACKET_SIZE);
setKeepAlive(MQTT_KEEPALIVE);
setSocketTimeout(MQTT_SOCKET_TIMEOUT);
}
PubSubClient::PubSubClient(IPAddress addr, uint16_t port, MQTT_CALLBACK_SIGNATURE, Client& client, Stream& stream) {
this->_state = MQTT_DISCONNECTED;
setServer(addr,port);
setCallback(callback);
setClient(client);
setStream(stream);
this->bufferSize = 0;
setBufferSize(MQTT_MAX_PACKET_SIZE);
setKeepAlive(MQTT_KEEPALIVE);
setSocketTimeout(MQTT_SOCKET_TIMEOUT);
}
PubSubClient::PubSubClient(uint8_t *ip, uint16_t port, Client& client) {
this->_state = MQTT_DISCONNECTED;
setServer(ip, port);
setClient(client);
this->stream = NULL;
this->bufferSize = 0;
setBufferSize(MQTT_MAX_PACKET_SIZE);
setKeepAlive(MQTT_KEEPALIVE);
setSocketTimeout(MQTT_SOCKET_TIMEOUT);
}
PubSubClient::PubSubClient(uint8_t *ip, uint16_t port, Client& client, Stream& stream) {
this->_state = MQTT_DISCONNECTED;
setServer(ip,port);
setClient(client);
setStream(stream);
this->bufferSize = 0;
setBufferSize(MQTT_MAX_PACKET_SIZE);
setKeepAlive(MQTT_KEEPALIVE);
setSocketTimeout(MQTT_SOCKET_TIMEOUT);
}
PubSubClient::PubSubClient(uint8_t *ip, uint16_t port, MQTT_CALLBACK_SIGNATURE, Client& client) {
this->_state = MQTT_DISCONNECTED;
setServer(ip, port);
setCallback(callback);
setClient(client);
this->stream = NULL;
this->bufferSize = 0;
setBufferSize(MQTT_MAX_PACKET_SIZE);
setKeepAlive(MQTT_KEEPALIVE);
setSocketTimeout(MQTT_SOCKET_TIMEOUT);
}
PubSubClient::PubSubClient(uint8_t *ip, uint16_t port, MQTT_CALLBACK_SIGNATURE, Client& client, Stream& stream) {
this->_state = MQTT_DISCONNECTED;
setServer(ip,port);
setCallback(callback);
setClient(client);
setStream(stream);
this->bufferSize = 0;
setBufferSize(MQTT_MAX_PACKET_SIZE);
setKeepAlive(MQTT_KEEPALIVE);
setSocketTimeout(MQTT_SOCKET_TIMEOUT);
}
PubSubClient::PubSubClient(const char* domain, uint16_t port, Client& client) {
this->_state = MQTT_DISCONNECTED;
setServer(domain,port);
setClient(client);
this->stream = NULL;
this->bufferSize = 0;
setBufferSize(MQTT_MAX_PACKET_SIZE);
setKeepAlive(MQTT_KEEPALIVE);
setSocketTimeout(MQTT_SOCKET_TIMEOUT);
}
PubSubClient::PubSubClient(const char* domain, uint16_t port, Client& client, Stream& stream) {
this->_state = MQTT_DISCONNECTED;
setServer(domain,port);
setClient(client);
setStream(stream);
this->bufferSize = 0;
setBufferSize(MQTT_MAX_PACKET_SIZE);
setKeepAlive(MQTT_KEEPALIVE);
setSocketTimeout(MQTT_SOCKET_TIMEOUT);
}
PubSubClient::PubSubClient(const char* domain, uint16_t port, MQTT_CALLBACK_SIGNATURE, Client& client) {
this->_state = MQTT_DISCONNECTED;
setServer(domain,port);
setCallback(callback);
setClient(client);
this->stream = NULL;
this->bufferSize = 0;
setBufferSize(MQTT_MAX_PACKET_SIZE);
setKeepAlive(MQTT_KEEPALIVE);
setSocketTimeout(MQTT_SOCKET_TIMEOUT);
}
PubSubClient::PubSubClient(const char* domain, uint16_t port, MQTT_CALLBACK_SIGNATURE, Client& client, Stream& stream) {
this->_state = MQTT_DISCONNECTED;
setServer(domain,port);
setCallback(callback);
setClient(client);
setStream(stream);
this->bufferSize = 0;
setBufferSize(MQTT_MAX_PACKET_SIZE);
setKeepAlive(MQTT_KEEPALIVE);
setSocketTimeout(MQTT_SOCKET_TIMEOUT);
}
PubSubClient::~PubSubClient() {
free(this->buffer);
}
boolean PubSubClient::connect(const char *id) {
return connect(id,NULL,NULL,0,0,0,0,1);
}
boolean PubSubClient::connect(const char *id, const char *user, const char *pass) {
return connect(id,user,pass,0,0,0,0,1);
}
boolean PubSubClient::connect(const char *id, const char* willTopic, uint8_t willQos, boolean willRetain, const char* willMessage) {
return connect(id,NULL,NULL,willTopic,willQos,willRetain,willMessage,1);
}
boolean PubSubClient::connect(const char *id, const char *user, const char *pass, const char* willTopic, uint8_t willQos, boolean willRetain, const char* willMessage) {
return connect(id,user,pass,willTopic,willQos,willRetain,willMessage,1);
}
boolean PubSubClient::connect(const char *id, const char *user, const char *pass, const char* willTopic, uint8_t willQos, boolean willRetain, const char* willMessage, boolean cleanSession) {
if (!connected()) {
int result = 0;
if(_client->connected()) {
result = 1;
} else {
if (domain != NULL) {
result = _client->connect(this->domain, this->port);
} else {
result = _client->connect(this->ip, this->port);
}
}
if (result == 1) {
nextMsgId = 1;
// Leave room in the buffer for header and variable length field
uint16_t length = MQTT_MAX_HEADER_SIZE;
unsigned int j;
#if MQTT_VERSION == MQTT_VERSION_3_1
uint8_t d[9] = {0x00,0x06,'M','Q','I','s','d','p', MQTT_VERSION};
#define MQTT_HEADER_VERSION_LENGTH 9
#elif MQTT_VERSION == MQTT_VERSION_3_1_1
uint8_t d[7] = {0x00,0x04,'M','Q','T','T',MQTT_VERSION};
#define MQTT_HEADER_VERSION_LENGTH 7
#endif
for (j = 0;j<MQTT_HEADER_VERSION_LENGTH;j++) {
this->buffer[length++] = d[j];
}
uint8_t v;
if (willTopic) {
v = 0x04|(willQos<<3)|(willRetain<<5);
} else {
v = 0x00;
}
if (cleanSession) {
v = v|0x02;
}
if(user != NULL) {
v = v|0x80;
if(pass != NULL) {
v = v|(0x80>>1);
}
}
this->buffer[length++] = v;
this->buffer[length++] = ((this->keepAlive) >> 8);
this->buffer[length++] = ((this->keepAlive) & 0xFF);
CHECK_STRING_LENGTH(length,id)
length = writeString(id,this->buffer,length);
if (willTopic) {
CHECK_STRING_LENGTH(length,willTopic)
length = writeString(willTopic,this->buffer,length);
CHECK_STRING_LENGTH(length,willMessage)
length = writeString(willMessage,this->buffer,length);
}
if(user != NULL) {
CHECK_STRING_LENGTH(length,user)
length = writeString(user,this->buffer,length);
if(pass != NULL) {
CHECK_STRING_LENGTH(length,pass)
length = writeString(pass,this->buffer,length);
}
}
write(MQTTCONNECT,this->buffer,length-MQTT_MAX_HEADER_SIZE);
lastInActivity = lastOutActivity = millis();
while (!_client->available()) {
unsigned long t = millis();
if (t-lastInActivity >= ((int32_t) this->socketTimeout*1000UL)) {
_state = MQTT_CONNECTION_TIMEOUT;
_client->stop();
return false;
}
}
uint8_t llen;
uint32_t len = readPacket(&llen);
if (len == 4) {
if (buffer[3] == 0) {
lastInActivity = millis();
pingOutstanding = false;
_state = MQTT_CONNECTED;
return true;
} else {
_state = buffer[3];
}
}
_client->stop();
} else {
_state = MQTT_CONNECT_FAILED;
}
return false;
}
return true;
}
// reads a byte into result
boolean PubSubClient::readByte(uint8_t * result) {
uint32_t previousMillis = millis();
while(!_client->available()) {
yield();
uint32_t currentMillis = millis();
if(currentMillis - previousMillis >= ((int32_t) this->socketTimeout * 1000)){
return false;
}
}
*result = _client->read();
return true;
}
// reads a byte into result[*index] and increments index
boolean PubSubClient::readByte(uint8_t * result, uint16_t * index){
uint16_t current_index = *index;
uint8_t * write_address = &(result[current_index]);
if(readByte(write_address)){
*index = current_index + 1;
return true;
}
return false;
}
uint32_t PubSubClient::readPacket(uint8_t* lengthLength) {
uint16_t len = 0;
if(!readByte(this->buffer, &len)) return 0;
bool isPublish = (this->buffer[0]&0xF0) == MQTTPUBLISH;
uint32_t multiplier = 1;
uint32_t length = 0;
uint8_t digit = 0;
uint16_t skip = 0;
uint32_t start = 0;
do {
if (len == 5) {
// Invalid remaining length encoding - kill the connection
_state = MQTT_DISCONNECTED;
_client->stop();
return 0;
}
if(!readByte(&digit)) return 0;
this->buffer[len++] = digit;
length += (digit & 127) * multiplier;
multiplier <<=7; //multiplier *= 128
} while ((digit & 128) != 0);
*lengthLength = len-1;
if (isPublish) {
// Read in topic length to calculate bytes to skip over for Stream writing
if(!readByte(this->buffer, &len)) return 0;
if(!readByte(this->buffer, &len)) return 0;
skip = (this->buffer[*lengthLength+1]<<8)+this->buffer[*lengthLength+2];
start = 2;
if (this->buffer[0]&MQTTQOS1) {
// skip message id
skip += 2;
}
}
uint32_t idx = len;
for (uint32_t i = start;i<length;i++) {
if(!readByte(&digit)) return 0;
if (this->stream) {
if (isPublish && idx-*lengthLength-2>skip) {
this->stream->write(digit);
}
}
if (len < this->bufferSize) {
this->buffer[len] = digit;
len++;
}
idx++;
}
if (!this->stream && idx > this->bufferSize) {
len = 0; // This will cause the packet to be ignored.
}
return len;
}
boolean PubSubClient::loop() {
if (connected()) {
unsigned long t = millis();
if ((t - lastInActivity > this->keepAlive*1000UL) || (t - lastOutActivity > this->keepAlive*1000UL)) {
if (pingOutstanding) {
this->_state = MQTT_CONNECTION_TIMEOUT;
_client->stop();
return false;
} else {
this->buffer[0] = MQTTPINGREQ;
this->buffer[1] = 0;
_client->write(this->buffer,2);
lastOutActivity = t;
lastInActivity = t;
pingOutstanding = true;
}
}
if (_client->available()) {
uint8_t llen;
uint16_t len = readPacket(&llen);
uint16_t msgId = 0;
uint8_t *payload;
if (len > 0) {
lastInActivity = t;
uint8_t type = this->buffer[0]&0xF0;
if (type == MQTTPUBLISH) {
if (callback) {
uint16_t tl = (this->buffer[llen+1]<<8)+this->buffer[llen+2]; /* topic length in bytes */
memmove(this->buffer+llen+2,this->buffer+llen+3,tl); /* move topic inside buffer 1 byte to front */
this->buffer[llen+2+tl] = 0; /* end the topic as a 'C' string with \x00 */
char *topic = (char*) this->buffer+llen+2;
// msgId only present for QOS>0
if ((this->buffer[0]&0x06) == MQTTQOS1) {
msgId = (this->buffer[llen+3+tl]<<8)+this->buffer[llen+3+tl+1];
payload = this->buffer+llen+3+tl+2;
callback(topic,payload,len-llen-3-tl-2);
this->buffer[0] = MQTTPUBACK;
this->buffer[1] = 2;
this->buffer[2] = (msgId >> 8);
this->buffer[3] = (msgId & 0xFF);
_client->write(this->buffer,4);
lastOutActivity = t;
} else {
payload = this->buffer+llen+3+tl;
callback(topic,payload,len-llen-3-tl);
}
}
} else if (type == MQTTPINGREQ) {
this->buffer[0] = MQTTPINGRESP;
this->buffer[1] = 0;
_client->write(this->buffer,2);
} else if (type == MQTTPINGRESP) {
pingOutstanding = false;
}
} else if (!connected()) {
// readPacket has closed the connection
return false;
}
}
return true;
}
return false;
}
boolean PubSubClient::publish(const char* topic, const char* payload) {
return publish(topic,(const uint8_t*)payload, payload ? strnlen(payload, this->bufferSize) : 0,false);
}
boolean PubSubClient::publish(const char* topic, const char* payload, boolean retained) {
return publish(topic,(const uint8_t*)payload, payload ? strnlen(payload, this->bufferSize) : 0,retained);
}
boolean PubSubClient::publish(const char* topic, const uint8_t* payload, unsigned int plength) {
return publish(topic, payload, plength, false);
}
boolean PubSubClient::publish(const char* topic, const uint8_t* payload, unsigned int plength, boolean retained) {
if (connected()) {
if (this->bufferSize < MQTT_MAX_HEADER_SIZE + 2+strnlen(topic, this->bufferSize) + plength) {
// Too long
return false;
}
// Leave room in the buffer for header and variable length field
uint16_t length = MQTT_MAX_HEADER_SIZE;
length = writeString(topic,this->buffer,length);
// Add payload
uint16_t i;
for (i=0;i<plength;i++) {
this->buffer[length++] = payload[i];
}
// Write the header
uint8_t header = MQTTPUBLISH;
if (retained) {
header |= 1;
}
return write(header,this->buffer,length-MQTT_MAX_HEADER_SIZE);
}
return false;
}
boolean PubSubClient::publish_P(const char* topic, const char* payload, boolean retained) {
return publish_P(topic, (const uint8_t*)payload, payload ? strnlen(payload, this->bufferSize) : 0, retained);
}
boolean PubSubClient::publish_P(const char* topic, const uint8_t* payload, unsigned int plength, boolean retained) {
uint8_t llen = 0;
uint8_t digit;
unsigned int rc = 0;
uint16_t tlen;
unsigned int pos = 0;
unsigned int i;
uint8_t header;
unsigned int len;
int expectedLength;
if (!connected()) {
return false;
}
tlen = strnlen(topic, this->bufferSize);
header = MQTTPUBLISH;
if (retained) {
header |= 1;
}
this->buffer[pos++] = header;
len = plength + 2 + tlen;
do {
digit = len & 127; //digit = len %128
len >>= 7; //len = len / 128
if (len > 0) {
digit |= 0x80;
}
this->buffer[pos++] = digit;
llen++;
} while(len>0);
pos = writeString(topic,this->buffer,pos);
rc += _client->write(this->buffer,pos);
for (i=0;i<plength;i++) {
rc += _client->write((char)pgm_read_byte_near(payload + i));
}
lastOutActivity = millis();
expectedLength = 1 + llen + 2 + tlen + plength;
return (rc == expectedLength);
}
boolean PubSubClient::beginPublish(const char* topic, unsigned int plength, boolean retained) {
if (connected()) {
// Send the header and variable length field
uint16_t length = MQTT_MAX_HEADER_SIZE;
length = writeString(topic,this->buffer,length);
uint8_t header = MQTTPUBLISH;
if (retained) {
header |= 1;
}
size_t hlen = buildHeader(header, this->buffer, plength+length-MQTT_MAX_HEADER_SIZE);
uint16_t rc = _client->write(this->buffer+(MQTT_MAX_HEADER_SIZE-hlen),length-(MQTT_MAX_HEADER_SIZE-hlen));
lastOutActivity = millis();
return (rc == (length-(MQTT_MAX_HEADER_SIZE-hlen)));
}
return false;
}
int PubSubClient::endPublish() {
return 1;
}
size_t PubSubClient::write(uint8_t data) {
lastOutActivity = millis();
return _client->write(data);
}
size_t PubSubClient::write(const uint8_t *buffer, size_t size) {
lastOutActivity = millis();
return _client->write(buffer,size);
}
size_t PubSubClient::buildHeader(uint8_t header, uint8_t* buf, uint16_t length) {
uint8_t lenBuf[4];
uint8_t llen = 0;
uint8_t digit;
uint8_t pos = 0;
uint16_t len = length;
do {
digit = len & 127; //digit = len %128
len >>= 7; //len = len / 128
if (len > 0) {
digit |= 0x80;
}
lenBuf[pos++] = digit;
llen++;
} while(len>0);
buf[4-llen] = header;
for (int i=0;i<llen;i++) {
buf[MQTT_MAX_HEADER_SIZE-llen+i] = lenBuf[i];
}
return llen+1; // Full header size is variable length bit plus the 1-byte fixed header
}
boolean PubSubClient::write(uint8_t header, uint8_t* buf, uint16_t length) {
uint16_t rc;
uint8_t hlen = buildHeader(header, buf, length);
#ifdef MQTT_MAX_TRANSFER_SIZE
uint8_t* writeBuf = buf+(MQTT_MAX_HEADER_SIZE-hlen);
uint16_t bytesRemaining = length+hlen; //Match the length type
uint8_t bytesToWrite;
boolean result = true;
while((bytesRemaining > 0) && result) {
bytesToWrite = (bytesRemaining > MQTT_MAX_TRANSFER_SIZE)?MQTT_MAX_TRANSFER_SIZE:bytesRemaining;
rc = _client->write(writeBuf,bytesToWrite);
result = (rc == bytesToWrite);
bytesRemaining -= rc;
writeBuf += rc;
}
return result;
#else
rc = _client->write(buf+(MQTT_MAX_HEADER_SIZE-hlen),length+hlen);
lastOutActivity = millis();
return (rc == hlen+length);
#endif
}
boolean PubSubClient::subscribe(const char* topic) {
return subscribe(topic, 0);
}
boolean PubSubClient::subscribe(const char* topic, uint8_t qos) {
size_t topicLength = strnlen(topic, this->bufferSize);
if (topic == 0) {
return false;
}
if (qos > 1) {
return false;
}
if (this->bufferSize < 9 + topicLength) {
// Too long
return false;
}
if (connected()) {
// Leave room in the buffer for header and variable length field
uint16_t length = MQTT_MAX_HEADER_SIZE;
nextMsgId++;
if (nextMsgId == 0) {
nextMsgId = 1;
}
this->buffer[length++] = (nextMsgId >> 8);
this->buffer[length++] = (nextMsgId & 0xFF);
length = writeString((char*)topic, this->buffer,length);
this->buffer[length++] = qos;
return write(MQTTSUBSCRIBE|MQTTQOS1,this->buffer,length-MQTT_MAX_HEADER_SIZE);
}
return false;
}
boolean PubSubClient::unsubscribe(const char* topic) {
size_t topicLength = strnlen(topic, this->bufferSize);
if (topic == 0) {
return false;
}
if (this->bufferSize < 9 + topicLength) {
// Too long
return false;
}
if (connected()) {
uint16_t length = MQTT_MAX_HEADER_SIZE;
nextMsgId++;
if (nextMsgId == 0) {
nextMsgId = 1;
}
this->buffer[length++] = (nextMsgId >> 8);
this->buffer[length++] = (nextMsgId & 0xFF);
length = writeString(topic, this->buffer,length);
return write(MQTTUNSUBSCRIBE|MQTTQOS1,this->buffer,length-MQTT_MAX_HEADER_SIZE);
}
return false;
}
void PubSubClient::disconnect() {
this->buffer[0] = MQTTDISCONNECT;
this->buffer[1] = 0;
_client->write(this->buffer,2);
_state = MQTT_DISCONNECTED;
_client->flush();
_client->stop();
lastInActivity = lastOutActivity = millis();
}
uint16_t PubSubClient::writeString(const char* string, uint8_t* buf, uint16_t pos) {
const char* idp = string;
uint16_t i = 0;
pos += 2;
while (*idp) {
buf[pos++] = *idp++;
i++;
}
buf[pos-i-2] = (i >> 8);
buf[pos-i-1] = (i & 0xFF);
return pos;
}
boolean PubSubClient::connected() {
boolean rc;
if (_client == NULL ) {
rc = false;
} else {
rc = (int)_client->connected();
if (!rc) {
if (this->_state == MQTT_CONNECTED) {
this->_state = MQTT_CONNECTION_LOST;
_client->flush();
_client->stop();
}
} else {
return this->_state == MQTT_CONNECTED;
}
}
return rc;
}
PubSubClient& PubSubClient::setServer(uint8_t * ip, uint16_t port) {
IPAddress addr(ip[0],ip[1],ip[2],ip[3]);
return setServer(addr,port);
}
PubSubClient& PubSubClient::setServer(IPAddress ip, uint16_t port) {
this->ip = ip;
this->port = port;
this->domain = NULL;
return *this;
}
PubSubClient& PubSubClient::setServer(const char * domain, uint16_t port) {
this->domain = domain;
this->port = port;
return *this;
}
PubSubClient& PubSubClient::setCallback(MQTT_CALLBACK_SIGNATURE) {
this->callback = callback;
return *this;
}
PubSubClient& PubSubClient::setClient(Client& client){
this->_client = &client;
return *this;
}
PubSubClient& PubSubClient::setStream(Stream& stream){
this->stream = &stream;
return *this;
}
int PubSubClient::state() {
return this->_state;
}
boolean PubSubClient::setBufferSize(uint16_t size) {
if (size == 0) {
// Cannot set it back to 0
return false;
}
if (this->bufferSize == 0) {
this->buffer = (uint8_t*)malloc(size);
} else {
uint8_t* newBuffer = (uint8_t*)realloc(this->buffer, size);
if (newBuffer != NULL) {
this->buffer = newBuffer;
} else {
return false;
}
}
this->bufferSize = size;
return (this->buffer != NULL);
}
uint16_t PubSubClient::getBufferSize() {
return this->bufferSize;
}
PubSubClient& PubSubClient::setKeepAlive(uint16_t keepAlive) {
this->keepAlive = keepAlive;
return *this;
}
PubSubClient& PubSubClient::setSocketTimeout(uint16_t timeout) {
this->socketTimeout = timeout;
return *this;
}

View File

@ -0,0 +1,184 @@
/*
PubSubClient.h - A simple client for MQTT.
Nick O'Leary
http://knolleary.net
*/
#ifndef PubSubClient_h
#define PubSubClient_h
#include <Arduino.h>
#include "IPAddress.h"
#include "Client.h"
#include "Stream.h"
#define MQTT_VERSION_3_1 3
#define MQTT_VERSION_3_1_1 4
// MQTT_VERSION : Pick the version
//#define MQTT_VERSION MQTT_VERSION_3_1
#ifndef MQTT_VERSION
#define MQTT_VERSION MQTT_VERSION_3_1_1
#endif
// MQTT_MAX_PACKET_SIZE : Maximum packet size. Override with setBufferSize().
#ifndef MQTT_MAX_PACKET_SIZE
#define MQTT_MAX_PACKET_SIZE 256
#endif
// MQTT_KEEPALIVE : keepAlive interval in Seconds. Override with setKeepAlive()
#ifndef MQTT_KEEPALIVE
#define MQTT_KEEPALIVE 15
#endif
// MQTT_SOCKET_TIMEOUT: socket timeout interval in Seconds. Override with setSocketTimeout()
#ifndef MQTT_SOCKET_TIMEOUT
#define MQTT_SOCKET_TIMEOUT 15
#endif
// MQTT_MAX_TRANSFER_SIZE : limit how much data is passed to the network client
// in each write call. Needed for the Arduino Wifi Shield. Leave undefined to
// pass the entire MQTT packet in each write call.
//#define MQTT_MAX_TRANSFER_SIZE 80
// Possible values for client.state()
#define MQTT_CONNECTION_TIMEOUT -4
#define MQTT_CONNECTION_LOST -3
#define MQTT_CONNECT_FAILED -2
#define MQTT_DISCONNECTED -1
#define MQTT_CONNECTED 0
#define MQTT_CONNECT_BAD_PROTOCOL 1
#define MQTT_CONNECT_BAD_CLIENT_ID 2
#define MQTT_CONNECT_UNAVAILABLE 3
#define MQTT_CONNECT_BAD_CREDENTIALS 4
#define MQTT_CONNECT_UNAUTHORIZED 5
#define MQTTCONNECT 1 << 4 // Client request to connect to Server
#define MQTTCONNACK 2 << 4 // Connect Acknowledgment
#define MQTTPUBLISH 3 << 4 // Publish message
#define MQTTPUBACK 4 << 4 // Publish Acknowledgment
#define MQTTPUBREC 5 << 4 // Publish Received (assured delivery part 1)
#define MQTTPUBREL 6 << 4 // Publish Release (assured delivery part 2)
#define MQTTPUBCOMP 7 << 4 // Publish Complete (assured delivery part 3)
#define MQTTSUBSCRIBE 8 << 4 // Client Subscribe request
#define MQTTSUBACK 9 << 4 // Subscribe Acknowledgment
#define MQTTUNSUBSCRIBE 10 << 4 // Client Unsubscribe request
#define MQTTUNSUBACK 11 << 4 // Unsubscribe Acknowledgment
#define MQTTPINGREQ 12 << 4 // PING Request
#define MQTTPINGRESP 13 << 4 // PING Response
#define MQTTDISCONNECT 14 << 4 // Client is Disconnecting
#define MQTTReserved 15 << 4 // Reserved
#define MQTTQOS0 (0 << 1)
#define MQTTQOS1 (1 << 1)
#define MQTTQOS2 (2 << 1)
// Maximum size of fixed header and variable length size header
#define MQTT_MAX_HEADER_SIZE 5
#if defined(ESP8266) || defined(ESP32)
#include <functional>
#define MQTT_CALLBACK_SIGNATURE std::function<void(char*, uint8_t*, unsigned int)> callback
#else
#define MQTT_CALLBACK_SIGNATURE void (*callback)(char*, uint8_t*, unsigned int)
#endif
#define CHECK_STRING_LENGTH(l,s) if (l+2+strnlen(s, this->bufferSize) > this->bufferSize) {_client->stop();return false;}
class PubSubClient : public Print {
private:
Client* _client;
uint8_t* buffer;
uint16_t bufferSize;
uint16_t keepAlive;
uint16_t socketTimeout;
uint16_t nextMsgId;
unsigned long lastOutActivity;
unsigned long lastInActivity;
bool pingOutstanding;
MQTT_CALLBACK_SIGNATURE;
uint32_t readPacket(uint8_t*);
boolean readByte(uint8_t * result);
boolean readByte(uint8_t * result, uint16_t * index);
boolean write(uint8_t header, uint8_t* buf, uint16_t length);
uint16_t writeString(const char* string, uint8_t* buf, uint16_t pos);
// Build up the header ready to send
// Returns the size of the header
// Note: the header is built at the end of the first MQTT_MAX_HEADER_SIZE bytes, so will start
// (MQTT_MAX_HEADER_SIZE - <returned size>) bytes into the buffer
size_t buildHeader(uint8_t header, uint8_t* buf, uint16_t length);
IPAddress ip;
const char* domain;
uint16_t port;
Stream* stream;
int _state;
public:
PubSubClient();
PubSubClient(Client& client);
PubSubClient(IPAddress, uint16_t, Client& client);
PubSubClient(IPAddress, uint16_t, Client& client, Stream&);
PubSubClient(IPAddress, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client);
PubSubClient(IPAddress, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client, Stream&);
PubSubClient(uint8_t *, uint16_t, Client& client);
PubSubClient(uint8_t *, uint16_t, Client& client, Stream&);
PubSubClient(uint8_t *, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client);
PubSubClient(uint8_t *, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client, Stream&);
PubSubClient(const char*, uint16_t, Client& client);
PubSubClient(const char*, uint16_t, Client& client, Stream&);
PubSubClient(const char*, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client);
PubSubClient(const char*, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client, Stream&);
~PubSubClient();
PubSubClient& setServer(IPAddress ip, uint16_t port);
PubSubClient& setServer(uint8_t * ip, uint16_t port);
PubSubClient& setServer(const char * domain, uint16_t port);
PubSubClient& setCallback(MQTT_CALLBACK_SIGNATURE);
PubSubClient& setClient(Client& client);
PubSubClient& setStream(Stream& stream);
PubSubClient& setKeepAlive(uint16_t keepAlive);
PubSubClient& setSocketTimeout(uint16_t timeout);
boolean setBufferSize(uint16_t size);
uint16_t getBufferSize();
boolean connect(const char* id);
boolean connect(const char* id, const char* user, const char* pass);
boolean connect(const char* id, const char* willTopic, uint8_t willQos, boolean willRetain, const char* willMessage);
boolean connect(const char* id, const char* user, const char* pass, const char* willTopic, uint8_t willQos, boolean willRetain, const char* willMessage);
boolean connect(const char* id, const char* user, const char* pass, const char* willTopic, uint8_t willQos, boolean willRetain, const char* willMessage, boolean cleanSession);
void disconnect();
boolean publish(const char* topic, const char* payload);
boolean publish(const char* topic, const char* payload, boolean retained);
boolean publish(const char* topic, const uint8_t * payload, unsigned int plength);
boolean publish(const char* topic, const uint8_t * payload, unsigned int plength, boolean retained);
boolean publish_P(const char* topic, const char* payload, boolean retained);
boolean publish_P(const char* topic, const uint8_t * payload, unsigned int plength, boolean retained);
// Start to publish a message.
// This API:
// beginPublish(...)
// one or more calls to write(...)
// endPublish()
// Allows for arbitrarily large payloads to be sent without them having to be copied into
// a new buffer and held in memory at one time
// Returns 1 if the message was started successfully, 0 if there was an error
boolean beginPublish(const char* topic, unsigned int plength, boolean retained);
// Finish off this publish message (started with beginPublish)
// Returns 1 if the packet was sent successfully, 0 if there was an error
int endPublish();
// Write a single byte of payload (only to be used with beginPublish/endPublish)
virtual size_t write(uint8_t);
// Write size bytes from buffer into the payload (only to be used with beginPublish/endPublish)
// Returns the number of bytes written
virtual size_t write(const uint8_t *buffer, size_t size);
boolean subscribe(const char* topic);
boolean subscribe(const char* topic, uint8_t qos);
boolean unsubscribe(const char* topic);
boolean loop();
boolean connected();
int state();
};
#endif

View File

@ -235,92 +235,168 @@ const BoardDef bsps[_BOARD_MAX] = {
.name = "ONE_INDOOR",
},
/** OPEN_AIR_OUTDOOR */
[OPEN_AIR_OUTDOOR] = {
.SenseAirS8 =
{
.uart_tx_pin = 1,
.uart_rx_pin = 0,
[OPEN_AIR_OUTDOOR] =
{
.SenseAirS8 =
{
.uart_tx_pin = 1,
.uart_rx_pin = 0,
#if defined(ESP8266)
.supported = false,
.supported = false,
#else
.supported = true,
.supported = true,
#endif
},
/** Use UART0 don't use define pin number */
.Pms5003 =
{
.uart_tx_pin = -1,
.uart_rx_pin = -1,
},
/** Use UART0 don't use define pin number */
.Pms5003 =
{
.uart_tx_pin = -1,
.uart_rx_pin = -1,
#if defined(ESP8266)
.supported = false,
.supported = false,
#else
.supported = true,
.supported = true,
#endif
},
.I2C =
{
.sda_pin = 7,
.scl_pin = 6,
},
.I2C =
{
.sda_pin = 7,
.scl_pin = 6,
#if defined(ESP8266)
.supported = false,
.supported = false,
#else
.supported = true,
.supported = true,
#endif
},
.SW =
{
},
.SW =
{
#if defined(ESP8266)
.pin = -1,
.activeLevel = 1,
.supported = false,
.pin = -1,
.activeLevel = 1,
.supported = false,
#else
.pin = 9,
.activeLevel = 0,
.supported = true,
.pin = 9,
.activeLevel = 0,
.supported = true,
#endif
},
.LED =
{
},
.LED =
{
#if defined(ESP8266)
.pin = -1,
.rgbNum = 0,
.onState = 0,
.supported = false,
.rgbSupported = false,
.pin = -1,
.rgbNum = 0,
.onState = 0,
.supported = false,
.rgbSupported = false,
#else
.pin = 10,
.rgbNum = 0,
.onState = 1,
.supported = true,
.rgbSupported = false,
.pin = 10,
.rgbNum = 0,
.onState = 1,
.supported = true,
.rgbSupported = false,
#endif
},
.OLED =
{
},
.OLED =
{
#if defined(ESP8266)
.width = 0,
.height = 0,
.addr = 0,
.supported = false,
.width = 0,
.height = 0,
.addr = 0,
.supported = false,
#else
.width = 128,
.height = 64,
.addr = 0x3C,
.supported = true,
.width = 128,
.height = 64,
.addr = 0x3C,
.supported = true,
#endif
},
.WDG =
{
},
.WDG =
{
#if defined(ESP8266)
.resetPin = -1,
.supported = false,
.resetPin = -1,
.supported = false,
#else
.resetPin = 2,
.supported = true,
.resetPin = 2,
.supported = true,
#endif
},
.name = "OPEN_AIR_OUTDOOR",
}};
},
.name = "OPEN_AIR_OUTDOOR",
},
/** DIY_PRO_INDOOR_V3_3 */
[DIY_PRO_INDOOR_V3_3] =
{
.SenseAirS8 =
{
.uart_tx_pin = 2,
.uart_rx_pin = 0,
#if defined(ESP8266)
.supported = true,
#else
.supported = false,
#endif
},
.Pms5003 =
{
.uart_tx_pin = 14,
.uart_rx_pin = 12,
#if defined(ESP8266)
.supported = true,
#else
.supported = false,
#endif
},
.I2C =
{
.sda_pin = 4,
.scl_pin = 5,
#if defined(ESP8266)
.supported = true,
#else
.supported = false,
#endif
},
.SW =
{
#if defined(ESP8266)
.pin = -1, /** D7 */
.activeLevel = 0,
.supported = false,
#else
.pin = -1,
.activeLevel = 1,
.supported = false,
#endif
},
.LED =
{
.pin = -1,
.rgbNum = 0,
.onState = 0,
.supported = false,
.rgbSupported = false,
},
.OLED =
{
#if defined(ESP8266)
.width = 128,
.height = 64,
.addr = 0x3C,
.supported = true,
#else
.width = 0,
.height = 0,
.addr = 0,
.supported = false,
#endif
},
.WDG =
{
.resetPin = -1,
.supported = false,
},
.name = "DIY_PRO_INDOOR_V3_3",
},
};
/**
* @brief Get Board Support Package
@ -337,9 +413,9 @@ const BoardDef *getBoardDef(BoardType def) {
/**
* @brief Get the Board Name
*
*
* @param type BoarType
* @return const char*
* @return const char*
*/
const char *getBoardDefName(BoardType type) {
if (type >= _BOARD_MAX) {

View File

@ -21,6 +21,7 @@ enum BoardType {
DIY_PRO_INDOOR_V4_2 = 0x01,
ONE_INDOOR = 0x02,
OPEN_AIR_OUTDOOR = 0x03,
DIY_PRO_INDOOR_V3_3 = 0x04,
_BOARD_MAX
};

View File

@ -62,13 +62,13 @@ void LedBar::setColor(uint8_t red, uint8_t green, uint8_t blue, int ledNum) {
/**
* @brief Set LED brightness apply for all LED bar
*
* @param brightness Brightness (0 - 255)
* @param brightness Brightness (0 - 100)%
*/
void LedBar::setBrighness(uint8_t brightness) {
void LedBar::setBrightness(uint8_t brightness) {
if (this->isBegin() == false) {
return;
}
pixel()->setBrightness(brightness);
pixel()->setBrightness((brightness * 0xff) / 100);
}
/**
@ -116,6 +116,9 @@ void LedBar::setColor(uint8_t red, uint8_t green, uint8_t blue) {
*/
void LedBar::show(void) {
// Ignore update the LED if LED bar disabled
if(this->isBegin() == false) {
return;
}
if (enabled == false) {
return;
}

View File

@ -19,7 +19,7 @@ public:
void begin(void);
void setColor(uint8_t red, uint8_t green, uint8_t blue, int ledNum);
void setColor(uint8_t red, uint8_t green, uint8_t blue);
void setBrighness(uint8_t brightness);
void setBrightness(uint8_t brightness);
int getNumberOfLeds(void);
void show(void);
void clear(void);

94
src/Main/utils.cpp Normal file
View File

@ -0,0 +1,94 @@
#include "utils.h"
#define VALID_TEMPERATURE_MAX (125)
#define VALID_TEMPERATURE_MIN (-40)
#define INVALID_TEMPERATURE (-1000)
#define VALID_HUMIDITY_MAX (100)
#define VALID_HUMIDITY_MIN (0)
#define INVALID_HUMIDITY (-1)
#define VALID_PMS_MAX (1000)
#define VALID_PMS_MIN (0)
#define INVALID_PMS (-1)
#define VALID_PMS03COUNT_MIN (0)
#define VALID_CO2_MAX (10000)
#define VALID_CO2_MIN (0)
#define INVALID_CO2 (-1)
#define VALID_NOX_MIN (0)
#define VALID_VOC_MIN (0)
#define INVALID_NOX (-1)
#define INVALID_VOC (-1)
utils::utils(/* args */) {}
utils::~utils() {}
bool utils::isValidTemperature(float value) {
if ((value >= VALID_TEMPERATURE_MIN) && (value <= VALID_TEMPERATURE_MAX)) {
return true;
}
return false;
}
bool utils::isValidHumidity(float value) {
if ((value >= VALID_HUMIDITY_MIN) && (value <= VALID_HUMIDITY_MAX)) {
return true;
}
return false;
}
bool utils::isValidCO2(int16_t value) {
if ((value >= VALID_CO2_MIN) && (value <= VALID_CO2_MAX)) {
return true;
}
return false;
}
bool utils::isValidPm(int value) {
if ((value >= VALID_PMS_MIN) && (value <= VALID_PMS_MAX)) {
return true;
}
return false;
}
bool utils::isValidPm03Count(int value) {
if (value >= VALID_PMS03COUNT_MIN) {
return true;
}
return false;
}
bool utils::isValidNOx(int value) {
if (value >= VALID_NOX_MIN) {
return true;
}
return false;
}
bool utils::isValidVOC(int value) {
if (value >= VALID_VOC_MIN) {
return true;
}
return false;
}
float utils::getInvalidTemperature(void) { return INVALID_TEMPERATURE; }
float utils::getInvalidHumidity(void) { return INVALID_HUMIDITY; }
int utils::getInvalidCO2(void) { return INVALID_CO2; }
int utils::getInvalidPmValue(void) { return INVALID_PMS; }
int utils::getInvalidNOx(void) { return INVALID_NOX; }
int utils::getInvalidVOC(void) { return INVALID_VOC; }
float utils::degreeC_To_F(float t) {
/** (t * 9)/5 + 32 */
return t * 1.8f + 32.0f;
}

31
src/Main/utils.h Normal file
View File

@ -0,0 +1,31 @@
#ifndef _UTILS_H_
#define _UTILS_H_
#include <Arduino.h>
class utils
{
private:
/* data */
public:
utils(/* args */);
~utils();
static bool isValidTemperature(float value);
static bool isValidHumidity(float value);
static bool isValidCO2(int16_t value);
static bool isValidPm(int value);
static bool isValidPm03Count(int value);
static bool isValidNOx(int value);
static bool isValidVOC(int value);
static float getInvalidTemperature(void);
static float getInvalidHumidity(void);
static int getInvalidCO2(void);
static int getInvalidPmValue(void);
static int getInvalidNOx(void);
static int getInvalidVOC(void);
static float degreeC_To_F(float t);
};
#endif /** _UTILS_H_ */

View File

@ -1,11 +1,19 @@
#ifdef ESP32
#include "MqttClient.h"
#include "Libraries/pubsubclient-2.8/src/PubSubClient.h"
#ifdef ESP32
static void __mqtt_event_handler(void *handler_args, esp_event_base_t base,
int32_t event_id, void *event_data);
#else
#define CLIENT() ((PubSubClient *)client)
#endif
MqttClient::MqttClient(Stream &debugLog) : PrintLog(debugLog, "MqttClient") {}
MqttClient::MqttClient(Stream &debugLog) : PrintLog(debugLog, "MqttClient") {
#ifdef ESP32
#else
client = NULL;
#endif
}
MqttClient::~MqttClient() {}
@ -22,6 +30,7 @@ bool MqttClient::begin(String uri) {
this->uri = uri;
logInfo("Init uri: " + uri);
#ifdef ESP32
/** config esp_mqtt client */
esp_mqtt_client_config_t config = {
.uri = this->uri.c_str(),
@ -45,6 +54,108 @@ bool MqttClient::begin(String uri) {
logError("Client start failed");
return false;
}
#else
// mqtt://<Username>:<Password>@<Host>:<Port>
bool hasUser = false;
for (unsigned int i = 0; i < this->uri.length(); i++) {
if (this->uri[i] == '@') {
hasUser = true;
break;
}
}
user = "";
password = "";
server = "";
port = 0;
char *serverPort = NULL;
char *buf = (char *)this->uri.c_str();
if (hasUser) {
// mqtt://<Username>:<Password>@<Host>:<Port>
char *userPass = strtok(buf, "@");
serverPort = strtok(NULL, "@");
if (userPass == NULL) {
logError("User and Password invalid");
return false;
} else {
if ((userPass[5] == '/') && (userPass[6] == '/')) { /** Check mqtt:// */
userPass = &userPass[7];
} else if ((userPass[6] == '/') &&
(userPass[7] == '/')) { /** Check mqtts:// */
userPass = &userPass[8];
} else {
logError("Server invalid");
return false;
}
buf = strtok(userPass, ":");
if (buf == NULL) {
logError("User invalid");
return false;
}
user = String(buf);
buf = strtok(NULL, "@");
if (buf == NULL) {
logError("Password invalid");
return false;
}
password = String(buf);
logInfo("Username: " + user);
logInfo("Password: " + password);
}
if (serverPort == NULL) {
logError("Server and port invalid");
return false;
}
} else {
// mqtt://<Host>:<Port>
if ((buf[5] == '/') && (buf[6] == '/')) { /** Check mqtt:// */
serverPort = &buf[7];
} else if ((buf[6] == '/') && (buf[7] == '/')) { /** Check mqtts:// */
serverPort = &buf[8];
} else {
logError("Server invalid");
return false;
}
}
if (serverPort == NULL) {
logError("Server and port invalid");
return false;
}
buf = strtok(serverPort, ":");
if (buf == NULL) {
logError("Server invalid");
return false;
}
server = String(buf);
logInfo("Server: " + server);
buf = strtok(NULL, ":");
if (buf == NULL) {
logError("Port invalid");
return false;
}
port = (uint16_t)String(buf).toInt();
logInfo("Port: " + String(port));
if (client == NULL) {
client = new PubSubClient(__wifiClient);
if (client == NULL) {
return false;
}
}
CLIENT()->setServer(server.c_str(), port);
CLIENT()->setBufferSize(1024);
connected = false;
#endif
isBegin = true;
connectionFailedCount = 0;
@ -56,12 +167,16 @@ void MqttClient::end(void) {
logWarning("Already end, call 'begin' and try again");
return;
}
#ifdef ESP32
esp_mqtt_client_disconnect(client);
esp_mqtt_client_stop(client);
esp_mqtt_client_destroy(client);
client = NULL;
#else
CLIENT()->disconnect();
#endif
isBegin = false;
this->uri = "";
logInfo("end");
}
@ -86,10 +201,17 @@ bool MqttClient::publish(const char *topic, const char *payload, int len) {
return false;
}
#ifdef ESP32
if (esp_mqtt_client_publish(client, topic, payload, len, 0, 0) == ESP_OK) {
logInfo("Publish success");
return true;
}
#else
if (CLIENT()->publish(topic, payload)) {
logInfo("Publish success");
return true;
}
#endif
logError("Publish failed");
return false;
}
@ -114,7 +236,9 @@ bool MqttClient::isCurrentUri(String &uri) {
* @return true Connected
* @return false Disconnected
*/
bool MqttClient::isConnected(void) { return connected; }
bool MqttClient::isConnected(void) {
return connected;
}
/**
* @brief Get number of connection failed
@ -123,6 +247,35 @@ bool MqttClient::isConnected(void) { return connected; }
*/
int MqttClient::getConnectionFailedCount(void) { return connectionFailedCount; }
#ifdef ESP8266
bool MqttClient::connect(String id) {
if (isBegin == false) {
return false;
}
if (this->uri.isEmpty()) {
return false;
}
connected = false;
if (user.isEmpty()) {
logInfo("Connect without auth");
if(CLIENT()->connect(id.c_str())) {
connected = true;
}
return connected;
}
return CLIENT()->connect(id.c_str(), user.c_str(), password.c_str());
}
void MqttClient::handle(void) {
if (isBegin == false) {
return;
}
CLIENT()->loop();
}
#endif
#ifdef ESP32
static void __mqtt_event_handler(void *handler_args, esp_event_base_t base,
int32_t event_id, void *event_data) {
MqttClient *mqtt = (MqttClient *)handler_args;
@ -164,5 +317,4 @@ static void __mqtt_event_handler(void *handler_args, esp_event_base_t base,
break;
}
}
#endif /** ESP32 */
#endif

View File

@ -2,8 +2,10 @@
#define _AG_MQTT_CLIENT_H_
#ifdef ESP32
#include "mqtt_client.h"
#else
#include <WiFiClient.h>
#endif /** ESP32 */
#include "Main/PrintLog.h"
#include <Arduino.h>
@ -11,7 +13,16 @@ class MqttClient: public PrintLog {
private:
bool isBegin = false;
String uri;
#ifdef ESP32
esp_mqtt_client_handle_t client;
#else
WiFiClient __wifiClient;
void* client;
String password;
String user;
String server;
uint16_t port;
#endif
bool connected = false;
int connectionFailedCount = 0;
@ -26,8 +37,10 @@ public:
bool isCurrentUri(String &uri);
bool isConnected(void);
int getConnectionFailedCount(void);
#ifdef ESP8266
bool connect(String id);
void handle(void);
#endif
};
#endif /** ESP32 */
#endif /** _AG_MQTT_CLIENT_H_ */

View File

@ -2,248 +2,294 @@
#include "../Main/BoardDef.h"
/**
* @brief Init and check that sensor has connected
*
* @brief Initializes the sensor and attempts to read data.
*
* @param stream UART stream
* @return true Sucecss
* @return false Failure
*/
bool PMSBase::begin(Stream *stream) {
this->stream = stream;
Serial.printf("initializing PM sensor\n");
failed = true;
lastRead = 0; // To read buffer on handle without wait after 1.5sec
failCount = 0;
_connected = false;
this->stream->flush();
// empty first
int bytesCleared = 0;
while (stream->read() != -1) {
bytesCleared++;
}
Serial.printf("cleared %d byte(s)\n", bytesCleared);
// explicitly put the sensor into active mode, this seems to be be needed for the Cubic PM2009X
Serial.printf("setting active mode\n");
uint8_t activeModeCommand[] = { 0x42, 0x4D, 0xE1, 0x00, 0x01, 0x01, 0x71 };
size_t bytesWritten = stream->write(activeModeCommand, sizeof(activeModeCommand));
Serial.printf("%d byte(s) written\n", bytesWritten);
// Run and check sensor data for 4sec
while (1) {
handle();
if (failed == false) {
return true;
unsigned long lastInit = millis();
while (true) {
readPackage(stream);
if (_connected) {
break;
}
delay(1);
uint32_t ms = (uint32_t)(millis() - lastRead);
unsigned long ms = (unsigned long)(millis() - lastInit);
if (ms >= 4000) {
break;
}
}
return false;
return _connected;
}
/**
* @brief Check and read sensor data then update variable.
* Check result from method @isFailed before get value
* @brief Read PMS package send to device each 1sec
*
* @param serial
*/
void PMSBase::handle() {
uint32_t ms;
if (lastRead == 0) {
lastRead = millis();
if (lastRead == 0) {
lastRead = 1;
void PMSBase::readPackage(Stream *serial) {
/** If readPackage has process as period larger than READ_PACKAGE_TIMEOUT,
* should be clear the lastPackage and readBufferIndex */
if (lastReadPackage) {
unsigned long ms = (unsigned long)(millis() - lastReadPackage);
if (ms >= READ_PACKGE_TIMEOUT) {
/** Clear buffer */
readBufferIndex = 0;
/** Disable check read package timeout */
lastPackage = 0;
Serial.println("Last process timeout, clear buffer and last handle package");
}
lastReadPackage = millis();
if (!lastReadPackage) {
lastReadPackage = 1;
}
} else {
ms = (uint32_t)(millis() - lastRead);
/**
* The PMS in Active mode sends an update data every 1 second. If we read
* exactly every 1 sec then we may or may not get an update (depending on
* timing tolerances). Hence we read every 2.5 seconds and expect 2 ..3
* updates,
*/
if (ms < 2500) {
return;
lastReadPackage = millis();
if (!lastReadPackage) {
lastReadPackage = 1;
}
}
bool result = false;
char buf[32];
int bufIndex;
int step = 0;
int len = 0;
int bcount = 0;
while (stream->available()) {
char value = stream->read();
switch (step) {
case 0: {
/** Count to call delay() to release the while loop MCU resource for avoid the
* watchdog time reset */
uint8_t delayCount = 0;
while (serial->available()) {
/** Get value */
uint8_t value = (uint8_t)serial->read();
/** Process receiving package... */
switch (readBufferIndex) {
case 0: /** Start byte 1 */
if (value == 0x42) {
step = 1;
bufIndex = 0;
buf[bufIndex++] = value;
readBuffer[readBufferIndex++] = value;
}
break;
}
case 1: {
case 1: /** Start byte 2 */
if (value == 0x4d) {
step = 2;
buf[bufIndex++] = value;
// Serial.println("Got 0x4d");
readBuffer[readBufferIndex++] = value;
} else {
step = 0;
readBufferIndex = 0;
}
break;
}
case 2: {
buf[bufIndex++] = value;
if (bufIndex >= 4) {
len = toValue(&buf[2]);
if (len != 28) {
// Serial.printf("Got good bad len %d\r\n", len);
len += 4;
step = 3;
} else {
// Serial.println("Got good len");
step = 4;
case 2: /** Frame length */
if (value == 0x00) {
readBuffer[readBufferIndex++] = value;
} else {
readBufferIndex = 0;
}
break;
case 3: /** Frame length */
if (value == 0x1C) {
readBuffer[readBufferIndex++] = value;
} else {
readBufferIndex = 0;
}
break;
default: /** Data */
{
readBuffer[readBufferIndex++] = value;
/** Check that received full bufer */
if (readBufferIndex >= sizeof(readBuffer)) {
/** validata package */
if (validate(readBuffer)) {
_connected = true; /** Set connected status */
/** Parse data */
parse(readBuffer);
/** Set last received package */
lastPackage = millis();
if (lastPackage == 0) {
lastPackage = 1;
}
}
/** Clear buffer index */
readBufferIndex = 0;
}
break;
}
case 3: {
bufIndex++;
if (bufIndex >= len) {
step = 0;
// Serial.println("Bad lengh read all buffer");
}
break;
}
case 4: {
buf[bufIndex++] = value;
if (bufIndex >= 32) {
result |= validate(buf);
step = 0;
// Serial.println("Got data");
}
break;
}
default:
break;
}
// Reduce core panic: delay 1 ms each 32bytes data
bcount++;
if ((bcount % 32) == 0) {
/** Avoid task watchdog timer reset... */
delayCount++;
if (delayCount >= 32) {
delayCount = 0;
delay(1);
}
}
if (result) {
lastRead = millis();
if (lastRead == 0) {
lastRead = 1;
}
failed = false;
} else {
if (ms > 5000) {
failed = true;
/** Check that sensor removed */
if (lastPackage) {
unsigned long ms = (unsigned long)(millis() - lastPackage);
if (ms >= READ_PACKGE_TIMEOUT) {
lastPackage = 0;
_connected = false;
Serial.println("PMS disconnected");
}
}
}
/**
* @brief Check that PMS send is failed or disconnected
* @brief Increate number of fail
*
* @return true Failed
* @return false No problem
*/
bool PMSBase::isFailed(void) { return failed; }
void PMSBase::updateFailCount(void) {
if (failCount < failCountMax) {
failCount++;
}
}
void PMSBase::resetFailCount(void) { failCount = 0; }
/**
* @brief Get number of fail
*
* @return int
*/
int PMSBase::getFailCount(void) { return failCount; }
int PMSBase::getFailCountMax(void) { return failCountMax; }
/**
* @brief Read PMS 0.1 ug/m3 with CF = 1 PM estimates
*
* @return uint16_t
*/
uint16_t PMSBase::getRaw0_1(void) { return toValue(&package[4]); }
uint16_t PMSBase::getRaw0_1(void) { return pms_raw0_1; }
/**
* @brief Read PMS 2.5 ug/m3 with CF = 1 PM estimates
*
* @return uint16_t
*/
uint16_t PMSBase::getRaw2_5(void) { return toValue(&package[6]); }
uint16_t PMSBase::getRaw2_5(void) { return pms_raw2_5; }
/**
* @brief Read PMS 10 ug/m3 with CF = 1 PM estimates
*
* @return uint16_t
*/
uint16_t PMSBase::getRaw10(void) { return toValue(&package[8]); }
uint16_t PMSBase::getRaw10(void) { return pms_raw10; }
/**
* @brief Read PMS 0.1 ug/m3
*
* @return uint16_t
*/
uint16_t PMSBase::getPM0_1(void) { return toValue(&package[10]); }
uint16_t PMSBase::getPM0_1(void) { return pms_pm0_1; }
/**
* @brief Read PMS 2.5 ug/m3
*
* @return uint16_t
*/
uint16_t PMSBase::getPM2_5(void) { return toValue(&package[12]); }
uint16_t PMSBase::getPM2_5(void) { return pms_pm2_5; }
/**
* @brief Read PMS 10 ug/m3
*
* @return uint16_t
*/
uint16_t PMSBase::getPM10(void) { return toValue(&package[14]); }
uint16_t PMSBase::getPM10(void) { return pms_pm10; }
/**
* @brief Get numnber concentrations over 0.3 um/0.1L
*
* @return uint16_t
*/
uint16_t PMSBase::getCount0_3(void) { return toValue(&package[16]); }
uint16_t PMSBase::getCount0_3(void) { return pms_count0_3; }
/**
* @brief Get numnber concentrations over 0.5 um/0.1L
*
* @return uint16_t
*/
uint16_t PMSBase::getCount0_5(void) { return toValue(&package[18]); }
uint16_t PMSBase::getCount0_5(void) { return pms_count0_5; }
/**
* @brief Get numnber concentrations over 1.0 um/0.1L
*
* @return uint16_t
*/
uint16_t PMSBase::getCount1_0(void) { return toValue(&package[20]); }
uint16_t PMSBase::getCount1_0(void) { return pms_count1_0; }
/**
* @brief Get numnber concentrations over 2.5 um/0.1L
*
* @return uint16_t
*/
uint16_t PMSBase::getCount2_5(void) { return toValue(&package[22]); }
uint16_t PMSBase::getCount2_5(void) { return pms_count2_5; }
bool PMSBase::connected(void) { return _connected; }
/**
* @brief Get numnber concentrations over 5.0 um/0.1L (only PMS5003)
*
* @return uint16_t
*/
uint16_t PMSBase::getCount5_0(void) { return toValue(&package[24]); }
uint16_t PMSBase::getCount5_0(void) { return pms_count5_0; }
/**
* @brief Get numnber concentrations over 10.0 um/0.1L (only PMS5003)
*
* @return uint16_t
*/
uint16_t PMSBase::getCount10(void) { return toValue(&package[26]); }
uint16_t PMSBase::getCount10(void) { return pms_count10; }
/**
* @brief Get temperature (only PMS5003T)
*
* @return uint16_t
*/
uint16_t PMSBase::getTemp(void) { return toValue(&package[24]); }
int16_t PMSBase::getTemp(void) { return pms_temp; }
/**
* @brief Get humidity (only PMS5003T)
*
* @return uint16_t
*/
uint16_t PMSBase::getHum(void) { return toValue(&package[26]); }
uint16_t PMSBase::getHum(void) { return pms_hum; }
/**
* @brief Get firmware version code
*
* @return uint8_t
*/
uint8_t PMSBase::getFirmwareVersion(void) { return pms_firmwareVersion; }
/**
* @brief Ge PMS5003 error code
*
* @return uint8_t
*/
uint8_t PMSBase::getErrorCode(void) { return pms_errorCode; }
/**
* @brief Convert PMS2.5 to US AQI unit
@ -252,31 +298,118 @@ uint16_t PMSBase::getHum(void) { return toValue(&package[26]); }
* @return int
*/
int PMSBase::pm25ToAQI(int pm02) {
if (pm02 <= 12.0)
return ((50 - 0) / (12.0 - .0) * (pm02 - .0) + 0);
if (pm02 <= 9.0)
return ((50 - 0) / (9.0 - .0) * (pm02 - .0) + 0);
else if (pm02 <= 35.4)
return ((100 - 50) / (35.4 - 12.0) * (pm02 - 12.0) + 50);
return ((100 - 51) / (35.4 - 9.1) * (pm02 - 9.0) + 51);
else if (pm02 <= 55.4)
return ((150 - 100) / (55.4 - 35.4) * (pm02 - 35.4) + 100);
else if (pm02 <= 150.4)
return ((200 - 150) / (150.4 - 55.4) * (pm02 - 55.4) + 150);
else if (pm02 <= 250.4)
return ((300 - 200) / (250.4 - 150.4) * (pm02 - 150.4) + 200);
else if (pm02 <= 350.4)
return ((400 - 300) / (350.4 - 250.4) * (pm02 - 250.4) + 300);
else if (pm02 <= 500.4)
return ((500 - 400) / (500.4 - 350.4) * (pm02 - 350.4) + 400);
return ((150 - 101) / (55.4 - 35.5) * (pm02 - 35.5) + 101);
else if (pm02 <= 125.4)
return ((200 - 151) / (125.4 - 55.5) * (pm02 - 55.5) + 151);
else if (pm02 <= 225.4)
return ((300 - 201) / (225.4 - 125.5) * (pm02 - 125.5) + 201);
else if (pm02 <= 325.4)
return ((500 - 301) / (325.4 - 225.5) * (pm02 - 225.5) + 301);
else
return 500;
}
/**
* @brief SLR correction for PM2.5
*
* Reference: https://www.airgradient.com/blog/low-readings-from-pms5003/
*
* @param pm25 PM2.5 raw value
* @param pm003Count PM0.3 count
* @param scalingFactor Scaling factor
* @param intercept Intercept
* @return float Calibrated PM2.5 value
*/
float PMSBase::slrCorrection(float pm25, float pm003Count, float scalingFactor, float intercept) {
float calibrated;
float lowCalibrated = (scalingFactor * pm003Count) + intercept;
if (lowCalibrated < 31) {
calibrated = lowCalibrated;
} else {
calibrated = pm25;
}
// No negative value for pm2.5
if (calibrated < 0) {
return 0.0;
}
return calibrated;
}
/**
* @brief Correction PM2.5
*
* Formula: https://www.airgradient.com/documentation/correction-algorithms/
*
* @param pm25 Raw PM2.5 value
* @param humidity Humidity value (%)
* @return compensated pm25 value
*/
float PMSBase::compensate(float pm25, float humidity) {
float value;
// Correct invalid humidity value
if (humidity < 0) {
humidity = 0;
}
if (humidity > 100) {
humidity = 100.0f;
}
// If its already 0, do not proceed
if (pm25 == 0) {
return 0.0;
}
if (pm25 < 30) { /** pm2.5 < 30 */
value = (pm25 * 0.524f) - (humidity * 0.0862f) + 5.75f;
} else if (pm25 < 50) { /** 30 <= pm2.5 < 50 */
value = (0.786f * (pm25 * 0.05f - 1.5f) + 0.524f * (1.0f - (pm25 * 0.05f - 1.5f))) * pm25 -
(0.0862f * humidity) + 5.75f;
} else if (pm25 < 210) { /** 50 <= pm2.5 < 210 */
value = (0.786f * pm25) - (0.0862f * humidity) + 5.75f;
} else if (pm25 < 260) { /** 210 <= pm2.5 < 260 */
value = (0.69f * (pm25 * 0.02f - 4.2f) + 0.786f * (1.0f - (pm25 * 0.02f - 4.2f))) * pm25 -
(0.0862f * humidity * (1.0f - (pm25 * 0.02f - 4.2f))) +
(2.966f * (pm25 * 0.02f - 4.2f)) + (5.75f * (1.0f - (pm25 * 0.02f - 4.2f))) +
(8.84f * (1.e-4) * pm25 * pm25 * (pm25 * 0.02f - 4.2f));
} else { /** 260 <= pm2.5 */
value = 2.966f + (0.69f * pm25) + (8.84f * (1.e-4) * pm25 * pm25);
}
// No negative value for pm2.5
if (value < 0) {
return 0.0;
}
return value;
}
/**
* @brief Convert two byte value to uint16_t value
*
* @param buf bytes array (must be >= 2)
* @return uint16_t
* @return int16_t
*/
uint16_t PMSBase::toValue(char *buf) { return (buf[0] << 8) | buf[1]; }
int16_t PMSBase::toI16(const uint8_t *buf) {
int16_t value = buf[0];
value = (value << 8) | buf[1];
return value;
}
uint16_t PMSBase::toU16(const uint8_t *buf) {
uint16_t value = buf[0];
value = (value << 8) | buf[1];
return value;
}
/**
* @brief Validate package data
@ -285,16 +418,38 @@ uint16_t PMSBase::toValue(char *buf) { return (buf[0] << 8) | buf[1]; }
* @return true Success
* @return false Failed
*/
bool PMSBase::validate(char *buf) {
bool PMSBase::validate(const uint8_t *buf) {
uint16_t sum = 0;
for (int i = 0; i < 30; i++) {
sum += buf[i];
}
if (sum == toValue(&buf[30])) {
for (int i = 0; i < 32; i++) {
package[i] = buf[i];
}
if (sum == toU16(&buf[30])) {
return true;
}
return false;
}
void PMSBase::parse(const uint8_t *buf) {
// Standard particle
pms_raw0_1 = toU16(&buf[4]);
pms_raw2_5 = toU16(&buf[6]);
pms_raw10 = toU16(&buf[8]);
// atmospheric
pms_pm0_1 = toU16(&buf[10]);
pms_pm2_5 = toU16(&buf[12]);
pms_pm10 = toU16(&buf[14]);
// particle count
pms_count0_3 = toU16(&buf[16]);
pms_count0_5 = toU16(&buf[18]);
pms_count1_0 = toU16(&buf[20]);
pms_count2_5 = toU16(&buf[22]);
pms_count5_0 = toU16(&buf[24]); // PMS5003 only
pms_count10 = toU16(&buf[26]); // PMS5003 only
// Others
pms_temp = toU16(&buf[24]); // PMS5003T only
pms_hum = toU16(&buf[26]); // PMS5003T only
pms_firmwareVersion = buf[28];
pms_errorCode = buf[29];
}

View File

@ -3,11 +3,19 @@
#include <Arduino.h>
#define PMS_FAIL_COUNT_SET_INVALID 3
/**
* Known to work with these sensors: Plantower PMS5003, Plantower PMS5003, Cubic PM2009X
*/
class PMSBase {
public:
bool begin(Stream *stream);
void handle();
bool isFailed(void);
void readPackage(Stream *stream);
void updateFailCount(void);
void resetFailCount(void);
int getFailCount(void);
int getFailCountMax(void);
uint16_t getRaw0_1(void);
uint16_t getRaw2_5(void);
uint16_t getRaw10(void);
@ -18,26 +26,65 @@ public:
uint16_t getCount0_5(void);
uint16_t getCount1_0(void);
uint16_t getCount2_5(void);
bool connected(void);
/** For PMS5003 */
uint16_t getCount5_0(void);
uint16_t getCount10(void);
/** For PMS5003T*/
uint16_t getTemp(void);
int16_t getTemp(void);
uint16_t getHum(void);
uint8_t getFirmwareVersion(void);
uint8_t getErrorCode(void);
int pm25ToAQI(int pm02);
float slrCorrection(float pm25, float pm003Count, float scalingFactor, float intercept);
float compensate(float pm25, float humidity);
private:
Stream *stream;
char package[32];
int packageIndex;
bool failed = false;
uint32_t lastRead;
static const uint8_t package_size = 32;
uint16_t toValue(char *buf);
bool validate(char *buf);
/** In normal package interval is 200-800ms, In case small changed on sensor
* it's will interval reach to 2.3sec
*/
const uint16_t READ_PACKGE_TIMEOUT = 3000; /** ms */
const int failCountMax = 10;
int failCount = 0;
uint8_t readBuffer[package_size];
uint8_t readBufferIndex = 0;
/**
* Save last time received package success. 0 to disable check package
* timeout.
*/
unsigned long lastPackage = 0;
bool _connected;
unsigned long lastReadPackage = 0;
uint16_t pms_raw0_1;
uint16_t pms_raw2_5;
uint16_t pms_raw10;
uint16_t pms_pm0_1;
uint16_t pms_pm2_5;
uint16_t pms_pm10;
uint16_t pms_count0_3;
uint16_t pms_count0_5;
uint16_t pms_count1_0;
uint16_t pms_count2_5;
uint16_t pms_count5_0;
uint16_t pms_count10;
int16_t pms_temp;
uint16_t pms_hum;
uint8_t pms_errorCode;
uint8_t pms_firmwareVersion;
int16_t toI16(const uint8_t *buf);
uint16_t toU16(const uint8_t *buf);
bool validate(const uint8_t *buf);
void parse(const uint8_t* buf);
};
#endif /** _PMS5003_BASE_H_ */

View File

@ -1,8 +1,8 @@
#include "PMS5003.h"
#include "Arduino.h"
#include "../Main/utils.h"
#if defined(ESP8266)
#include <SoftwareSerial.h>
/**
* @brief Init sensor
*
@ -37,14 +37,11 @@ bool PMS5003::begin(HardwareSerial &serial) {
PMS5003::PMS5003(BoardType def) : _boardDef(def) {}
/**
* @brief Init sensor
*
* @return true Success
* @return false Failure
* Initializes the sensor.
*/
bool PMS5003::begin(void) {
if (this->_isBegin) {
AgLog("Initialized, call end() then try again");
AgLog("Already initialized, call end() then try again");
return true;
}
@ -62,11 +59,10 @@ bool PMS5003::begin(void) {
}
#if defined(ESP8266)
bsp->Pms5003.uart_tx_pin;
SoftwareSerial *uart =
this->_serial =
new SoftwareSerial(bsp->Pms5003.uart_tx_pin, bsp->Pms5003.uart_rx_pin);
uart->begin(9600);
if (pms.begin(uart) == false) {
this->_serial->begin(9600);
if (pms.begin(this->_serial) == false) {
AgLog("PMS failed");
return false;
}
@ -77,38 +73,96 @@ bool PMS5003::begin(void) {
return false;
}
#endif
_ver = pms.getFirmwareVersion();
this->_isBegin = true;
return true;
}
/**
* @brief Read PM1.0 must call this function after @ref readData success
* @brief Read PM1.0
*
* @return int PM1.0 index
* @return int PM1.0 index (atmospheric environment)
*/
int PMS5003::getPm01Ae(void) { return pms.getPM0_1(); }
/**
* @brief Read PM2.5 must call this function after @ref readData success
* @brief Read PM2.5
*
* @return int PM2.5 index
* @return int PM2.5 index (atmospheric environment)
*/
int PMS5003::getPm25Ae(void) { return pms.getPM2_5(); }
/**
* @brief Read PM10.0 must call this function after @ref readData success
* @brief Read PM10.0
*
* @return int PM10.0 index
* @return int PM10.0 index (atmospheric environment)
*/
int PMS5003::getPm10Ae(void) { return pms.getPM10(); }
/**
* @brief Read PM0.3 must call this function after @ref readData success
* @brief Read PM1.0
*
* @return int PM1.0 index (standard particle)
*/
int PMS5003::getPm01Sp(void) { return pms.getRaw0_1(); }
/**
* @brief Read PM2.5
*
* @return int PM2.5 index (standard particle)
*/
int PMS5003::getPm25Sp(void) { return pms.getRaw2_5(); }
/**
* @brief Read PM10
*
* @return int PM10 index (standard particle)
*/
int PMS5003::getPm10Sp(void) { return pms.getRaw10(); }
/**
* @brief Read particle 0.3 count
*
* @return int PM0.3 index
*/
int PMS5003::getPm03ParticleCount(void) { return pms.getCount0_3(); }
int PMS5003::getPm03ParticleCount(void) {
return pms.getCount0_3();
}
/**
* @brief Read particle 1.0 count
*
* @return int particle 1.0 count index
*/
int PMS5003::getPm01ParticleCount(void) { return pms.getCount1_0(); }
/**
* @brief Read particle 0.5 count
*
* @return int particle 0.5 count index
*/
int PMS5003::getPm05ParticleCount(void) { return pms.getCount0_5(); }
/**
* @brief Read particle 2.5 count
*
* @return int particle 2.5 count index
*/
int PMS5003::getPm25ParticleCount(void) { return pms.getCount2_5(); }
/**
* @brief Read particle 5.0 count
*
* @return int particle 5.0 count index
*/
int PMS5003::getPm5ParticleCount(void) { return pms.getCount5_0(); }
/**
* @brief Read particle 10 count
*
* @return int particle 10 count index
*/
int PMS5003::getPm10ParticleCount(void) { return pms.getCount10(); }
/**
* @brief Convert PM2.5 to US AQI
@ -118,6 +172,43 @@ int PMS5003::getPm03ParticleCount(void) { return pms.getCount0_3(); }
*/
int PMS5003::convertPm25ToUsAqi(int pm25) { return pms.pm25ToAQI(pm25); }
float PMS5003::slrCorrection(float pm25, float pm003Count, float scalingFactor, float intercept) {
return pms.slrCorrection(pm25, pm003Count, scalingFactor, intercept);
}
/**
* @brief Correct PM2.5
*
* Reference formula: https://www.airgradient.com/documentation/correction-algorithms/
*
* @param pm25 PM2.5 raw value
* @param humidity Humidity value
* @return compensated value in float
*/
float PMS5003::compensate(float pm25, float humidity) { return pms.compensate(pm25, humidity); }
/**
* @brief Get sensor firmware version
*
* @return int
*/
int PMS5003::getFirmwareVersion(void) { return _ver; }
/**
* @brief Get sensor error code
*
* @return uint8_t
*/
uint8_t PMS5003::getErrorCode(void) { return pms.getErrorCode(); }
/**
* @brief Is sensor connect with device
*
* @return true Connected
* @return false Removed
*/
bool PMS5003::connected(void) { return pms.connected(); }
/**
* @brief Check device initialized or not
*
@ -152,12 +243,26 @@ void PMS5003::end(void) {
* @brief Check and read PMS sensor data. This method should be callack from
* loop process to continoue check sensor data if it's available
*/
void PMS5003::handle(void) { pms.handle(); }
void PMS5003::handle(void) { pms.readPackage(this->_serial); }
void PMS5003::updateFailCount(void) {
pms.updateFailCount();
}
void PMS5003::resetFailCount(void) {
pms.resetFailCount();
}
/**
* @brief Get sensor status
* @brief Get number of fail count
*
* @return true No problem
* @return false Communication timeout or sensor has removed
* @return int
*/
bool PMS5003::isFailed(void) { return pms.isFailed(); }
int PMS5003::getFailCount(void) { return pms.getFailCount(); }
/**
* @brief Get number of fail count max
*
* @return int
*/
int PMS5003::getFailCountMax(void) { return pms.getFailCountMax(); }

View File

@ -4,6 +4,9 @@
#include "../Main/BoardDef.h"
#include "PMS.h"
#include "Stream.h"
#ifdef ESP8266
#include <SoftwareSerial.h>
#endif
/**
* @brief The class define how to handle PMS5003 sensor bas on @ref PMS class
@ -18,21 +21,43 @@ public:
#endif
void end(void);
void handle(void);
bool isFailed(void);
void updateFailCount(void);
void resetFailCount(void);
int getFailCount(void);
int getFailCountMax(void);
// Atmospheric environment
int getPm01Ae(void);
int getPm25Ae(void);
int getPm10Ae(void);
// Standard particle
int getPm01Sp(void);
int getPm25Sp(void);
int getPm10Sp(void);
// Particle count
int getPm03ParticleCount(void);
int getPm05ParticleCount(void);
int getPm01ParticleCount(void);
int getPm25ParticleCount(void);
int getPm5ParticleCount(void);
int getPm10ParticleCount(void);
int convertPm25ToUsAqi(int pm25);
float slrCorrection(float pm25, float pm003Count, float scalingFactor, float intercept);
float compensate(float pm25, float humidity);
int getFirmwareVersion(void);
uint8_t getErrorCode(void);
bool connected(void);
private:
bool _isBegin = false;
int _ver;
BoardType _boardDef;
PMSBase pms;
const BoardDef *bsp;
#if defined(ESP8266)
Stream *_debugStream;
const char *TAG = "PMS5003";
SoftwareSerial *_serial;
#else
HardwareSerial *_serial;
#endif

View File

@ -1,5 +1,6 @@
#include "PMS5003T.h"
#include "Arduino.h"
#include "../Main/utils.h"
#if defined(ESP8266)
#include <SoftwareSerial.h>
@ -66,11 +67,10 @@ bool PMS5003T::begin(void) {
}
#if defined(ESP8266)
bsp->Pms5003.uart_tx_pin;
SoftwareSerial *uart =
this->_serial =
new SoftwareSerial(bsp->Pms5003.uart_tx_pin, bsp->Pms5003.uart_rx_pin);
uart->begin(9600);
if (pms.begin(uart) == false) {
this->_serial->begin(9600);
if (pms.begin(this->_serial) == false) {
AgLog("PMS failed");
return false;
}
@ -102,38 +102,82 @@ bool PMS5003T::begin(void) {
return false;
}
#endif
_ver = pms.getFirmwareVersion();
this->_isBegin = true;
return true;
}
/**
* @brief Read PM1.0 must call this function after @ref readData success
* @brief Read PM1.0
*
* @return int PM1.0 index
* @return int PM1.0 index (atmospheric environment)
*/
int PMS5003T::getPm01Ae(void) { return pms.getPM0_1(); }
/**
* @brief Read PM2.5 must call this function after @ref readData success
* @brief Read PM2.5
*
* @return int PM2.5 index
* @return int PM2.5 index (atmospheric environment)
*/
int PMS5003T::getPm25Ae(void) { return pms.getPM2_5(); }
/**
* @brief Read PM10.0 must call this function after @ref readData success
* @brief Read PM10.0
*
* @return int PM10.0 index
* @return int PM10.0 index (atmospheric environment)
*/
int PMS5003T::getPm10Ae(void) { return pms.getPM10(); }
/**
* @brief Read PM 0.3 Count must call this function after @ref readData success
* @brief Read PM1.0
*
* @return int PM 0.3 Count index
* @return int PM1.0 index (standard particle)
*/
int PMS5003T::getPm03ParticleCount(void) { return pms.getCount0_3(); }
int PMS5003T::getPm01Sp(void) { return pms.getRaw0_1(); }
/**
* @brief Read PM2.5
*
* @return int PM2.5 index (standard particle)
*/
int PMS5003T::getPm25Sp(void) { return pms.getRaw2_5(); }
/**
* @brief Read PM10
*
* @return int PM10 index (standard particle)
*/
int PMS5003T::getPm10Sp(void) { return pms.getRaw10(); }
/**
* @brief Read particle 0.3 count
*
* @return int particle 0.3 count index
*/
int PMS5003T::getPm03ParticleCount(void) {
return pms.getCount0_3();
}
/**
* @brief Read particle 0.5 count
*
* @return int particle 0.5 count index
*/
int PMS5003T::getPm05ParticleCount(void) { return pms.getCount0_5(); }
/**
* @brief Read particle 1.0 count
*
* @return int particle 1.0 count index
*/
int PMS5003T::getPm01ParticleCount(void) { return pms.getCount1_0(); }
/**
* @brief Read particle 2.5 count
*
* @return int particle 2.5 count index
*/
int PMS5003T::getPm25ParticleCount(void) { return pms.getCount2_5(); }
/**
* @brief Convert PM2.5 to US AQI
@ -149,7 +193,7 @@ int PMS5003T::convertPm25ToUsAqi(int pm25) { return pms.pm25ToAQI(pm25); }
* @return float Degree Celcius
*/
float PMS5003T::getTemperature(void) {
return pms.getTemp()/10.0f;
return pms.getTemp() / 10.0f;
}
/**
@ -158,9 +202,42 @@ float PMS5003T::getTemperature(void) {
* @return float Percent (%)
*/
float PMS5003T::getRelativeHumidity(void) {
return pms.getHum()/10.0f;
return pms.getHum() / 10.0f;
}
/**
* @brief Correct PM2.5
*
* Reference formula: https://www.airgradient.com/documentation/correction-algorithms/
*
* @param pm25 PM2.5 raw value
* @param humidity Humidity value
* @return compensated value
*/
float PMS5003T::compensate(float pm25, float humidity) { return pms.compensate(pm25, humidity); }
/**
* @brief Get module(s) firmware version
*
* @return int Version code
*/
int PMS5003T::getFirmwareVersion(void) { return _ver; }
/**
* @brief Get sensor error code
*
* @return uint8_t
*/
uint8_t PMS5003T::getErrorCode(void) { return pms.getErrorCode(); }
/**
* @brief Is sensor connect to device
*
* @return true Connected
* @return false Removed
*/
bool PMS5003T::connected(void) { return pms.connected(); }
/**
* @brief Check device initialized or not
*
@ -192,13 +269,26 @@ void PMS5003T::end(void) {
* @brief Check and read PMS sensor data. This method should be callack from
* loop process to continoue check sensor data if it's available
*/
void PMS5003T::handle(void) { pms.handle(); }
void PMS5003T::handle(void) { pms.readPackage(this->_serial); }
void PMS5003T::updateFailCount(void) {
pms.updateFailCount();
}
void PMS5003T::resetFailCount(void) {
pms.resetFailCount();
}
/**
* @brief Get sensor status
*
* @return true No problem
* @return false Communication timeout or sensor has removed
* @brief Get fail count
*
* @return int
*/
bool PMS5003T::isFailed(void) { return pms.isFailed(); }
int PMS5003T::getFailCount(void) { return pms.getFailCount(); }
/**
* @brief Get fail count max
*
* @return int
*/
int PMS5003T::getFailCountMax(void) { return pms.getFailCountMax(); }

View File

@ -6,6 +6,9 @@
#include "PMS5003TBase.h"
#include "Stream.h"
#include <HardwareSerial.h>
#ifdef ESP8266
#include <SoftwareSerial.h>
#endif
/**
* @brief The class define how to handle PMS5003T sensor bas on @ref PMS class
@ -21,24 +24,43 @@ public:
void end(void);
void handle(void);
bool isFailed(void);
void updateFailCount(void);
void resetFailCount(void);
int getFailCount(void);
int getFailCountMax(void);
// Atmospheric environment
int getPm01Ae(void);
int getPm25Ae(void);
int getPm10Ae(void);
// Standard particle
int getPm01Sp(void);
int getPm25Sp(void);
int getPm10Sp(void);
// Particle count
int getPm03ParticleCount(void);
int getPm05ParticleCount(void);
int getPm01ParticleCount(void);
int getPm25ParticleCount(void);
int convertPm25ToUsAqi(int pm25);
float getTemperature(void);
float getRelativeHumidity(void);
float compensate(float pm25, float humidity);
int getFirmwareVersion(void);
uint8_t getErrorCode(void);
bool connected(void);
private:
bool _isBegin = false;
bool _isSleep = false;
int _ver; /** Firmware version code */
BoardType _boardDef;
const BoardDef *bsp;
#if defined(ESP8266)
Stream *_debugStream;
const char *TAG = "PMS5003T";
SoftwareSerial *_serial;
#else
HardwareSerial *_serial;
#endif

View File

@ -4,14 +4,30 @@ PMS5003TBase::PMS5003TBase() {}
PMS5003TBase::~PMS5003TBase() {}
float PMS5003TBase::temperatureCompensated(float temp) {
/**
* @brief Compensate the temperature
*
* Reference formula: https://www.airgradient.com/documentation/correction-algorithms/
*
* @param temp
* @return * float
*/
float PMS5003TBase::compensateTemp(float temp) {
if (temp < 10.0f) {
return temp * 1.327f - 6.738f;
}
return temp * 1.181f - 5.113f;
}
float PMS5003TBase::humidityCompensated(float hum) {
/**
* @brief Compensate the humidity
*
* Reference formula: https://www.airgradient.com/documentation/correction-algorithms/
*
* @param temp
* @return * float
*/
float PMS5003TBase::compensateHum(float hum) {
hum = hum * 1.259f + 7.34f;
if (hum > 100.0f) {

View File

@ -8,8 +8,8 @@ private:
public:
PMS5003TBase();
~PMS5003TBase();
float temperatureCompensated(float temp);
float humidityCompensated(float hum);
float compensateTemp(float temp);
float compensateHum(float hum);
};
#endif

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