Ⅶ. 完成品としての仕上げ(ESP-WROOM-02)

 ここまで5つのステップを通して計測・記録・サーバー機能と、ブラウザとのコミュニケーション・インターフェイスを検討してきました。
  ・espRTC.ino:NTPによる時刻自動補正とバッテリーバックアップを備えた時計の製作
  ・espI2C.ino:I2Cによる温湿度・気圧・照度センサーの組み込みと計測
  ・espSD.ino:SDカードの増設とテキストデータの入出力、およびアナログセンサーの増設
  ・espMeasure.ino:タイマー割り込みによる自動計測システム
  ・espWiFi.ino:Wi-Fi Web Serverの構築とコミュニケーション・インターフェイス
 今回はこれらの機能を統合して「所定の計測条件にしたがって計測を継続しながら、ブラウザからの要求にも応答する「AirMonitor」に仕上げます。また、MCUブレッドボードをユニバーサルプリント基板に組み付けて完成させます。

 ※スケッチのダウンロードは右側の[Download]ボタンをクリックしてください。    

1.今回の開発テーマ

(1)動作条件の設定・変更機能の追加

 AirMonitorの動作条件を弾力的に変更できるように、『Ⅴ. 定期自動計測とデータ記録』で検討した測定処理条件をSDカード上のファイル「AIRMON.PRM」に保持させることにします。必要に応じてその内容をメンテナンスできるように次の保守画面を新設します。

 保守画面を表示するために、コマンド入力画面に「mainte:動作条件を変更する」を追加します。

 コマンド入力画面の操作は前回説明したとおりですが、入力欄で「mainte」と入力して[実行]ボタンをクリックするか[Enter]キーをたたくと、新設した動作条件設定画面に切り替わります。条件設定画面ではSSIDやPasswordも変更できますが、入力内容が間違っていると次回の起動ができなくなるので注意が必要です。
 変更した内容はすぐにファイルに書き込まれますが、MCUボードのリセットスイッチを押した時点で有効になります。


(2)自動計測記録処理とサーバーの並行動作の実現

 AirMonitorは起動すると、システムクロックの「分」が「計測開始分」で設定した値になるまで待って計測を開始します。以後は設定された時間間隔ごとに測定して、測定値をSDカード上のファイルに記録します。さらに、指定された時間間隔でNTPサーバーと通信してシステムクロックの時刻合わせを行います。
 これらの自動化された処理と並行して、起動後はいつでもWebサーバーとして機能させる必要があります。計測中でも計測開始前でも、リアルタイム計測や動作条件の変更、記録されている計測データの検索表示ができなければなりません。ただし、動作条件変更後のシステムリセットについては、今回は手処理によることとします。

2.MCUボードの製作

 前回までブレッドボードに組み付けていたMCUの部分を、ユニバーサル基板(秋月電子通商、60円)に移行しました。ESP-WROOM-02はスイッチサイエンス社のコンパクトな「ピッチ変換済みモジュール《フル版》」を使用しました。執筆時点の価格は909円で、別途ヘッダーピンを購入して半田付けが必要です。

(1)回路図

 回路はいままでとほぼ同じですが、モード選択のスイッチをタクトスイッチから基板用スライドスイッチ(秋月電子通商、20円)に変更、LEDを3mmの小さいものに変更、MCUのRST(リセット)端子にプルアップ抵抗(10kΩ)を追加した点が異なっています。また、外部との配線用に6Pのピンソケット(秋月電子通商、20円)を追加しています。


(2)出来上がったMCUボード

 基板の四隅にスペーサーをビス止めして脚にしてみました。裏面の配線はかなりすさまじいですが、ご参考までに。
 左はPCに接続したMCUボードです。右は単独で計測中の姿です。スライドスイッチは、リセット用の赤色タクトスイッチ側にセットするとLEDが消灯して書き込みモードになり、反対にセットするとLEDが点灯して実行モードになります。RTCや計測センサー類も別の基板に組み込んで、電源部をコンパクトな3端子レギュレーターに置き換えれば、MCUボードと二層構造にしたAirMonitorになるのですが、今回はここまでとします。

3.ソフト開発のポイント

 基本的にはいままで個別に開発してきた機能を組み合わせるだけなのですが、処理開始の手順(スケッチのsetup部分の構成)や反復処理(loop部)での計測開始時の割込タイマー設定などはひと工夫が必要です。また、クライアント(ブラウザ)との通信を簡単にするために、サーバーのコールバック関数を的確に利用することが重要です。

(1)基幹部の処理

 最終的なスケッチのサイズは1,200行を超えますが、setup()とloop()で構成される基幹部の処理は、コメントや十分な空白行を含めて90行ほどのコンパクトなコードです。
 下図はそのフローチャートです。


 フローチャートを見ると、ブルーカラーで表示したまだ内容がわからない部分以外は、すでに説明済みなので容易に理解できると思います。「外部設定条件を読み込む(readParameterFile)」は文字通りSDカード上のファイルから設定条件を読み取る処理です。以下ではピンクのWebサーバー関連部分を再度確認し、初回設定処理(InitialProcess)を見ておきましょう。


(2)サーバー・コールバック関数

 server.onはクライアントからの要求を受けて実行させるメソッド、つまりサーバーのコールバック関数を指定するものです。「HTTPリクエストヘッダーが'GET'で通信してきたら、コマンド入力画面を表示するsendCommandScreen()を実行させる」、また「HTTPリクエストヘッダーが'GET'で通信してきたら、処理を切り分けるprocControl()メソッドを実行させる」ことを指定しています。
 loop内では、handleClient()でクライアントからの要求を待ち構えています。コマンド入力画面と動作条件設定画面は、それぞれHTML文のformタグでリクエストヘッダーに'POST'を指定しているので、要求があるとprocControlメソッドが呼び出されます。検索結果や計測結果、入力エラーなどの表示画面はリクエストヘッダーに'GET'を指定しているので、ボタンをクリックして要求を受けるとsendCommandScreenでコマンド入力画面が呼び出されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
void setup() {
                    :
                    :
                    :
 
    // Set server callback functions
    server.on("/", HTTP_GET, sendCommandScreen);
    server.on("/", HTTP_POST, procControl);
}
 
void loop() {
    server.handleClient();
}
 新たに登場したコールバック関数procControlのコードは次のようになっています。
 6行目のserver.arg("proc_kind")で、'POST'要求を発行した画面の種別を取得して、コマンド入力画面か動作条件設定画面かを判定しています。コマンド入力画面であれば即コマンド解析メソッドprocAnalyzeCommandを実行させます。動作条件設定画面であれば、さらに11行目のserver.arg("proc_mode")で[設定][中止]のいずれのボタンがクリックされたかを判別し、[設定]ならrewriteParameterFileメソッドで入力内容をパラメータファイルに書き出し、続いてコマンド入力画面を表示させています。
 このように呼び出し元の画面から'proc_kind'や'proc_mode'などの値を取得できるのは、それぞれの画面を表示するHTML文に仕掛けがあります。まず25行と45行に注目してください。どちらの画面もproc_kindという名前の非表示(type='hidden')タグを持ち、一方は'command'、もう一方は'mainte'という値を設定しています。これが6行目で取得されて、画面の種別判定に利用されているわけです。
 計測設定条件画面には、同様に名前proc_modeで'XYZ'という値をもった非表示タグが書かれています。この値は、43~44行目のボタンがクリックされた時に発生するonclickイベントを補足します。それぞれsetModeDo()またはsetModeCancel()を呼んで、proc_modeの値に'set'か'cancel'を設定します。具体的な設定は、39~40行のJavaScriptで行っています。先と同様に、11行目でこの値を取得してボタン種別を判定しています。
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
/*
 * Process controll.
 */
