-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathFijnstof_display_131.ino
2307 lines (2174 loc) · 105 KB
/
Fijnstof_display_131.ino
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Stable multi-config version.
#define VERSION "v1.31 Connecting..." // firmware version + connecting string
//=========================================================================
// This program's author:
// Copyright (C) 2019/2024 Rik Drabs - [email protected]
// Assestraat 55, 1700 Dilbeek - Belgium
//=========================================================================
// This program may be used freely under the GNU General Public Licence,
// for bringing the particulate matter air pollution problem in the
// attention of the general public. When used, this program is WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// Text till this line should not be removed.
//=========================================================================
// Large Bargraph Remote Display PM10 & PM2.5 for:
// Luftdaten.info - sensor.community - InfluencAir - LUCHTPIJP.
// Display on LEDmatrix 16*8*8 dots
//
// Start of project: 16/09/2019
// Program originally written for project LUCHTPIJP, an initiative of:
// Beweging.net - CM (Christelijke Mutualiteit) - Bank BELFIUS
//
// LUCHTPIJP is the 2018 successor of project InfluencAir,
// an initiative of: Meetup group CIVIC LAB BRUSSELS, operating under
// the umbrella of OPEN KNOWLEDGE BELGIUM.
//
// InfluencAir followed the Stuttgart project Luftdaten.info in 2017.
//
// The Luftdaten.info project is more recently known as sensor.community.
// Sensor.community is a project of OK Lab Stuttgart under the umbrella of
// OPEN KNOWLEDGE GERMANY.
//=========================================================================
// Parts list:
// ===========
// 4 pieces display block 4 units 8*8 pixels.
// 1 piece MCU
// 1,5 meter ALU profile to fix the display. (see pictures)
// 1 roll transparent doublesided tape. (tesa powerbond 55744-00001)
//
// Hardware construction:
// ======================
// The free connector on the first display block (input to the whole
// display) must be removed, and a pin row (5 pins) must be soldered
// vertically on the back of the PCB in its place.(see pictures)
// A second pin row (4 pins) must be soldered to the output of the
// second display matrix 8*8 (see pictures for alignment !!)
//
// Mount the MCU on an InfluencAir display PCB, and solder it to the
// pins on the back of the first display block 4*8*8.
// (see pictures)
//
// Now cascade the 4 display blocks, by bending the pins of 3
// of them, and soldering them in cascade, so that a level row of
// 4 by 4 display blocks is created.(see pictures)
//
// Finally check that the display blocks are mounted flat, and correct
// flatness if needed. Then clean, fix to a flat surface, and tape with
// double sided tape to the profile. (see pictures)
//
// After mounting, modify config.data.h and/or multi_config.h to use
// your parameters, flash the program, and enjoy.
//
// Software adaptation instructions:
// ---------------------------------
// To adapt the WiFi login, see function "initWiFi".
// To modify the language, welcome message and coupled sensors, see config_data.h.
// To modify the lines shown on display, see function "readAndBuild".
// To modify timings and repeats on the display, see "Display timing".
// When garbage is on the display, modify the hardware type of the display in config_data.h.
//========================================================================================
// List of supported languages. Feel free to add your own language, and share with us!
// DUTCH, FRENCH, ENGLISH, GERMAN, ITALIAN.
//========================================================================================
// Header files, libraries
#include <Arduino.h>
#include <ESP8266WebServer.h> // ESP8266 Web Server
#include <ESP8266WiFi.h> // ESP8266 WiFi
#include <MD_Parola.h> // Parola Library
#include <MD_MAX72XX.h> // Max72xx Library
#include <SPI.h> // SPI Library
#include <ESP8266HTTPClient.h> // ESP8266 HTTP Client
#include <ESP8266WiFiMulti.h> // ESP8266 WiFi multiple AP login
#include "EEPROM.h" // Eeprom library
#include "8266_ntp.h" // NTP routines
#include "parola_fonts.h" // Parola adapted fonts
#include "config_data.h" // Local configuration data
#include "multi_config.h" // Multi configuration data
#include "setup_page.h" // Web Setup Page
// DEBUG defines
#define DEBUG 0 // 0 = normal; 1 = debug
// Display repeat setting
#define REPEAT_COUNT 2
// Define website with data
#define SENSORSERVER_URL "http://data.sensor.community/airrohr/v1/sensor/" // server address with data
// Use terminal on serial-line/USB for debugging
#if DEBUG
#define PRINT(s, x) {Serial.print(F(s)); Serial.print(x);}
#define PRINTS(s) {Serial.print(s);}
#else
#define PRINT(s, x)
#define PRINTS(s)
#endif
// Define the PM10 & PM2,5 maxima according to WHO
#define PM10_MAX 15 // PM10 maximum = 15µg/m³
#define PM25_MAX 5 // PM2.5 maximum = 5µg/m³
#define MXV_PM10 30
#define MXV_PM25 10
// Define the PM10 & PM2,5 values for AQI = 50
#define AQI10_UG 54 // PM10 54µg/m³ = 50 AQI
#define AQI25_UG 12 // PM2,5 12µg/m³ = 50 AQI
// Define Web Server
ESP8266WebServer server(80); // web server on port 80
// Define WiFi multi login interface
ESP8266WiFiMulti WiFiMulti;
// Display modes
#define SCROLL_ONEPIXEL 'S' // Scrolling, with one pixel between characters (for showing separate chars)
#define SCROLL_NOPIXEL 's' // Scrolling, with zero pixels between characters (for showing graphics)
#define FIXED_ONEPIXEL 'F' // Fixed left justified, with one pixel between characters (for showing separate chars)
#define FIXED_ONEPIXEL_T 'T' // Fixed left justified, with one pixel between characters (for showing separate chars) + Update time msg. before display
#define FIXED_NOPIXEL 'f' // Fixed left justified, with zero pixels between characters (for showing graphics)
// PM string ID defines
#define STRING_PM10 0 // String "PM10"
#define STRING_PM25 1 // String "PM2,5"
#define STRING_UGRAM3 2 // String "µg/m³"
#define STRING_DROPLET 3 // String droplet sign
#define STRING_THERMO 4 // String thermometer sign
#define STRING_DEGREES 5 // String degrees celcius
#define STRING_PM10_EQ 6 // String "PM10 waarde = "
#define STRING_PM25_EQ 7 // String "PM2,5 waarde = "
#define STRING_TEMP_EQ 8 // String "Temperatuur = "
#define STRING_VOCHT_EQ 9 // String "Vochtigheid = "
#define BINNEN 10 // String "Binnen"
#define BUITEN 11 // String "Buiten"
#define PERCNTSTR0 12 // String "Er is " - meer
#define PERCNTSTR1 13 // String "Er is " - minder
#define PERCNTSTR2 14 // String "% meer "
#define PERCNTSTR3 15 // String "% minder "
#define PERCNTSTR4 16 // String " dan bij meter "
#define PERCNTSTR5 17 // String " fijnstof bij meter "
#define READSERVER 18 // String "Leest gegevens..."
#define ERRORNODATA 19 // String " geen nieuwe gegevens."
#define STRING_MAX24 20 // String "Maximumwaarde 24h. "
#define STRING_MEAN 21 // String "Gemiddelde over alle meters "
#define STRING_TEHOOG 22 // String "Overschrijding op meter "
#define STRING_NODATA 23 // String "Geen resultaten van "
#define NULLSTR 24 // String ""
// Display keyword ID defines
#define SHOW_ALPHA 1 // Show alphanumerical result
#define SHOW_GRAPH 2 // Show graphical result
#define SHOW_VALUE 3 // Show value after fixed text
#define SHOW_COMPAR 4 // Show indoor/outdoor comparison in text
#define SHOW_PERCNT 5 // Show outdoor1/outdoor2 comparison in percent
#define SHOW_ADVICE 6 // Show AQI advice about outdoor air quality
#define SHOW_MAX24 7 // Show 24h maximum value
#define SHOW_TIME 8 // Show actual time
// Data selector PM10/PM2,5/Temp/Humi
#define OUT_PM10 1 // Select outdoor PM10 data
#define OUT_PM25 2 // Select outdoor PM2,5 data
#define OUT_TEMP 3 // Select outdoor temperature data
#define OUT_HUMI 4 // Select outdoor humidity data
#define IN_PM10 5 // Select indoor PM10 data
#define IN_PM25 6 // Select indoor PM2,5 data
#define PM10_PM25 7 // Select outdoor PM10 & PM2,5 data combined
// Global variables
// ================
// General advice when adding to or adapting the program:
// Use global variables instead of local ones, since the stack of the ESP8266 is
// only 4K and nearly full, and that's the place where local variables are stored.
// Adding more local variables increases the risc for crashes dramatically.
// I know, using only globals is normally no good programming practice...
//
float pm10 = -1000.0; // receptor for PM10
float pm25 = -1000.0; // receptor for PM2,5
float temperature = -1000.0; // receptor for temperature
float humidity = -1000.0; // receptor for humidity
char timestamp[21] = "2020-12-31 23:59:59"; // receptor for timestamp
uint8_t actualHour = 24; // actual hour of measuring period
//
float outdoorPm10 = -1000.0; // receptor for outdoor PM10
float outdoorPm25 = -1000.0; // receptor for outdoor PM2,5
float outdoorTemperature = -1000.0; // receptor for outdoor temperature
float outdoorHumidity = -1000.0; // receptor for outdoor humidity
char outdoorTimestamp[21] = "2020-12-31 23:59:59"; // receptor for outdoor timestamp
char outdoorName[50]; // name of outdoor sensor 1
//
float indoorPm10 = -1000.0; // receptor for indoor PM10
float indoorPm25 = -1000.0; // receptor for indoor PM2,5
float indoorTemperature = -1000.0; // receptor for indoor temperature
float indoorHumidity = -1000.0; // receptor for indoor humidity
char indoorTimestamp[21] = "2020-12-31 23:59:59"; // receptor for indoor timestamp
char indoorName[50]; // name of indoor sensor 2
//
float multiPm10[MULTI_MAX]; // receptor array PM10 multi configuration
float multiPm25[MULTI_MAX]; // receptor array PM2,5 multi configuration
float meanPm10 = 0; // mean PM10 value in array
float meanPm10Max = 0; // mean PM10 maximum value in array
float meanPm25 = 0; // mean PM2,5 value in array
float meanPm25Max = 0; // mean PM2,5 maximum value in array
uint8_t multiLine = 0; // message line for multi configuration
//
uint8_t i; // general variables
uint8_t j; // ..
uint8_t k; // ..
uint8_t m; // ..
//
float compareResult = 0.0; // result of comparing indoor & outdoor
float posCompareResult = 0.0; // compare result for printing purposes - always positive
bool resultFound = false; // result found marker
//
uint8_t messageSelect = 0; // active message selector
uint8_t barMessage = 0; // bar message on display while rereading data
uint8_t repeatCount = REPEAT_COUNT; // repeat counter
//
char formatChar; // separated format character in message
char sds011url[70]; // sds url build string
char dht22url[70]; // dht url build string
char displayIP[50]; // ip-address of display
char forDisplay[50]; // string for display
char workString[50]; // work string
char displayString[150]; // display string
//
float pm10_Max[25]; // array for storing the hourly maximum values PM10
float pm25_Max[25]; // array for storing the hourly maximum values PM2,5
float pm10_Max24 = 0; // actual 24h maximum PM10
float pm25_Max24 = 0; // actual 24h maximum PM2,5
// Global defines
WiFiClient client;
HTTPClient http;
// Define text strings for messages
char *messageText[] =
{
" PM10: ", // PM10: string
" PM2,5: ", // PM2,5: string
"µg/m³", // µg/m³ string
" ñ ", // droplet sign
" Ñ ", // thermometer sign
"°C", // degrees celsius
//
#ifdef DUTCH
"PM10 waarde = ",
"PM2,5 waarde = ",
"Temperatuur = ",
"Vochtigheid = ",
"Binnen",
"Buiten",
"Er is ",
"Er is ",
"% méér ",
"% minder ",
" dan bij meter ",
" fijnstof bij meter ",
"Leest gegevens...",
" geen nieuwe gegevens.",
"Piek 24u.",
"Gemiddelde over alle meters: ",
"In overschrijding: ",
"Geen gegevens:",
"",
#endif
#ifdef FRENCH
"Valeur PM10 = ",
"Valeur PM2,5 = ",
"Température = ",
"Humidité = ",
"A l'intérieur",
"A l'extérieur",
"Il y a ",
"Il y a ",
"% plus de particules fines ",
"% moins de particules fines ",
" qu'aux environs de ",
" aux environs de ",
"Lecture de données...",
" pas de nouvelles données.",
"Crête 24h.",
"Moyenne de toutes les mesures: ",
"En dépassement: ",
"Pas de données:",
"",
#endif
#ifdef ENGLISH
"PM10 value = ",
"PM2,5 value = ",
"Temperature = ",
"Humidity = ",
"Inside",
"Outside",
"There is ",
"There is ",
"% more particulate matter ",
"% less particulate matter ",
" than in the vicinity of ",
" in the vicinity of ",
"Reading data...",
" no new data.",
"Peak 24h.",
"Mean of all measurements: ",
"In exceedance: ",
"No data:",
"",
#endif
#ifdef GERMAN
"PM10 Werte = ",
"PM2,5 Werte = ",
"Temperatur = ",
"Feuchtigkeit = ",
"Innen",
"Aussen",
"Es gibt ",
"Es gibt ",
"% mehr ",
"% weniger ",
" als am Meter ",
" Feinstaub am Meter ",
"Lesen von Daten...",
" keine neue Daten.",
"Spitz 24h.",
"Durchschnitt über alle Meter: ",
"In uberschreitung: ",
"Keine daten:",
"",
#endif
#ifdef ITALIAN
"Valore PM10 = ",
"Valore PM2,5 = ",
"Temperatura = ",
"Umidità = ",
"Interno",
"Esterno",
"C'è il ",
"C'è materia il ",
"% in più",
"% in meno ",
" che in metri a ",
" di particolato in metri a ",
"Leggere i dati...",
" nessun nuovo dato.",
"Picco 24h.",
"Media su tutti i metri: ",
"In eccesso: ",
"Nessun dato:",
"",
#endif
};
// Damping factor for small values PM
// Damping is added to both PM values before comparing to % result.
#define DAMPING 10 // minimum 0 - maximum 20
// Table driven comparative system
float compareTable[] =
{
//==============================
-100, // Outdoor = -100%/-35%
-35, // Open windows
//==============================
-35, // Outdoor = -35%/-10%
-10, // No advice for windows
//==============================
-10, // Outdoor = -10%/+10%
+10, // No advice for windows
//==============================
+10, // Outdoor = +10%/+35%
+35, // No advice for windows
//==============================
+35, // Outdoor = +35%/+100%
+100, // Close windows
//==============================
+100, // Outdoor = >+100%
+10000, // Close windows
//==============================
};
// Text strings for comparative system
char *compareText[] =
{
#ifdef DUTCH
//"0 1 2 3 4 5 6 7 8 9 0 1"
//"012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"
//"==============================================================================================================="
"De kwaliteit van de binnenlucht is slechter dan die van de buitenlucht - Verluchten!", // Outdoor -100%/-30%
"De kwaliteit van de binnenlucht is wat slechter dan die van de buitenlucht.", // Outdoor -30%/-10%
"De kwaliteit van de binnen- en buitenlucht is ongeveer gelijk.", // Outdoor -10%/+10%
"De kwaliteit van de binnenlucht is wat beter dan die van de buitenlucht.", // Outdoor +10%/+30%
"De kwaliteit van de binnenlucht is beter dan die van de buitenlucht - Ramen en deuren sluiten!", // Outdoor +30%/+100%
"De kwaliteit van de binnenlucht is veel beter dan die van de buitenlucht - Ramen en deuren sluiten!", // Outdoor >100%
#endif
#ifdef FRENCH
//"==============================================================================================================="
"La qualité de l'air interieure est plus mauvaise que celle de l'air exterieure - Ventiler!", // Outdoor -100%/-30%
"La qualité de l'air interieure est un peu plus mauvaise que celle de l'air exterieure.", // Outdoor -30%/-10%
"Les qualités de l'air interieure et de l'air exterieure sont à peu près égales.", // Outdoor -10%/+10%
"La qualité de l'air interieure est un peu plus meilleure que celle de l'air extérieure.", // Outdoor +10%/+30%
"La qualité de l'air interieure est meilleure que celle de l'air extérieure - Fermer portes et fenêtres!", // Outdoor +30%/+100%
"La qualité de l'air interieure est meilleure que celle de l'air extérieure - Fermer portes et fenêtres!", // Outdoor >100%
#endif
#ifdef ENGLISH
//"==============================================================================================================="
"The interior air is of worse quality than the exterior air - Aerate!", // Outdoor -100%/-30%
"The interior air is of somewhat worse quality than the exterior air.", // Outdoor -30%/-10%
"The quality of interior air and exterior air is approximately equal.", // Outdoor -10%/+10%
"The interior air is of somewhat better quality than the exterior air.", // Outdoor +10%/+30%
"The interior air is of better quality than the exterior air - Close doors and windows!", // Outdoor +30%/+100%
"The interior air is of much better quality than the exterior air - Close doors and windows!", // Outdoor >100%
#endif
#ifdef GERMAN
//"==============================================================================================================="
"Die Luftqualität der Innenluft ist schlechter als die der Außenluft – Lüften!", // Outdoor -100%/-30%
"Die Luftqualität der Innenluft ist etwas schlechter als die der Außenluft.", // Outdoor -30%/-10%
"Die Luftqualität der Innen- und Außenluft ist etwa gleich.", // Outdoor -10%/+10%
"Die Luftqualität der Innenluft ist etwas besser als die der Außenluft.", // Outdoor +10%/+30%
"Die Luftqualität der Innenluft ist besser als die der Außenluft – Fenster und Türen schließen!", // Outdoor +30%/+100%
"Die Luftqualität der Innenluft ist viel besser als die der Außenluft – Fenster und Türen schließen!", // Outdoor >100%
#endif
#ifdef ITALIAN
//"==============================================================================================================="
"La qualità dell'aria interna è peggiore di quella dell'aria esterna - Ventilare!", // Outdoor -100%/-30%
"La qualità dell'aria interna è leggermente peggiore di quella dell'aria esterna.", // Outdoor -30%/-10%
"La qualità dell'aria interna ed esterna è più o meno la stessa.", // Outdoor -10%/+10%
"La qualità dell'aria interna è leggermente migliore di quella dell'aria esterna.", // Outdoor +10%/+30%
"La qualità dell'aria interna è migliore dell'aria esterna: chiudi finestre e porte!", // Outdoor +30%/+100%
"La qualità dell'aria interna è molto migliore di quella dell'aria esterna - Chiudi finestre e porte!", // Outdoor >100%
#endif
};
// Table driven WHO/AQI warning system
// Based on info at https://aqicn.org/sensor/sds011/
// http://www.airqualitynow.eu/about_indices_definition.php
// https://www.airnow.gov/index.cfm?action=aqibasics.aqi
// https://www.airnow.gov/aqi/aqi-calculator/
//
float AQItable[] =
{
//==============================
+0, // AQI 0 to 50
+50, // "CODE GREEN: Good air quality - no risc"
//==============================
+51, // AQI 51 to 100
+100, // "CODE YELLOW: Moderate air quality / WHO norm exceeded"
//==============================
+101, // AQI 101 to 150
+150, // "CODE ORANGE: Unhealthy for sensitive groups"
//==============================
+151, // From 31 to 40 (AQI 151 to 200)
+200, // "CODE RED: Unhealthy"
//==============================
+201, // From 41 to 75 (AQI 201 to 300)
+300, // "CODE VIOLET: Very unhealty"
//==============================
+301, // From 76 to 10000 (AQI 300+)
+30000, // "CODE BROWN: Hazardous"
//==============================
};
// Text strings for WHO/AQI warning system
// Based on info at https://aqicn.org/sensor/sds011/
// and http://www.airqualitynow.eu/about_indices_definition.php
//
char *AQItext[] =
{
#ifdef DUTCH
//"0 1 2 3 4 5 6 7 8 9 0 1"
//"012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"
//"==============================================================================================================="
"Code GROEN: De buitenluchtkwaliteit is goed. Er zijn geen gezondheidsrisico's.", // Code green
"Code GEEL: De buitenluchtkwaliteit is matig. De WHO norm is overschreden.", // Code yellow
"Code ORANJE: De buitenlucht is ongezond voor kwetsbare groepen.", // Code orange
"Code ROOD: De buitenlucht is ongezond. Iedereen kan gezondheidseffecten ondervinden.", // Code red
"Code VIOLET: De buitenlucht is zeer ongezond. Iedereen kan gezondheidseffecten ondervinden.", // Code violet
"Code BRUIN: De buitenlucht is gevaarlijk. Iedereen kan ernstige gevolgen voor de gezondheid ervaren.", // Code brown
#endif
#ifdef FRENCH
//"==============================================================================================================="
"Code VERT: La qualité de l'air extérieur est bonne. Il n'y a pas de risque pour la santé.", // Code green
"Code JAUNE: La qualité de l'air extérieur est médiocre. Les normes OMS sont dépassées.", // Code yellow
"Code ORANGE: La qualité de l'air extérieur peut provoquer des problèmes de santé aux personnes sensibles.", // Code orange
"Code ROUGE: La qualité de l'air extérieur est malsaine. Tout le monde peut subir des effets de santé.", // Code red
"Code VIOLET: La qualité de l'air extérieur est très malsaine. Tout le monde peut subir des effets de santé.", // Code violet
"Code MARON: La qualité de l'air extérieur est dangereuse. Tout le monde peut subir des effets de santé graves", // Code brown
#endif
#ifdef ENGLISH
//"==============================================================================================================="
"Code GREEN: The exterior air quality is satisfactory, and adverse health effects are not expected.", // Code green
"Code YELLOW: The exterior air quality is moderate. WHO pollution limits are breached.", // Code yellow
"Code ORANGE: The exterior air is unhealthy for sensitive groups.", // Code orange
"Code RED: The exterior air is unhealthy. Everyone may experience health effects.", // Code red
"Code VIOLET: The exterior air is very unhealthy. Everyone may experience health effects.", // Code violet
"Code BROWN: The exterior air is hazardous. Everyone may experience more serious health effects", // Code brown
#endif
#ifdef GERMAN
//"==============================================================================================================="
"Code GRÜN: Die Außenluftqualität ist gut. Es bestehen keine Gesundheitsrisiken.", // Code green
"Code GELB: Die Außenluftqualität ist mäßig. Der WHO-Standard wurde übertroffen.", // Code yellow
"Code ORANGE: Die Außenluft ist für gefährdete Personen ungesund.", // Code orange
"Code ROT: Die Außenluft ist ungesund. Gesundheitsschäden können bei jedem auftreten.", // Code red
"Code VIOLETT: Die Außenluft ist sehr ungesund. Gesundheitsschäden können bei jedem auftreten.", // Code violet
"Code BRAUN: Die Außenluft ist gefährlich. Jeder kann schwerwiegende gesundheitliche Auswirkungen haben.", // Code brown
#endif
#ifdef ITALIAN
//"==============================================================================================================="
"Codice VERDE: La qualità dell'aria esterna è buona. Non ci sono rischi per la salute.", // Code green
"Codice GIALLO: La qualità dell'aria esterna è moderata. Lo standard dell'OMS è stato superato.", // Code yellow
"Codice ARANCIONE: L'aria esterna non è salubre per le persone vulnerabili gruppi.", // Code orange
"Codice ROSSO: L'aria esterna è malsana. Chiunque può riscontrare effetti sulla salute.", // Code red
"Codice VIOLA: L'aria esterna è molto malsana. Chiunque può riscontrare effetti sulla salute.", // Code violet
"Codice MARRONE: L'aria esterna è pericolosa Chiunque può sperimentare gravi effetti sulla salute.", // Code brown
#endif
};
// This is the messagebuffer. It receives the new lines to display from received data,
// or holds previous results for display, if received data is temporarily unavailable.
// Messages are constructed according to definition in the "readAndBuild" function.
// You can also place fixed messages directly in the buffer, to be displayed.
// Messages here must start with S for scrolling, or with F for fixed format.
// Use lowercase (s, f) for characters without intermediate pixel column (graphics, etc.)
// A maximum of 26 messages is provided for. Feel free to expand the buffer!
// messagebuffer array size defines
#define NUMSTR 26 // number of strings in messagebuffer array
#define NUMCHR 120 // number of stringcharacters in messagebuffer array (= 110 + 10 spare!)
char message[NUMSTR][NUMCHR] = // message buffer
{
//"0 1 2 3 4 5 6 7 8 9 0 1"
//"012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"
//"==============================================================================================================="
WELCOME_MESSAGE, // 0 Title - welcome message
" ",// 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
};
//"0 1 2 3 4 5 6 7 8 9 0 1"
//"012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"
// This is the message valid & time-out array
// A zero means the corresponding message is not valid (anymore) and should not be shown
#if DEBUG
#define MSGLINE_TIMEOUT 2 // Messageline time-out number of cycles for testing.
#else
#define MSGLINE_TIMEOUT 10 // Messageline time-out number of cycles when no reception of data - default 10.
#endif
uint8_t messageValid[NUMSTR] = // message valid & time-out array
{
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
void readRemote(char* sds011url, char* dht22url)
// read remote sensor data JSON messages (from server in Germany)
// interprete the always changing layout of the 2 response messages,
// confusing the algorithm to readout the values.
// always read the most recently measured values !
{
// restart if heap low
if (ESP.getFreeHeap() < 6000) {
ESP.restart();
}
char* pActualDataSet;
char* pLastDataSet;
char* pTypeP1;
char* pValueP1;
char* pTypeP2;
char* pValueP2;
char* pTimeD1;
char* pTimeD2;
// make reuse possible
http.setReuse(true);
// prepare default values
pm10 = -1000.0;
pm25 = -1000.0;
temperature = -1000.0;
humidity = -1000.0;
// wait for WiFi connection
if (WiFiMulti.run() == WL_CONNECTED) {
// start connection and send HTTP header
if (http.begin(client, sds011url)) {
uint16_t httpCode = http.GET();
// HTTP header has been send and Server response header has been handled
if (httpCode == HTTP_CODE_OK) {
String payload = http.getString();
const char *jsonx = payload.c_str();
// interprete JSON message for PM10 & PM2.5 values
pActualDataSet = strstr(jsonx, "\"sensordatavalues\":");
// scan to last dataset
while ((pActualDataSet)&&(pLastDataSet = strstr(pActualDataSet+1, "\"sensordatavalues\":"))) {
pActualDataSet = pLastDataSet;
}
// valid dataset found?
if (pActualDataSet) {
// yes, set pointers to value_type & value parameters
if (pTypeP1 = strstr(pActualDataSet, "\"value_type\":\"P1\"")) {
if (pTypeP2 = strstr(pActualDataSet, "\"value_type\":\"P2\"")) {
if (pValueP1 = strstr(pActualDataSet, "\"value\":")) {
if (pValueP2 = strstr(pValueP1+1, "\"value\":")) {
// parameters reversed?
if (pTypeP1 < pTypeP2) {
// not reversed, readout PM10 and then PM2.5
sscanf((pValueP1+9), "%f", &pm10);
sscanf((pValueP2+9), "%f", &pm25);
} else {
// reversed, readout PM2.5 and then PM10
sscanf((pValueP1+9), "%f", &pm25);
sscanf((pValueP2+9), "%f", &pm10);
}
// set repeat counter when data ok
repeatCount = REPEAT_COUNT;
// handle maxima 24 hours
setMaxima24h(pm10, OUT_PM10);
setMaxima24h(pm25, OUT_PM25);
}
}
}
}
}
}
}
}
#if DHT_ACTIVE
if (sds011url != dht22url) {
// wait for WiFi connection
if (WiFi.status() == WL_CONNECTED) {
// start connection and send HTTP header
if (http.begin(client, dht22url)) {
uint16_t httpCode = http.GET();
// HTTP header has been send and Server response header has been
if (httpCode == HTTP_CODE_OK) {
String payload = http.getString();
const char *jsonx = payload.c_str();
// interprete JSON message for humidity & temperature values
pActualDataSet = strstr(jsonx, "\"sensordatavalues\":");
// scan to last dataset
while ((pActualDataSet)&&(pLastDataSet = strstr(pActualDataSet+1, "\"sensordatavalues\":"))) {
pActualDataSet = pLastDataSet;
}
// valid dataset found?
if (pActualDataSet) {
// yes, set pointers to value_type & value parameters
if (pTypeP1 = strstr(pActualDataSet, "\"value_type\":\"humidity\"")) {
if (pTypeP2 = strstr(pActualDataSet, "\"value_type\":\"temperature\"")) {
if (pValueP1 = strstr(pActualDataSet, "\"value\":")) {
if (pValueP2 = strstr(pValueP1+1, "\"value\":")) {
// parameters reversed?
if (pTypeP1 < pTypeP2) {
// not reversed, readout humidity and then temperature
sscanf((pValueP1+9), "%f", &humidity); sscanf((pValueP2+9), "%f", &temperature);
} else {
// reversed, readout temperature and then humidity
sscanf((pValueP1+9), "%f", &temperature);
sscanf((pValueP2+9), "%f", &humidity);
}
}
}
}
}
}
}
}
}
}
#endif
}
void buildTotalMessage(uint8_t msgLine, char* desc)
// Construct displaymessage from all values read.
// Construct error message if not all values OK, or keep previous message if available.
{
displayString[0] = SCROLL_ONEPIXEL; // scrolling mode, 1 pixel spacing
displayString[1] = '\0'; // terminate string
strcat(displayString, desc); // add the description
if (displayString[1] != '\0') { // if description not empty
strcat(displayString, ":"); // then add ':'
}
if ((pm10 > -1000)&&(pm25 > -1000)) { // ensure PM values OK
//
if ((temperature > -1000)&&(humidity > -1000)) { // ensure temperature & humidity values OK
strcat(displayString, messageText[STRING_THERMO]); // add temperature sign
if (temperature < 0) { // if value below zero
sprintf(workString, "%d", ((int)(temperature-0.5f))); // round down
} else {
sprintf(workString, "%d", ((int)(temperature+0.5f))); // else round up
}
strcat(displayString, workString); // incorporate measured value
strcat(displayString, messageText[STRING_DEGREES]); // add degrees Celsius message
//
strcat(displayString, messageText[STRING_DROPLET]); // add droplet sign
sprintf(workString, "%d", ((int)(humidity+0.5f)));
strcat(displayString, workString); // incorporate measured value
strcat(displayString, "%"); // add percent message
}
//
strcat(displayString, messageText[STRING_PM10]); // add PM10 message
sprintf(workString, "%d", ((int)(pm10+0.5f)));
strcat(displayString, workString); // incorporate measured value
strcat(displayString, messageText[STRING_UGRAM3]); // final µg/m³ message
//
strcat(displayString, messageText[STRING_PM25]); // add PM2,5 message
sprintf(workString, "%d", ((int)(pm25+0.5f)));
strcat(displayString, workString); // incorporate measured value
strcat(displayString, messageText[STRING_UGRAM3]); // add final µg/m³ message
sprintf(message[msgLine], "%s", (displayString)); // add constructed line to message buffer
//
// Set memory message time-out
messageValid[msgLine] = MSGLINE_TIMEOUT; // start message line time-out
//
} else {
if (messageValid[msgLine]) {
// PM values are missing & message is valid; hold the previous message on display for a certain time.
messageValid[msgLine]--;
} else {
// PM values are missing & no previous message available, or time-out.
strcat(displayString, messageText[ERRORNODATA]); // add error no data
sprintf(message[msgLine], "%s", (displayString)); // add constructed line to message buffer
//
repeatCount = 1; // no long display loops please, try to read data again
}
}
}
void buildMessageValue(uint8_t msgLine, uint8_t dataSel, float valueToShow)
// Build message with value in message buffer
// Parameters:
// msgLine: Line number in display buffer, where to put the constructed message.
// dataSel: OUT_PM10: Outdoor PM10 value to be selected for SHOW_VALUE
// OUT_PM25: Outdoor PM2.5 value to be selected for SHOW_VALUE
// OUT_TEMP: Outdoor temperature value to be selected for SHOW_VALUE
// OUT_HUMI: Outdoor humidity value to be selected for SHOW_VALUE
// IN_PM10: Indoor PM10 value to be selected for SHOW_VALUE
// IN_PM25: Indoor PM2.5 value to be selected for SHOW_VALUE
// valueToShow:Value to show
{
if (valueToShow > -1000.0) { // if value not OK, don't touch message buffer (previous message preserved!)
//
displayString[0] = ' '; // mark result empty
displayString[1] = '\0'; // terminate string
//
switch(dataSel) {
case OUT_PM10: {
displayString[0] = FIXED_ONEPIXEL; // fixed mode, 1 pixel spacing
displayString[1] = '\0'; // terminate string
strcat(displayString, messageText[STRING_PM10_EQ]); // add "PM10 = " string
sprintf(workString, "%d", ((int)(pm10+0.5f))); // prepare value
strcat(displayString, workString); // incorporate value
strcat(displayString, messageText[STRING_UGRAM3]); // add final µg/m³ message
break;
}
case OUT_PM25: {
displayString[0] = FIXED_ONEPIXEL; // fixed mode, 1 pixel spacing
displayString[1] = '\0'; // terminate string
strcat(displayString, messageText[STRING_PM25_EQ]); // add "PM2,5 = " string
sprintf(workString, "%d", ((int)(pm25+0.5f))); // prepare value
strcat(displayString, workString); // incorporate value
strcat(displayString, messageText[STRING_UGRAM3]); // add final µg/m³ message
break;
}
case OUT_TEMP: {
displayString[0] = FIXED_ONEPIXEL; // fixed mode, 1 pixel spacing
displayString[1] = '\0'; // terminate string
strcat(displayString, messageText[STRING_TEMP_EQ]); // add "Temp = " string
if (temperature < 0) { // if value below zero
sprintf(workString, "%d", ((int)(temperature-0.5f))); // round down
} else {
sprintf(workString, "%d", ((int)(temperature+0.5f))); // else round up
}
strcat(displayString, workString); // incorporate value
strcat(displayString, messageText[STRING_DEGREES]); // add degrees celsius
break;
}
case OUT_HUMI: {
displayString[0] = FIXED_ONEPIXEL; // fixed mode, 1 pixel spacing
displayString[1] = '\0'; // terminate string
strcat(displayString, messageText[STRING_VOCHT_EQ]); // add "Vocht = " string
sprintf(workString, "%d", ((int)(humidity+0.5f))); // prepare value
strcat(displayString, workString); // incorporate value
strcat(displayString, "%"); // add percent
break;
}
case IN_PM10: {
displayString[0] = FIXED_ONEPIXEL; // fixed mode, 1 pixel spacing
displayString[1] = '\0'; // terminate string
strcat(displayString, messageText[STRING_PM10_EQ]); // add "PM10 = " string
sprintf(workString, "%d", ((int)(indoorPm10+0.5f))); // prepare value
strcat(displayString, workString); // incorporate value
strcat(displayString, messageText[STRING_UGRAM3]); // add final µg/m³ message
break;
}
case IN_PM25: {
displayString[0] = FIXED_ONEPIXEL; // fixed mode, 1 pixel spacing
displayString[1] = '\0'; // terminate string
strcat(displayString, messageText[STRING_PM25_EQ]); // add "PM2,5 = " string
sprintf(workString, "%d", ((int)(indoorPm25+0.5f))); // prepare value
strcat(displayString, workString); // incorporate value
strcat(displayString, messageText[STRING_UGRAM3]); // add final µg/m³ message
break;
}
default:
break;
}
sprintf(message[msgLine], "%s", (displayString)); // add constructed line to message buffer
}
}
void buildGraphDisplayStr(float valueToShow, uint8_t maxVal)
// Construct string containing bargraph
// Don't show bargraph if value not OK.
{
displayString[0] = ' '; // mark result empty
displayString[1] = '\0'; // terminate string
if (valueToShow > -1000) { // test if value OK
uint8_t dotsAvailable = 8 * NUMBER_DISPLAYS; // total available dots till end of display
uint8_t dotScale10; // dots to fill scale 0 to 10
uint8_t dotScale100; // dots to fill scale 0 to 100
uint8_t dotsNeeded; // dots needed to show value
uint8_t dotsTemp; // dots temperature value
uint8_t white; // counter for "white" bar
uint8_t black; // counter for "black" bar
uint8_t scaleCount; // scale counter
uint8_t scaleMark; // scale mark counter
bool overTheTop = false; // data bigger than display
bool topDrawn = false; // top drawn flag
if (valueToShow < 0) { // negative values show 0
valueToShow = 0;
}
if (valueToShow > (1.2 * maxVal)) { // limit to maximum++ to avoid overflow
valueToShow = (1.2 * maxVal);
}
// start building graph line
// set format & bolleke
m = 0;
displayString[m++] = FIXED_NOPIXEL; // fixed mode, no pixel spacing
displayString[m++] = BAR_BOL; // bolleke
displayString[m++] = BAR_WIDTH_1; // 1 pixel
dotsAvailable -= 9; // adjust dotsAvailable (9 less: bolleke+1 pixel)
dotsAvailable -= 2; // adjust dotsAvailable (2 less: 1 x double BAR_SCALE)
dotsAvailable -= 2; // adjust dotsAvailable (2 less: 2 x 1/2 double BAR_SCALE)
dotScale10 = (dotsAvailable/10); // calculate dots to represent scale step of 10%
dotScale100 = (dotScale10*10); // calculate dots to represent scale 0 to 100
dotsTemp = (valueToShow+0.5f); // round & convert to int
dotsNeeded = ((dotsTemp*dotScale100)/maxVal); // calculate dots to show value
// check for overflow
if (dotsNeeded > dotsAvailable) {
dotsNeeded = dotsAvailable;
overTheTop = true;
}
scaleMark = 11; // 11 scale marks
while (scaleMark--) {
// draw full / half scale
switch (scaleMark) {
case 0:
// fall through
case 5: // half scale
if (dotsNeeded) dotsNeeded--;
// fall through
case 10: // begin scale
displayString[m++] = BAR_SCALE;
displayString[m++] = BAR_SCALE;
if (dotsAvailable) dotsAvailable--;
if (dotsAvailable) dotsAvailable--;
break;
default: // all other places
displayString[m++] = BAR_HALF_SCALE;
if (dotsAvailable) dotsAvailable--;
if (dotsNeeded) dotsNeeded--;
break;
}
// draw "white" dots when value not yet reached, else draw "black" dots
white = 0;
black = 0;
scaleCount = (dotScale10-1);
while ((dotsAvailable)&&(scaleCount)) {
if (dotsAvailable) dotsAvailable--;
if (scaleCount) scaleCount--;
if (dotsNeeded) {
dotsNeeded--;
white++;
} else {
black++;
}
}
if (white > 8) {
displayString[m++] = BAR_WIDTH_7;
displayString[m++] = BAR_WIDTH_1;
white -= 8;
}
switch (white) {
case 0:
break;
case 1:
displayString[m++] = BAR_WIDTH_1;
break;
case 2:
displayString[m++] = BAR_WIDTH_1;
displayString[m++] = BAR_WIDTH_1;
break;
case 3:
displayString[m++] = BAR_WIDTH_1;
displayString[m++] = BAR_WIDTH_1;
displayString[m++] = BAR_WIDTH_1;
break;
case 4:
displayString[m++] = BAR_WIDTH_1;
displayString[m++] = BAR_WIDTH_1;
displayString[m++] = BAR_WIDTH_1;
displayString[m++] = BAR_WIDTH_1;
break;
case 5:
displayString[m++] = BAR_WIDTH_2;
displayString[m++] = BAR_WIDTH_1;
displayString[m++] = BAR_WIDTH_1;
displayString[m++] = BAR_WIDTH_1;
break;
case 6:
displayString[m++] = BAR_WIDTH_3;
displayString[m++] = BAR_WIDTH_1;
displayString[m++] = BAR_WIDTH_1;
displayString[m++] = BAR_WIDTH_1;
break;
case 7:
displayString[m++] = BAR_WIDTH_4;
displayString[m++] = BAR_WIDTH_1;
displayString[m++] = BAR_WIDTH_1;
displayString[m++] = BAR_WIDTH_1;
break;
case 8:
displayString[m++] = BAR_WIDTH_5;
displayString[m++] = BAR_WIDTH_1;
displayString[m++] = BAR_WIDTH_1;
displayString[m++] = BAR_WIDTH_1;
break;
default:
break;
}
if (!dotsNeeded) {
if (topDrawn == false) {
topDrawn = true;
if ((displayString[m-1] != BAR_SCALE)&&(displayString[m-1] != BAR_HALF_SCALE)) {