void procControl()
{
    String procKind = server.arg("proc_kind");
    if (procKind == "command") {
        procAnalyzeCommand();
    }
    else if (procKind == "mainte") {
        String sMode = server.arg("proc_mode");
        if (sMode == "set")
            rewriteParameterFile();
        procAnalyzeCommand();
    }
}
 
void sendCommandScreen()
{
                
                
    String strBuf = "<!DOCTYPE html><html>"
                
                
        "<input type='hidden' name='proc_kind' value='command' />"
                
                
        "</html>";
    server.send(200, "text/html", strBuf);
}
 
void sendMainteScreen()
{
                
                
    String strBuf = "<!DOCTYPE html><html>"
                
                
        "<script>function setModeDo() {document.getElementById('proc_mode').value='set';}"
        "function setModeCancel() {document.getElementById('proc_mode').value='cancel';}"</script>
                
                
        "<input type='submit' name='SUBMIT' value=' 設 定 ' onclick='setModeDo();' /> "
        "<input type='submit' value=' 中 止 ' onclick='setModeCancel();' />"
        "<input type='hidden' name='proc_kind' value='mainte' />"
        "<input type='hidden' name='proc_mode' id='proc_mode' value='XYZ' />"
                
                
        "</html>";
    server.send(200, "text/html", strBuf);
}


(3)初回設定処理(InitialProcess)

 計測開始時刻になると呼ばれるので、まず初回の計測を行います。続いてタイマー割り込みTickerの2つのインスタンスに対して、動作条件設定ファイルから取得した値を使って設定を行います。ひとつは計測始動メソッドkickRoutineWorkで、計測間隔ごとにタイマー割り込みを発生させるように指定しています。もうひとつは時刻合わせ始動メソッドのkickTimeAdjustで、時刻調整間隔ごとに実行するよう指定しています。
 kickRoutineWorkメソッドではdoMeasurementで計測を行い、editMeasuredResultで計測結果を編集させて、writeMeasurementResultで編集結果をログファイルに書き込んでいます。
 kickTimeAdjustは単にbReadyTickerをtrueに設定するだけです。この値が同じloop内でチェックされて、時刻合わせのメソッドadjustTimeが実行されます。
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
/*
 * Run only once at the beginning.
 */
void InitialProcess()
{
    // 1st measuement
    kickRoutineWork();
 
    // Timer interrupt time and event setting.
    ticker1.attach(iIntervalTime, kickRoutineWork);
    delay(1000);
    ticker2.attach(iNtpInterval, kickTimeAdjust);
}
 
/****************************< Interrupt handler >****************************/
/*
 * Timer interrupt event handler1
 *    <start measurement="">
 */
void  kickRoutineWork()
{
    // Measurment & write file
    doMeasurement();
    String buf =editMeasuredResult(rstMeasured);
    writeMeasurementResult(LOG_FILE, buf);
    Serial.println(buf);
}
 
/*
 * Timer interrupt event handler2
 *    <start time="" adgustment="">
 */
void kickTimeAdjust()
{
    bReadyTicker = true;
}
</start></start>

(4)全体の制御構造について

 すでにお分かりのように、基幹部以外のすべての処理は、基幹部または関連メソッドで設定する割り込みハンドラーとコールバック関数に委ねられています。クライアント(ブラウザ)とのやりとりはサーバーのコールバック関数が担当し、そのコールバック関数に書いたコードによって計測や記録、検索やコミュニケーションが進んで行きます。設定条件に従った定期的な計測はタイマーのコールバック関数がコントロールしています。システムクロックの時刻合わせも、タイマーのコールバック関数の役目です。
 MCU自体はいつもほとんど待ち状態であり、割り込みが発生した瞬間だけ必要な処理を行います。このように割り込み処理を利用すると複雑な手順制御が不要なので、スケッチのロジックを簡潔に組み立てることができます。また、割り込みが発生すると処理を一時中断してコールバック関数を実行するので、見かけ上マルチスレッドのような動作が可能になります。


4.連載の終わりにあたって

 『超小型格安チップ「ESP-WROOM-02(ESP8266)」はどこまで使えるか?』と題した7回の連載はこれで終了です。稚拙な内容ですが、長らくお付き合いいただきありがとうございました。これがきっかけになって、新たな取り組みへのご参考になれば嬉しいです。
 私事になりますが、山歩きで肉離れを起こしたのが原因で久々に向き合ったArduinoに始まり、ESP-WROOM-02 (ESP8266)にめぐり会っての3カ月ばかり、実に楽しい時間を過ごすことができました。最初は戸惑いながら購入したピッチ変換されただけのMCUでしたが、それぞれのピンの機能を確かめたり回路を考える上で、この選択は良かったと思っています。また今回組み立てたMCUボードは、この時期をこのように取り組んだ記念の品になりました。
 できれば今後も、このMCUボードを使っていろいろやってみたいのですが、「山は招いている」し、限られた時間でどこまで進めることができるかは不明です。なおいくつもの改良すべき事項を置き去りにしますが、限られた時間であれば、これにて「チョ~ン!」。


●スケッチ全景

 最後になりましたが、今までの流儀にしたがってスケッチのすべてを掲載します。前回同様、コードを利用する場合はこの画面をコピー&ペーストするのではなく、冒頭に配置しているボタンでダウンロードしてください。またコードを実行する際には、ssidとpasswordに環境に合った値を設定してください。当初は動作条件設定ファイルが作成されていないので、まずmainteで動作条件設定画面を表示して「NTPサーバー名」以降を設定します。MCUボードのスライドスイッチをLEDが点灯する側(実行モード)にセットして、リセットボタンを押すと実行開始です。
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
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
/*
 * File:      AirMonitor.ino   (Version 1.00)
 * Function:  Measure the temperature, humidity, atmospheric pressure, illuminance and soil
 *            moisture according to specified conditions and record on the SD card device.
 *            This acts as a Web server and responds to the following requests from the client.
 *              ・Answer the measurement result at the time of request immediately.
 *              ・Extract the measurement data recorded in the file from the designated position
 *                  by the specified number and distribute it on WiFi.
 *              ・Extract the measurement data recorded in the file by the specified date range
 *                  and distribute it on WiFi.
 *              ・Displays command usage.
 *            Those with fluctuating operating conditions depending on the environment can be
 *            described in an external file so that they can be changed.
 *            It has also automatic time adjustment function of RTC (DS1307) using NTP.
 *            To correct the date, key in into the serial monitor input box as the following
 *            format.
 *                  yymmdd or yymmddw or yy/mm/dd or yy/mm/dd/w
 *
 * Hardware   MCU:  ESP-WROOM-02(ESP8266)
 *            RTC:  DS1307 I2C Real time clock with battery backup
 *            BME280: Combined temperature, humidity and pressure sensor
 *            BH1750: Digital illuminance sensor
 *            YL-38 &YL-69: Soil moisture sensor
 *            SD card device: Hirose DM3AT series
 *
 * Remarks:   Measurement and calibration codes of BME 280
 *            uses SWITCH SCIENCE's sample sketch "BME280_I2C.zip".
 *                 https://trac.switch-science.com/wiki/BME280
 *
 * Date:      2017/02/10
 *                     Mail to: softlabo@nifty.com
 */
#include <Wire.h>
#include <SPI.h>
#include <SdFat.h>
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <NTPClient.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <Ticker.h>
 
// I2C Address
#define DS1307_ADDRESS 0x68   // Realtime clock
#define BME280_ADDRESS 0x76   // Humidity, Pressure and Temperature sensor
#define BH1750_ADDRESS 0x23   // Illuminance sensor
 
// Process mode
#define MODE_CURRENT  1       // Response current values
#define MODE_RECORDS  2       // Send records from SD limited the range of record position
#define MODE_DATE     3       // Send records from SD limited the range of date or date-time
#define MODE_MAINTE   11      // Create maintenance screen
 
// Analog input port
#define ANALOG_PIN     A0     // TOUT pin
 
// SD card drive & File name
#define SDCARD_DRIVE   16     // SD card chip select number
#define LOG_FILE    "logfile.txt"
#define PRM_FILE    "AIRMON.PRM"
 
// Time adjust
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP);
char sNtpUrl[26] = {"ntp.nict.jp"};
long iNtpOffset = 32400;      // UTC + 9h (9H * 3600)sec.
long iNtpInterval = 43200;    // Time adjust interval(12H * 3600)sec.
 
// Timer interruption control
Ticker ticker1;               // For measurement
Ticker ticker2;               // For time adjustment
bool bReadyTicker = false;
 
// SD card control
SdFat SD;
bool bSD_Enabled = false;
 
// Measurement conditions
bool bWait = true;
char strStartTime[3] = {"00"};  // "00"~"59" (min.)
long iIntervalTime = 3600;      // Measurement time interval(60M * 60)sec.
 
// WiFi connection
char ssid[33]     = {"your_ssid"};
char password[64] = {"your_password"};
 
// WiFi response control
ESP8266WebServer server(80);
String strFromDate, strToDate;
long iRecPos, iRecNo;
 
// WiFi Response constants
const String strResponseHeader = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n";
const String strHeader = "<!DOCTYPE html>\r\n"
                "<html><head><meta charset=\"utf-8\"><title>AirMonitor</title>"
                "<style>body{line-height:120%;font-family:monospace;}</style>"
                "</head><body><form name='resultform' target='_self' method='get'>";
const String strFooter = "   <INPUT type='submit' value=' 戻 る ' autofocus /></form></body></html>";
const String strTitle = "No., Temp(C), Press(hPa), Hum(%), Illum(lx), Date Time";
 
// DS1307 Clock data
struct ClockData {
    byte year;
    byte month;
    byte day;
    byte week;
    byte hour;
    byte minute;
    byte sec;
    byte ctrl;
};
ClockData dtClock;
 
//BME280 Global variables
unsigned long int hum_raw, temp_raw, pres_raw;
signed long int t_fine;
 
//BME280 Calibration variables
uint16_t dig_T1;
 int16_t dig_T2;
 int16_t dig_T3;
uint16_t dig_P1;
 int16_t dig_P2;
 int16_t dig_P3;
 int16_t dig_P4;
 int16_t dig_P5;
 int16_t dig_P6;
 int16_t dig_P7;
 int16_t dig_P8;
 int16_t dig_P9;
 int8_t  dig_H1;
 int16_t dig_H2;
 int8_t  dig_H3;
 int16_t dig_H4;
 int16_t dig_H5;
 int8_t  dig_H6;
  
//Measured result
struct MeasuredResult {
    double  temperature;
    double  pressure;
    double  humidity;
    int     illuminance;
    int     soil_moisture;
    String  datetime;
};
MeasuredResult rstMeasured;
 
// Parameter keyword
String kwd_ssid="ssid=";
String kwd_pwd="pwd=";
String kwd_ntp_svr="ntp_server=";
String kwd_ntp_int="ntp_interval=";
String kwd_ntp_off="ntp_time_offset=";
String kwd_start="measuring_start_time=";
String kwd_interval="measuring_interval=";
 
/*****************************************************************************
 *                          Predetermined Sequence                           *
 *****************************************************************************/
void setup() {
    Serial.begin(115200);
 
    // Prepare I2C protocol.
    Wire.begin();
    delay(50);
 
    // Prepare SD card unit.
    if (SD.begin(SDCARD_DRIVE))
        bSD_Enabled = true;
    else {
        bSD_Enabled = false;
        Serial.println("SD Drive does'nt work!");
        return;
    }
    Serial.println("SD enabled!");
 
    // Read parameter file and set variables.
    readParameterFile();
 
    // Set dateTime callback function to provide timestamp.
    SdFile::dateTimeCallback(dateTime);
 
    // Prepare WiFi system.
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    server.begin();
    Serial.print("\nServer started!  IP: ");
    Serial.println(WiFi.localIP());
 
    // Set local DNS
    MDNS.begin("airmon");
 
    // Create NTPClient object.
    timeClient = NTPClient(ntpUDP, sNtpUrl, iNtpOffset);
 
    // Prepare measurment
    prepareBME280();      // Humidity, Pressure and Temperature sensor
    delay(500);
    prepareBH1750(BH1750_ADDRESS);  // Illuminance sensor
 
    // Dummy measurement
    doMeasurement();
 
    // Tuning clock
    adjustTime();
    getDateTime(&dtClock);
    Serial.print("Start : "); Serial.println(editDateTime(dtClock));
 
    // Set server callback functions
    server.on("/", HTTP_GET, sendCommandScreen);
    server.on("/", HTTP_POST, procControl);
    Serial.print("Just wait until next time(minuets) =  "); Serial.println(strStartTime);
}
 
void loop() {
    if (!bSD_Enabled) {
        Serial.println("Can't work, the SD drive is disabled!");
        delay(60000);
        return;
    }
    // Check date input ('hh/mm/dd/w') from serial buffer.
    if (Serial.available() >= 8) {
        String date = Serial.readString();
        setDate(date);
    }
 
    // Wait until the specified time and start up
    if (bWait) {
        getDateTime(&dtClock);
        String sTime = editTime(dtClock);
        if (sTime.substring(3, 5) == strStartTime) {
            bWait = false;
            Serial.println("It is just in time!!");
            InitialProcess();
            return;
        }
        else
            delay(200);
    }
 
    /* [Timer interrupt process] */
    if (bReadyTicker) {
        adjustTime();
        bReadyTicker = false;
    }
    server.handleClient();
}
 
/*
 * Run only once at the beginning.
 */
void InitialProcess()
{
  Serial.println(">> InitialProcess()");
    // 1st measuement
    kickRoutineWork();
 
    // Timer interrupt time and event setting.
    ticker1.attach(iIntervalTime, kickRoutineWork);
    delay(1000);
    ticker2.attach(iNtpInterval, kickTimeAdjust);
}
 
/****************************< Interrupt handler >****************************/
/*
 * Timer interrupt event handler1
 *    <Start measurement>
 */
void  kickRoutineWork()
{
    // Measurment & write file
    doMeasurement();
    String buf =editMeasuredResult(rstMeasured);
    writeMeasurementResult(LOG_FILE, buf);
    Serial.println(buf);
}
 
/*
 * Timer interrupt event handler2
 *    <Start time adgustment>
 */
void kickTimeAdjust()
{
    bReadyTicker = true;
}
 
/************************< Server callback functions >************************/
/*
 * Display initial screen.
 */
void sendCommandScreen()
{
    String strBuf = "<!DOCTYPE html><html><head><meta charset=\'utf-8\'><title>AirMonitor</title>"
        "<style>body{line-height:150%;font-family:monospace;}"
        "table.tbl{font-size:13px;color:#0;margin-left:20px;border-collapse: collapse;}table.tbl tr {border: 1px solid #888888;text-align: left;}table.tbl td {border: 1px solid #888888;text-align: left;}table.tbl tr.ttl {background-color:#e6e6fa;}div{color:red;}</style>"
        "<script>function setCursor() {var obj = document.getElementsByName('COMMAND')[0];obj.focus();obj.value+= '';}"
        "function checkForm(){var data=document.getElementsByName('COMMAND')[0];if(data.value.trim()=='')return false; else return true;}</script></head>"
        "<body onload='setCursor();'><form name='mainform' target='_self' method='post' onsubmit='return checkForm();'><br>"
        "<input type='hidden' name='proc_kind' value='command' />"
        "<table  class='tbl'><caption><font color='blue' size='+1'><b>  【コマンド入力】</b></font><br></caption>"
        "<tr class='ttl'><td width='220'><b>   コマンド</b></td><td width='420'><b>   機能・動作</b></td></tr>"
        "<tr><td> now</td><td> 現在の計測値を表示する</td></tr>"
        "<tr><td> date={date}</td><td> 指定日付の計測データをすべて表示する</td></tr>"
        "<tr><td> date={from_date},{to_date}</td><td> 日付範囲のデータを表示する</td></tr>"
        "<tr><td> rec={position}</td><td> 指定レコード位置からファイル終端までのデータを表示する</td></tr>"
        "<tr><td> rec={position},{records}</td><td> 指定レコード位置から指定したレコード数だけ表示する</td></tr>"
        "<tr><td> mainte</td><td> 動作条件を変更する</td></tr></font></table>"
        "<table border='0'><tr><td width='20'> </td><td width='220'>→ <input type='text' name='COMMAND' maxlength='25' value='' autofocus' /></td>"
        "<td><input type='submit' name='SUBMIT' value=' 実 行 ' /></td></tr></table><br>"
        "<font color='darkcyan'> (注記)<br>    =記号の右辺には{ }の内容を指定します。<br>    dateは年月日をyy/mm/ddのように、positionとrecordsは数字を、それぞれ半角で指定してください。</font>"
        "</form></body></html>";
    server.send(200, "text/html", strBuf);
}
 
/*
 * Process controll.
 */
void procControl()
{
    String procKind = server.arg("proc_kind");
    if (procKind == "command") {
        procAnalyzeCommand();
    }
    else if (procKind == "mainte") {
        String sMode = server.arg("proc_mode");
        if (sMode == "set")
            rewriteParameterFile();
        procAnalyzeCommand();
    }
}
/*****************************************************************************/
  
/*
 * Analyze command  and control execution.
 */
void procAnalyzeCommand()
{
    String cmd = server.arg("COMMAND");
    if (cmd == "") {
        sendCommandScreen();
        return;
    }
    Serial.print("cmd: ");
    Serial.println(cmd);
    int iMode = testProcessMode(cmd);
 
    switch (iMode) {
        case MODE_CURRENT:
            doMeasurement();
            sendMeasuredResult(rstMeasured);
            break;
        case MODE_DATE:
            sendDataRecord(LOG_FILE, strFromDate, strToDate);
            break;
        case MODE_RECORDS:
            sendDataRecord(LOG_FILE, iRecPos, iRecNo);
            break;
        case MODE_MAINTE:
            sendMainteScreen();
            break;
        case 0:
            break;
        case -1:
            sendFormatError();
            break;
    }
}
/*****************************************************************************/
 
/*
 * Adjust the clock time.
 */
void adjustTime()
{
    while(1) {
        if (timeClient.update()) {
            String strJST = timeClient.getFormattedTime();
            Serial.print("JST time = "); Serial.println(strJST);
            setTime(strJST);
            return;
        }
        delay(50);
    }
}
 
/*
 * Analyze HTTP request parameter and decide process mode.
 *    Argument: (String)Command string.
 *    Return:   Process mode(MODE_CURRENT / MODE_DATE / MODE_RECORDS / -1(Error))
 */
int testProcessMode(String strParam)
{
    //strParam.trim();
    int iPos, iPos2;
 
    // Analyze the process
    int iMode = 0;
    if (strParam.indexOf("now") != -1) {
        iMode = MODE_CURRENT;
        Serial.println("Mode: Current data.");
    }
    else if ((iPos = strParam.indexOf("date=")) != -1) {
        iMode = MODE_DATE;
        strParam = strParam.substring(iPos);
        Serial.print("Param: ");
        Serial.println(strParam);
        if ((iPos2 = strParam.indexOf(",")) == -1) {
            strFromDate = strParam.substring(5);
            strToDate = strFromDate;
        } else {
            strFromDate = strParam.substring(5, iPos2);
            strToDate = strParam.substring(iPos2 + 1);
        }
        Serial.print(" From:");
        Serial.print(strFromDate);
        Serial.print(", To:");
        Serial.println(strToDate);
        if (!(strFromDate.length() == 8 && strToDate.length() == 8))
            return -1;
        if (!(strFromDate.charAt(2) == '/' && strFromDate.charAt(5) == '/'
                && strToDate.charAt(2) == '/' && strToDate.charAt(5) == '/'))
            return -1;
        if (!(isNumeric(strFromDate.substring(0, 2))
                && isNumeric(strFromDate.substring(3, 5))
                && isNumeric(strFromDate.substring(6, 8))
                && isNumeric(strToDate.substring(0, 2))
                && isNumeric(strToDate.substring(3, 5))
                && isNumeric(strToDate.substring(6, 8))))
            return -1;
        strFromDate = "20" + strFromDate;
        strToDate = "20" + strToDate;
    }
    else if ((iPos = strParam.indexOf("rec=")) != -1) {
        iMode = MODE_RECORDS;
        strParam = strParam.substring(iPos);
        Serial.print("Param: "); Serial.println(strParam);
        if ((iPos2 = strParam.indexOf(",")) == -1) {
            if (!(isNumeric(strParam.substring(4)) && strParam.substring(4).length() > 0)) {
                return -1;
            }
            iRecPos = (strParam.substring(4)).toInt();
            iRecNo = 10000;
        } else {
            if (!(isNumeric(strParam.substring(iPos + 4, iPos2))
                    && isNumeric(strParam.substring(iPos2 + 1))
                    && strParam.substring(iPos + 4, iPos2).length() > 0
                    && strParam.substring(iPos2 + 1).length() > 0)) {
                return -1;
            }
            iRecPos = (strParam.substring(iPos + 4, iPos2)).toInt();
            iRecNo = (strParam.substring(iPos2 + 1)).toInt();
        }
        if (iRecPos < 1)
            iRecPos = 1;
        if (iRecNo > 10000)
            iRecNo = 10000;
    }
    else if (strParam.indexOf("mainte") != -1)
        iMode = MODE_MAINTE;
    else
        iMode = -1;
    return iMode;
}
 
/*
 * Display Maintenance Screen
 */
void sendMainteScreen()
{
    String wsSsid = ssid;
    String wsPassword = password;
    String wsNtpUrl = "";
    String wsNtpInterval = "";
    String wsNtpOffset = "";
    String wsStartTime = "";
    String wsIntervalTime = "";
    char buf[10];
 
    if (readParameterFile()) {
        wsSsid = ssid;
        wsPassword = password;
        wsNtpUrl = sNtpUrl;
        wsNtpInterval = printNum(buf, iNtpInterval, 6);
        wsNtpOffset = printNum(buf, iNtpOffset, 6);
        wsStartTime = strStartTime;
        wsIntervalTime = printNum(buf, iIntervalTime, 6);
        wsNtpInterval.trim();  wsNtpOffset.trim();  wsIntervalTime.trim();
    }
    String strBuf = "<!DOCTYPE html><html><head><meta charset=\'utf-8\'><title>AirMonitor</title>"
        "<style>body{line-height:120%;font-family:monospace;}</style></head>"
        "<script>function setModeDo() {document.getElementById('proc_mode').value='set';}"
        "function setModeCancel() {document.getElementById('proc_mode').value='cancel';}"
        "function setCursor() {var obj = document.getElementsByName('ssid')[0];obj.focus();obj.value += '';}</script>"
        "<body onload='setCursor();'><form name='mainteform' target='_self' method='post'><br>"
        "<font color='blue' size='+1'><b>     【AirMonitor 動作条件の設定】</b></font><br><br>"
        "<table border='0'><tr><td width='20'> </td><td width='160'><b> 設定項目</b></td><td width='200'><b> 入 力 欄</b></td></tr>"
        "<tr><td></td><td>・SSID:</td><td><input name='ssid' size='15' maxlength='32' value='"+ wsSsid + "' autofocus /></td></tr>"
        "<tr><td></td><td>・Password:</td><td><input name='password' size='50' maxlength='63' value='" + wsPassword + "' /></td></tr>"
        "<tr><td></td><td>・NTPサーバー名:</td><td><input name='ntp_server' maxlength='25' size='30' value='" + wsNtpUrl + "' /></td></tr>"
        "<tr><td></td><td>・時刻調整間隔(秒):</td><td><input name='ntp_interval' maxlength='8' size='8' value='" + wsNtpInterval + "' /></td></tr>"
        "<tr><td></td><td>・UTFとの時差(秒):</td><td><input name='ntp_offset' maxlength='8' size='8' value='" + wsNtpOffset + "' /></td></tr>"
        "<tr><td></td><td>・計測開始分(00~59分):</td><td><input name='mes_start' maxlength='2' size='1' value='" + wsStartTime + "' /></td></tr>"
        "<tr><td></td><td>・計測間隔(秒):</td><td><input name='mes_interval' maxlength='8' size='8' value='" + wsIntervalTime + "' /></td></tr>"
        "<tr><td></td><td></td><td><input type='submit' name='SUBMIT' value=' 設 定 ' onclick='setModeDo();' /> "
        "<input type='submit' value=' 中 止 ' onclick='setModeCancel();' /></td></tr>"
        "<input type='hidden' name='proc_kind' value='mainte' /><input type='hidden' name='proc_mode' id='proc_mode' value='XYZ' /></table></form></body></html>"
        "<font color='darkcyan'><br> <b>(注意)</b><br>    設定した内容は<b>MCUボードのリセットボタンを押下</b>することで有効になります。</font>";
    server.send(200, "text/html", strBuf);
}
 
/*
 * Send HTTP response <MODE_RECORDS: Data record>
 *    Argument: (String)File name,
 *              (int)Target record position, (int)How many records.
 */
void sendDataRecord(String strName, int iRecPos, int iRecords)
{
    char buf[256], num[20];
     
    // Send title with a standard http response header
    String strBuf = strResponseHeader + strHeader + strTitle + "<br>";
    server.sendContent(strBuf);
 
    // Open data file
    File fc = SD.open(strName, FILE_READ);
    // Skip records
    bool bNodata = false;
    for (int i=1; i<iRecPos; i++) {
        int len = readln(&fc, buf, 250);
        if (len < 1) {
            bNodata = true;
            break;
        }
    }
    if (bNodata) {
        server.sendContent("Nothing!<br></bod</html>");
        return;
    }
 
    // Read and transfer each record until not EOF or condition is true
    int iCnt = iRecPos;
    int n = 0;
    strBuf = "";
    while (fc.available())
    {
        int len = readln(&fc, buf, 250);
        String number = printNum(num, iCnt++, 4);
        strBuf += number +  ", " + buf + "<br>";
        if (n % 50 == 0) {
            strBuf.replace(" ", " ");
            server.sendContent(strBuf);
            strBuf = "";
        }
        if (++n >= iRecords)
            break;
    }
    // Close file and sned trailer
    fc.close();
    if (n < 1)
        strBuf = "Nothing!";
    strBuf.replace(" ", " ");
    strBuf += "\r\n" + strFooter;
    server.sendContent(strBuf);
}
 
/*
 * Send HTTP response <MODE_DATE: Data record>
 *    Argument: (String)File name,
 *              (String)Date from, (String)Date to.
 */
void sendDataRecord(String strName, String strFrom, String strTo)
{
    char buf[256], num[20];
    int iLines = 0;
    int n = 0;
    String strRec, number;
 
    // Send title with a standard http response header
    String strBuf = strResponseHeader + strHeader + strTitle + "<br>";
    server.sendContent(strBuf);
 
    // Open data file
    File fc = SD.open(strName, FILE_READ);
    bool bAppeared = false;
 
    // Search top record
    while (fc.available()) {
        int len = readln(&fc, buf, 250);
        iLines++;
        strRec = buf;
        int iPos = strRec.lastIndexOf(',');
        String strWork = strRec.substring(iPos+1, iPos+12);
        strWork.trim();
        int iRes = strWork.compareTo(strFrom);
         
        if (iRes < 0)
            continue;
        else if (iRes > 0)
            goto rtn_end;
        else {
            break;
        }
    }
    number = printNum(num, iLines, 4);
    strBuf = number + ", " + buf + "<br>";
    n = 1;
 
    // Read and transfer each record until not EOF or condition is true
    while (fc.available())
    {
        int len = readln(&fc, buf, 250);
        if (len < 1)
            break;
        iLines++;  n++;
        strRec = buf;
        int iPos = strRec.lastIndexOf(',');
        String strWork = strRec.substring(iPos+1, iPos+12);
        strWork.trim();
        int iRes = strWork.compareTo(strTo);
        if (iRes > 0)
            break;
        bAppeared = true;
        number = printNum(num, iLines, 4);
        strBuf += number +  ", " + buf + "<br>";
        if (n % 50 == 0) {
            strBuf.replace(" ", " ");
            server.sendContent(strBuf);
            strBuf = "";
        }
    }
    // Close file and sned trailer
rtn_end:
    fc.close();
    if (!bAppeared)
        strBuf = "Nothing!";
    strBuf.replace(" ", " ");
    strBuf += "<br>" + strFooter;
    server.sendContent(strBuf);
}
 
/*
 * Send HTTP response <MODE_CURRENT: Real time measured result>
 *    Argument: (struct)Measured result.
 */
void sendMeasuredResult(MeasuredResult data)
{
    char num[20];
    String strBuf = strHeader + strTitle + "<br>";
    String number = printNum(num, 0, 4);
    number += ", " + (editMeasuredResult(data)) + "<br>";
    number.replace(" ", " ");
    strBuf += number +  "<br>" + strFooter;
    server.send(200, "text/html", strBuf);
}
 
/*
 * Send HTTP response <MODE missing: Input error message>
 */
void sendFormatError()
{
    String strBuf = strHeader + " <font color='red'>パラメータの様式に誤りがあります!</font>" + strFooter;
    server.send(200, "text/html", strBuf);
}
 
/*
 * Read parameter file and setup variables.
 *    Return:   (bool)true(success)/false(file not found)
 */
bool readParameterFile()
{
    Serial.println("** Input parameter from .prm file **");
    if (!SD.exists(PRM_FILE)) {
        Serial.println("Parameter file not found!");
        return false;
    }
 
    File fc = SD.open(PRM_FILE, FILE_READ);
    String buf;
 
    while (fc.available()) {
        int pos;
        char ch = fc.read();
        if (ch != '\n') {
            if (ch != '\r')
                buf += ch;
            continue;
        }
        if (buf.indexOf(kwd_ssid) >= 0 && (pos = buf.indexOf("=")) > 0)
            buf.substring(++pos).toCharArray(ssid, 40);
        if (buf.indexOf(kwd_pwd) >= 0 && (pos = buf.indexOf("=")) > 0)
            buf.substring(++pos).toCharArray(password, 40);
        if (buf.indexOf(kwd_ntp_svr) >= 0 && (pos = buf.indexOf("=")) > 0)
            buf.substring(++pos).toCharArray(sNtpUrl,40);
        if (buf.indexOf(kwd_ntp_int) >= 0 && (pos = buf.indexOf("=")) > 0)
            iNtpInterval = buf.substring(++pos).toInt();
        if (buf.indexOf(kwd_ntp_off) >= 0 && (pos = buf.indexOf("=")) > 0)
            iNtpOffset = buf.substring(++pos).toInt();
        if (buf.indexOf(kwd_start) >= 0 && (pos = buf.indexOf("=")) > 0)
            buf.substring(++pos).toCharArray(strStartTime, 3);
        if (buf.indexOf(kwd_interval) >= 0 && (pos = buf.indexOf("=")) > 0)
            iIntervalTime = buf.substring(++pos).toInt();
        buf.remove(0, buf.length());
    }
    fc.close();
    Serial.print("   > ssid:          ["); Serial.print(ssid); Serial.println("]");
    Serial.print("   > password:      ["); Serial.print(password); Serial.println("]");
    Serial.print("   > NTP Server:    ["); Serial.print(sNtpUrl); Serial.println("]");
    Serial.print("   > NTP Interval:  ["); Serial.print(iNtpInterval); Serial.println("]");
    Serial.print("   > NTP Offset:    ["); Serial.print(iNtpOffset); Serial.println("]");
    Serial.print("   > Start Time:    ["); Serial.print(strStartTime); Serial.println("]");
    Serial.print("   > Interval Time: ["); Serial.print(iIntervalTime); Serial.println("]");
    return true;
}
 
/*
 * Rewrite parameter file from setup variables.
 */
void rewriteParameterFile()
{
    String wsSsid = server.arg("ssid");
    String wsPassword = server.arg("password");
    String wsNtpServer = server.arg("ntp_server");
    String wsNtpInterval = server.arg("ntp_interval");
    String wsNtpOffset = server.arg("ntp_offset");
    String wsMesStart = server.arg("mes_start");
    String wsMesInterval = server.arg("mes_interval");
    wsSsid.trim();
    wsPassword.trim();
    wsNtpServer.trim();
    wsNtpInterval.trim();
    wsNtpOffset.trim();
    wsMesStart.trim();
    wsMesInterval.trim();
    if (!(isNumeric(wsNtpInterval) && isNumeric(wsNtpOffset)
            && isNumeric(wsMesStart) && isNumeric(wsMesInterval) && wsMesStart.length() == 2)) {
        sendFormatError();
        return;
    }
    if (wsSsid.length() < 6 || wsPassword.length() < 8 || wsNtpServer.length() < 8
            || wsNtpInterval.length() < 1 || wsNtpOffset.length() < 1 || wsMesInterval.length() < 1) {
        sendFormatError();
        return;
    }
 
    if (SD.exists(PRM_FILE))
        SD.remove(PRM_FILE);
    File sd = SD.open(PRM_FILE, FILE_WRITE);
    sd.println(kwd_ssid + wsSsid);
    sd.println(kwd_pwd + wsPassword);
    sd.println(kwd_ntp_svr + wsNtpServer);
    sd.println(kwd_ntp_int + wsNtpInterval);
    sd.println(kwd_ntp_off + wsNtpOffset);
    sd.println(kwd_start + wsMesStart);
    sd.println(kwd_interval + wsMesInterval);
    sd.close();
}
 
/*
 * Measure and  calibrate BME280, also illuminance, soil_moisture
 *    Result stored into rstMeasured structure.
 */
void doMeasurement()
{
    // Read BME280 measured result
    readData();
    // Calibration
    long temp_cal = calibration_T(temp_raw);
    long press_cal = calibration_P(pres_raw);
    long hum_cal = calibration_H(hum_raw);
 
    // Measurement illuminance
    int val1 = measureIlluminance(BH1750_ADDRESS);
 
    // Measurement soi_moisture
    int val2 = measureSoilMoisture(ANALOG_PIN);
 
    // Get date and time data from DS1307
    getDateTime(&dtClock);
     
    //Store into measured resut structure
    rstMeasured.temperature = (double)temp_cal / 100.0;
    rstMeasured.pressure = (double)press_cal / 100.0;
    rstMeasured.humidity = (double)hum_cal / 1024.0;
    rstMeasured.illuminance = val1;   
    rstMeasured.soil_moisture = val2;
    rstMeasured.datetime = editDateTime(dtClock);
}
 
/*
 * Edit measured result data.
 *    Argument: (struct)Measured result.
 *    Return:   Edited result.
 */
String editMeasuredResult(MeasuredResult data)
{
    char wbuf[20];
    String strWork = "";
    printNum(wbuf, data.temperature, 4, 1);
    strWork.concat(wbuf);
    strWork.concat(",");
    printNum(wbuf, data.pressure, 7, 1);
    strWork.concat(wbuf);
    strWork.concat(",");
    printNum(wbuf, data.humidity, 5, 1);
    strWork.concat(wbuf);
    strWork.concat(",");
    printNum(wbuf, data.illuminance, 6);
    strWork.concat(wbuf);
    strWork.concat(",");
    printNum(wbuf, data.soil_moisture, 4);
    strWork.concat(wbuf);
    strWork.concat(", ");
    strWork.concat(data.datetime);
    return strWork;
}
 
/*
 * Write measurment result to SD.
 *    Argument: (String)File name, String result.
 */
void writeMeasurementResult(String fname, String data)
{
    char buf[40];
    File df = SD.open(fname, FILE_WRITE);
    if (df)
    {
        df.println(data);
        df.close();
    }
}
 
/*
 * Read text line from SD card.
 *    Argument: File* handle, char* buffer, int Maximum text length.
 *    Return:   Size of read data.
 */
int readln(File* df, char* buf, int len)
{
    *buf = '\0';
    int pos = 0;   
     
    while (df->available()) {
        char ch = df->read();
        *(buf + pos) = ch;
        if (ch == 0x0a) {
            *(buf + pos + 1) = '\0';
            return pos;
        }
        pos++;
        if (pos >= len) {
            Serial.println("Overflowed!!");
            *buf = '\0';
            return 0;
        }
    }
}
 
/*
 * Print numeric (double, float)
 *    Argument: char* buffer, double data, int edit-length, int decimal.
 */
char *printNum(char *buf, double field, int len, int decimal)
{
    *buf ='\0';
    char bufw[40];
    dtostrf(field, len, decimal, bufw);
    strcpy(buf, bufw);
    return buf;
}
 
/*
 * Print numeric (int, long)
 *    Argument: char* buffer, int data, int edit-length.
 */
char *printNum(char *buf, int field, int len)
{
    *buf = '\0';
    char bufw[40];
    sprintf(bufw, "%d", field);
    int lenw = strlen(bufw);
    int i;
    for (i = 0; i < len - lenw; i++) {
        *(buf + i) = ' ';
    }
    *(buf + i) = '\0';
    strcat(buf, bufw);
    return buf;
}
 
/*
 * Test field is numeric or not.
 *    Argument: (String)Date string.
 *    Return:   (bool)true(numeric)/false(not numeric)
 */
bool isNumeric(String data)
{
    for (int i=0; i<data.length(); i++) {
        if (data.charAt(i) < '0' || data.charAt(i) > '9')
            return false;
    }
    return true;
}
 
/* ======================== DS1307 Clock Control ============================*/
/*
 * Read data from DS1307 Register
 */
void getDateTime(ClockData *dt)
{
    int iValue = 0;
   
    Wire.beginTransmission(DS1307_ADDRESS);
    Wire.write(iValue);
    Wire.endTransmission();
    Wire.requestFrom(DS1307_ADDRESS, 7);
    dt->sec = Wire.read();
    dt->minute = Wire.read();
    dt->hour = Wire.read();
    dt->week = Wire.read();
    dt->day = Wire.read();
    dt->month = Wire.read();
    dt->year = Wire.read();
}
 
/*
 * Tell day of week from code.
 */
String tellDayOfWeek(byte num)
{
    static String week[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
   
    if (num >= 1 && num <= 7)
      return week[num-1];
    else
      return "";
}
 
/*
 * Set DS1307 time register.
 *    Argument: (String)Time string 'hh:mm:ss'.
 */
void setTime(String sTime)
{
    byte bValue = 0x00;   //Top Address
    String buf = sTime;
    buf.replace(":", "");  buf.trim();
    if (buf.length() == 6) {
        byte hour = (buf.charAt(0) << 4) + (buf.charAt(1) & 0x0f);
        byte minute = (buf.charAt(2) << 4) + (buf.charAt(3) & 0x0f);
        byte sec = (buf.charAt(4) << 4) + (buf.charAt(5) & 0x0f);
        Wire.beginTransmission(DS1307_ADDRESS);
        Wire.write(bValue);
        Wire.write(sec);
        Wire.write(minute);
        Wire.write(hour);
        Wire.endTransmission();
    }
}
 
/*
 * Set DS1307 date register.
 *    Argument: (String)Time string 'yy/mm/dd/w'.
 */
void setDate(String sDate)
{
    byte bValue = 0x03;   //Top Address
    String buf = sDate;
    buf.replace("/", "");  buf.trim();
    if (buf.length() >= 6) {
        byte year = (buf.charAt(0) << 4) + (buf.charAt(1) & 0x0f);
        byte month = (buf.charAt(2) << 4) + (buf.charAt(3) & 0x0f);
        byte day = (buf.charAt(4) << 4) + (buf.charAt(5) & 0x0f);
        byte week = 0x00;
        if (buf.length() == 7) {
            week = (buf.charAt(6) & 0x07);
          if (week < 0x01 || week > 0x07)
            week = 0x01;
        }
        Wire.beginTransmission(DS1307_ADDRESS);
        Wire.write(bValue);
        Wire.write(week);
        Wire.write(day);
        Wire.write(month);
        Wire.write(year);
        Wire.endTransmission();
    }
}
 
/*
 * Edit DS1307 date and time.
 */
String editDateTime(ClockData dt)
{
    return editDate(dt) + " " + editTime(dt);
}
 
/*
 * Edit time register.
 */
String editTime(ClockData dt)
{
    String buf = "";
    char wbuf[12];
    sprintf(wbuf, "%02x:%02x:%02x", (int)dt.hour, (int)dt.minute, (int)dt.sec);
    buf.concat(wbuf);
    return buf;
}
 
/*
 * Edit date register.
 */
String editDate(ClockData dt)
{
    String buf = "20";
    char wbuf[12];
    sprintf(wbuf, "%02x/%02x/%02x", (int)dt.year, (int)dt.month, (int)dt.day);
    buf.concat(wbuf);
    return buf;
}
 
/*
 * Callback function to provide file timestamp.
 */
void dateTime(uint16_t* date, uint16_t* time)
{
    uint16_t year;
    uint8_t month, day, hour, minute, second;
     
    getDateTime(&dtClock);
    year = 2000 + ((int)(dtClock.year >> 4)) * 10 + (int)(dtClock.year & 0x0f);
    month = ((int)(dtClock.month >> 4)) * 10 + (int)(dtClock.month & 0x0f);
    day = ((int)(dtClock.day >> 4)) * 10 + (int)(dtClock.day & 0x0f);
    hour = ((int)(dtClock.hour >> 4)) * 10 + (int)(dtClock.hour & 0x0f);
    minute = ((int)(dtClock.minute >> 4)) * 10 + (int)(dtClock.minute & 0x0f);
    second = ((int)(dtClock.sec >> 4)) * 10 + (int)(dtClock.sec & 0x0f);
    *date = FAT_DATE(year, month, day);
    *time = FAT_TIME(hour, minute, second);
}
 
/* ====================== BME280 T-P-H Measurement ==========================*/
/*
 * Initiate BME280 and get calibration variables.
 */
void prepareBME280()
{
    uint8_t osrs_t = 1;             //Temperature oversampling x 1
    uint8_t osrs_p = 1;             //Pressure oversampling x 1
    uint8_t osrs_h = 1;             //Humidity oversampling x 1
    uint8_t mode = 3;               //Normal mode
    uint8_t t_sb = 5;               //Tstandby 1000ms
    uint8_t filter = 0;             //Filter off
    uint8_t spi3w_en = 0;           //3-wire SPI Disable
     
    uint8_t ctrl_meas_reg = (osrs_t << 5) | (osrs_p << 2) | mode;
    uint8_t config_reg    = (t_sb << 5) | (filter << 2) | spi3w_en;
    uint8_t ctrl_hum_reg  = osrs_h;
 
    writeReg(0xF2,ctrl_hum_reg);
    writeReg(0xF4,ctrl_meas_reg);
    writeReg(0xF5,config_reg);
    readTrim();
}
 
/*
 * Read calibration variables into structure.
 */
void readTrim()
{
    uint8_t data[32],i=0;
    Wire.beginTransmission(BME280_ADDRESS);
    Wire.write(0x88);
    Wire.endTransmission();
    Wire.requestFrom(BME280_ADDRESS,24);
    while(Wire.available()){
        data[i] = Wire.read();
        i++;
    }
     
    Wire.beginTransmission(BME280_ADDRESS);
    Wire.write(0xA1);
    Wire.endTransmission();
    Wire.requestFrom(BME280_ADDRESS,1);
    data[i] = Wire.read();
    i++;
     
    Wire.beginTransmission(BME280_ADDRESS);
    Wire.write(0xE1);
    Wire.endTransmission();
    Wire.requestFrom(BME280_ADDRESS,7);
    while(Wire.available()){
        data[i] = Wire.read();
        i++;   
    }
    dig_T1 = (data[1] << 8) | data[0];
    dig_T2 = (data[3] << 8) | data[2];
    dig_T3 = (data[5] << 8) | data[4];
    dig_P1 = (data[7] << 8) | data[6];
    dig_P2 = (data[9] << 8) | data[8];
    dig_P3 = (data[11]<< 8) | data[10];
    dig_P4 = (data[13]<< 8) | data[12];
    dig_P5 = (data[15]<< 8) | data[14];
    dig_P6 = (data[17]<< 8) | data[16];
    dig_P7 = (data[19]<< 8) | data[18];
    dig_P8 = (data[21]<< 8) | data[20];
    dig_P9 = (data[23]<< 8) | data[22];
    dig_H1 = data[24];
    dig_H2 = (data[26]<< 8) | data[25];
    dig_H3 = data[27];
    dig_H4 = (data[28]<< 4) | (0x0F & data[29]);
    dig_H5 = (data[30] << 4) | ((data[29] >> 4) & 0x0F);
    dig_H6 = data[31];  
}
 
/*
 * Write data into BME280 register.
 *    Argument: (uint8_t)Register address, (uint8_t)data.
 */
void writeReg(uint8_t reg_address, uint8_t data)
{
    Wire.beginTransmission(BME280_ADDRESS);
    Wire.write(reg_address);
    Wire.write(data);
    Wire.endTransmission();   
}
 
/*
 * Read data from BME280 register.
 */
void readData()
{
    int i = 0;
    uint32_t data[8];
    Wire.beginTransmission(BME280_ADDRESS);
    Wire.write(0xF7);
    Wire.endTransmission();
    Wire.requestFrom(BME280_ADDRESS,8);
    while(Wire.available()){
        data[i] = Wire.read();
        i++;
    }
    pres_raw = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4);
    temp_raw = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4);
    hum_raw  = (data[6] << 8) | data[7];
}
 
/*
 * Calibration [Temperature]
 */
signed long int calibration_T(signed long int adc_T)
{
    signed long int var1, var2, T;
    var1 = ((((adc_T >> 3) - ((signed long int)dig_T1<<1))) * ((signed long int)dig_T2)) >> 11;
    var2 = (((((adc_T >> 4) - ((signed long int)dig_T1)) * ((adc_T>>4) - ((signed long int)dig_T1))) >> 12) * ((signed long int)dig_T3)) >> 14;
     
    t_fine = var1 + var2;
    T = (t_fine * 5 + 128) >> 8;
    return T;
}
 
/*
 * Calibration [Pressure]
 */
unsigned long int calibration_P(signed long int adc_P)
{
    signed long int var1, var2;
    unsigned long int P;
    var1 = (((signed long int)t_fine)>>1) - (signed long int)64000;
    var2 = (((var1>>2) * (var1>>2)) >> 11) * ((signed long int)dig_P6);
    var2 = var2 + ((var1*((signed long int)dig_P5))<<1);
    var2 = (var2>>2)+(((signed long int)dig_P4)<<16);
    var1 = (((dig_P3 * (((var1>>2)*(var1>>2)) >> 13)) >>3) + ((((signed long int)dig_P2) * var1)>>1))>>18;
    var1 = ((((32768+var1))*((signed long int)dig_P1))>>15);
    if (var1 == 0)
    {
        return 0;
    }   
    P = (((unsigned long int)(((signed long int)1048576)-adc_P)-(var2>>12)))*3125;
    if(P<0x80000000)
    {
       P = (P << 1) / ((unsigned long int) var1);  
    }
    else
    {
        P = (P / (unsigned long int)var1) * 2;   
    }
    var1 = (((signed long int)dig_P9) * ((signed long int)(((P>>3) * (P>>3))>>13)))>>12;
    var2 = (((signed long int)(P>>2)) * ((signed long int)dig_P8))>>13;
    P = (unsigned long int)((signed long int)P + ((var1 + var2 + dig_P7) >> 4));
    return P;
}
 
/*
 * Calibration [Humidity]
 */
unsigned long int calibration_H(signed long int adc_H)
{
    signed long int v_x1;
     
    v_x1 = (t_fine - ((signed long int)76800));
    v_x1 = (((((adc_H << 14) -(((signed long int)dig_H4) << 20) - (((signed long int)dig_H5) * v_x1)) +
              ((signed long int)16384)) >> 15) * (((((((v_x1 * ((signed long int)dig_H6)) >> 10) *
              (((v_x1 * ((signed long int)dig_H3)) >> 11) + ((signed long int) 32768))) >> 10) + (( signed long int)2097152)) *
              ((signed long int) dig_H2) + 8192) >> 14));
    v_x1 = (v_x1 - (((((v_x1 >> 15) * (v_x1 >> 15)) >> 7) * ((signed long int)dig_H1)) >> 4));
    v_x1 = (v_x1 < 0 ? 0 : v_x1);
    v_x1 = (v_x1 > 419430400 ? 419430400 : v_x1);
    return (unsigned long int)(v_x1 >> 12);  
}
 
/* ========================= BH1750 Illuminance =============================*/
/*
 * Initiate BH1750
 */
void prepareBH1750(int i2cAddress)
{
    Wire.beginTransmission(i2cAddress); 
    Wire.write(0x10);
    Wire.endTransmission();
  delay(180);
}
 
/*
 * Measure illuminance
 */
int measureIlluminance(int i2cAddress)
{
    uint16_t val = 0;
    byte buf[2];
    if (measureIlluminance(i2cAddress, buf) == 2){
        val = ((buf[0] << 8) | buf[1]) / 1.2;   // Calculate
        return (int)val;
    }
    return 0;
int measureIlluminance(int i2cAddress, byte *buff)
 {
    int i = 0;
    *buff = 0x00;
    Wire.beginTransmission(i2cAddress);
    Wire.requestFrom(i2cAddress, 2); 
    while (Wire.available()) 
    {
        *(buff + i) = Wire.read();
        i++;
    }
    Wire.endTransmission(); 
    return i;
 }
 
/* ========================= YL-38 Soil moisture ============================*/
/*
 * Measure soil moisture
 *    Return: moisture value(0~100)
 */
int measureSoilMoisture(int pin_no)
{
    int val = 1023 - analogRead(pin_no);
    return (int)map(val, 56, 545, 0, 100);
}

 
Copyright (C) 2011-2025 Marchan, All rights reserved.