本システム開発は、2017年に取組んで以来4年を経過している。最新版では、光反射センサーによる入力回路の安定化、およびIoT(Internet of Things)化によるリアルタイム機能の付加を計り、水道メーターWEB監視システムとして実運用しているので、ここにシステムの詳細を紹介する。
水道メーターWEB監視システムの概要
システム開発の経緯
事の始まりは2012年10月の水道メーター検針だった。市の水道メーター検針で465,246円の請求書が来たのに驚いて、市の水道局に駆け込んだ。
市の水道局担当者に対策を聞くと、「水道メーターを毎日チェックするしかありません」とのことだったので、自前で漏水検知の方法を検討した。
最終的に、水道メーターに赤外線反射型センサーを取り付け、のパイロット(ローター)の回転時に反射する光パルスをマイコン(当初はSTM32)の割込みピンに入力する方法を取った。
パルスはマイコン内で処理され、通水、停止のイベント処理、設定時間を超えた時のアラーム処理などを行い、データはシリアル通信で接続されたノートパソコンを経由してサーバー(当初はラズベリーパイ)に送ることで、内容をブラウザから見ることが出来る。
その後、マイコンをWiFi機能付きのESP-WROOM-2マイコンに変更し、直接サーバーに送る(ノートパソコンが不要となる)ようにしたり、センサー入力部の改良などを行い、1年半ほどこのシステムにて運用を続けていた。詳細は、水道漏水検知システムの開発を参照。
今回、大幅な改良を行い長期運用に耐え得るシステムとなった。
システム構成
下記にシステム構成図を示す。
水道漏水検知マイコンからは、一定周期でデータ収集を行いレンタルサーバーに保存される。このデータは自宅内外でブラウザにてモニタリング出来る。(5秒おきのデータ自動更新)
一方で、水道漏水検知マイコンはサーバー機能を持ち、水道メーターのリアルタイム監視が可能となり、通水や停止状態およびその継続時間の他、流量のカウント値、センサー入力バイアス抵抗調整用切換えリレー出力などが、同様に自宅内外から行える。
最新改良版の詳細
改良ポイントと最新回路図(2021.5.28時点)
今回の改良のポイントを下記に示す。
- ブレッドボードで組んでいた回路を基板上に組んだ。(ハンダ付けにて回路基板製作)
- データログサーバーを自宅LAN上ラズベリーパイからレンタルサーバーに切換えた。
- センサー入力部のバイアス抵抗を可変抵抗による手動調整とリレー切換えによる8段階自動調整可能とした。
- センサー信号をこれまでのIOポート(IO4)入力によるon/offのデジタル信号から0-1Vのアナログ信号入力とし、波形の変化をアナログ値で読取る構成とした。
- マイコン(ESP-WROOM-2)のサーバー機能を利用し、自宅内外からリアルタイムで監視、およびセンサー入力部のバイアス抵抗のリレー切換えを可能とした。
以上の変更による最新の回路図を次に示す。
改良ポイントの詳細
ブレッドボードから回路基板製作
これまでブレッドボードで運用して来たのは、時々発生する回路変更に対応するためであったが、ここ1年以上変更がないためと安定した運用を計るためハンダ付けにて基板回路を構成した。
基板はマイコン基板とリレー基板でそれぞれ、ジャンパー線にて結合している。
ラズベリーパイからレンタルサーバーへ
昨年12月のADSL回線から光回線への切換えを機に、ラズベリーパイでのサーバー運用を見直し、レンタルサーバーへのデータ収集に切替えた。
理由としては、光回線で上り最大1Gbpsの超高速光回線で速度が速くなったのと、レンタルサーバーでの運用でブラウザでの監視に家庭内LANへの負荷を無くすためだった。
ブラウザ監視画面を下記に示す。
センサー入力部のバイアス抵抗切換え
水道メーターのパイロット(ローター)からの反射光パルスはセンサーユニットをセットした状態や結露などの影響で信号強度に若干の変動があるので入力部のバイアス抵抗にて調整する必要がある。
調整用の抵抗は10kΩの可変抵抗と10k、20k、47kΩのリレー切換え抵抗である。
特に検針があった後などは、検針員がセンサーユニットを取外し、再取付けするのでいくらかのズレが生じる。
このため検針後の信号強度を見て手動で調整を行っている。(2か月に1度)
調整は可変抵抗値を手動で回して行うが、抵抗値をゼロオームから回し、「入力パルス表示LED」が点灯する時点で止める。
次に3つの固定抵抗をリレー切換えにて行うが、ブラウザから自動調整を指示することが出来る。(後述)
自動調整要領は「入力パルス表示LED」が点灯から消灯する時点まで抵抗値の組合せを変えながら合成抵抗値を下げて行く方法を取っている。
センサー信号をアナログ入力
まず、ESP-WROOM-2マイコンのアナログ入力は0-1Vになっているので、分圧回路にて0-3.3Vから0-.0Vに分圧する。ここでは、47kと20kの抵抗を直列に繋ぎ、結合点から信号をTOUTに入力している。
取込み後の処理は、AD変換された値(0-1023)を監視して、上昇している場合、順次MAX値を更新して行く。下降に転じた場合、その差が一定値(例えば5%)になった時、水道メーターのパイロットが回転したと捉えフローカウント値をインクリメントする。
逆に下降している場合は、順次MIN値を更新して行く。下降が止まり上昇に転じた時、その差が一定値(例えば5%)になった時、その水道メーターのパイロットが回転したと捉えフローカウント値をインクリメントする。
この繰り返しを行うことで、パイロットの回転を検出し、フローの確認を行っている。
リアルタイム監視機能、センサー入力バイアス抵抗の自動調整機能等の付加
ESP-WROOM-2マイコンにはサーバー機能があるので、これを使ってWEBサーバーとして動作させ、リアルタイムでの監視機能、およびリレー切換えによるセンサー入力自動バイアス抵抗値調整機能を付加した。
リアルタイム監視の画面イメージを下記に示す。
ソフトウェア
ESP-WROOM-2マイコン側のプログラム
[code]
/* ロケーション 102
温度、湿度、水道メーター
2021/05/15:WaterMeter_2V205
入力信号を0-5Vに分圧してADCを使う。
2021/05/15
(WaterMeter_2V204): 通水・停止時間の計測基準を内部1秒カウンターからtime()関数に変更する。
ウォッチドッグリセットが多いので、割込みを変える。
パターン1:タイマー割込みで周期的に信号を見るのは、オシロで信号確認されるもFlo_wCountに反映されないことが多い。
パターン2:信号割込みでカウントおよび割込み禁止をして、タイマー割込みで許可を行う。⇒カウントするが、反応が遅い(小流量の時は検出されにくい)
カウント値は1秒で1~3カウントアップ程度。
パターン3:一定期間の信号割込みを試してみる。1/4程度。
1/5程度では、パターン2と変わらない
1/2では同じ
全割込みでもトイレの手洗い水では、1秒に多数カウントするが、次の1秒は0で同じ。
割込み多い分安心感がある。検出確率は少し上。後は、ウォッチドッグリセットがどうか?2hrでリセット。
それで1/3にする。
2021/05/03
基板化に伴う変更
2021/01/11
Fon光回線を契約後、サーバーをRaspberryPi3からxxx.sakura.ne.jpに移行する。(回線速度が速くなり、インターネット経由でのデータ保存、モニタが問題なくなったので)
Ver.3.4: センサーへの給電に電流加算用トランジスタを追加(抵抗は1k)
2017/09/27: 液晶表示追加、
2017/09/30: センサーへの給電は、IO0の状態でのみ有効とする。
また、抵抗値を1kから10kに変更する。
(目的は、強制的にON/OFFさせる
2018/10/27:Ver1.04は、フォトトランジスタ入力信号のベースバイアス抵抗をリレーで切替える、ベース電流に自己バイアスを加え自動調整にすることを追加
*/
define PROG_NAME "WaterMeter_2V205"
extern "C" {
include "user_interface.h"
}
define LED_BLINK 13
define LED_INTR 16 // パルス入力の値をそのままLED表示する
define InteruptInputPin 4
// #define AddCurrent 5 //電流可変用(テストでパルスを発生させるために置いている)
define SDA 2
define SCL 14
define Relay0 15
define Relay1 5
define Relay2 12
define ManualTunePin 0
define Cnt_WiFi_Failure 3
define JST 3600* 9
define Timer_Interrupt 10
define Count_OneSec 100
define Count_100mSec 10
define SensorPin A0
LiquidCrystal_I2C lcd(0x3f,16,2); //I2Cアドレス入力 1回目購入分は、0x3f, 2回目は、0x27, 16,2
//
IPAddress ip(192, 168, 1, 111); //ip
IPAddress gateway(192,168, 1, 1);
IPAddress subnet(255, 255, 255, 0);
IPAddress DNS(192, 168, 1, 1);
//MAC Address 5c:cf:7f:ec:31:4e
//
// comment WiFiServer server(80);
ESP8266WebServer server(80); // ポート番号 (HTTP 80) // 0504追加
Ticker ticker1;
Ticker ticker2;
int TimeDisplay = 5; // サーバーへの記録が一定時間無くなる原因として、符号なしで宣言していたため非常に大きな値となっていたことが考えられる。符号付として負の値とする。
int AlarmDisplay = 0;
unsigned long Flow_Count = 0;
bool Int_Flag = false;
unsigned long Q_FlowTime = 0;
unsigned long Previous_Flow_Count = 0; // from unsigned
bool Flow_Status = false;
unsigned long Previous_Time; // from unsigned
const unsigned int Alarm_Set_Time_Flow = 1560; // 1560;sec
const unsigned int Alarm_Set_Time_Stop = 18060; // 18060 = 36060;sec
bool Alarm_FlowTimeExess = false;
bool Alarm_StopTimeExess = false;
int Meter_Rotate=0;
const int Meter_Rotate_Stop = 20; // 30sec→10sec→20sec→15sec(異常停止のため)→20secへ
const int Meter_Rotate_Flow = 2;
unsigned long Duration_Time = 0; // from unsigned
unsigned long GetCounter = 0; // from unsigned
static int cnt_wifi_fail = Cnt_WiFi_Failure;
static int led_state = 0;
// ssid
const char* ssid2 = "myssid2";
const char* ssid = "myssid2";
const char* password = "password";
// xxxのサーバーに変更
const char* host = "xxx.sakura.ne.jp";
static int one_sec=0;
struct DATA_SET{
int num;
char date[25]; // 2021/05/11(Tue) 18:45:00
// 123456789012345678901234
char prog_name[25]; // (WaterMeter_2V203)
// 1234567890123456789
};
DATA_SET data;
time_t t;
time_t t_start;
struct tm *tm;
static const char *wd[7] = {"Sun","Mon","Tue","Wed","Thr","Fri","Sat"};
void adcRead(){
int sensorValue = system_adc_read();
Serial.println("sensorValue= "+ sensorValue);
}
void handleRoot() {
long int relay;
long int cur_relay;
char buf[20] ;
// server.send(200, "text/plain", "hello from esp8266!");
// Status: 192.168.1.111/waterMeter?S
Relay Operation: 192.168.1.111/waterMeter?R=000
Auto Tuning: 192.168.1.111/waterMeter?T";
// Read the first line of the request
String reqName = server.argName(0);
String req = server.arg(0);
// Match the request
relay = req.toInt();
cur_relay = digitalRead(Relay2)100 + digitalRead(Relay1)10 + digitalRead(Relay0);
// Serial.println("server request: " + reqName + "=" + req + ":relay = " + String(relay));
if(reqName == "R" && (relay != cur_relay)){ switch (relay){ case 0: case 1: case 10: case 11: case 100: case 101: case 110: case 111: relay_out(Relay0, relay %10); relay_out(Relay1, int((relay %100)/10)); relay_out(Relay2, int((relay %1000)/100)); break; default: break; } }else if(reqName == "T"){ autotune(); } if(reqName == "R" || reqName == "S" || reqName == "T" ){ t = time(NULL); // response
// String s = "
\n";
String s = "
Water Meter Realtime Monitor
\
\
table { border-collapse: collapse; text-align: center; border: 1px solid #333; \ background-color: #2c88d9; color: #FFF; width=\"640\" } td{ padding: 3px 7px;} body{ background-color:#dddddd; color : #000000; text-align: center; }\
Water Meter Realtime Monitor
Flow Status
\ Pilot Reflection: "; s += digitalRead(InteruptInputPin) ? "▼":"▲"; s += "
"; s += " "; s += Flow_Status ? "Flow":"Stop"; s += "
" + String(Duration_Time / 3600) + "h " + String((Duration_Time % 3600) / 60) + "m " + String((Duration_Time % 3600) % 60) + "s
Relay: " + String(digitalRead(Relay2))+ String(digitalRead(Relay1))+ String(digitalRead(Relay0)) + \ "
Count: " + String(Flow_Count) + "
Relay Output Button
"; s += "Relay 000
\ Relay 001
\ Relay 010
\ Relay 011
\ Relay 100
\ Relay 101
\ Relay 110
\ Relay 111
\ Auto Tuning
"; s += "Relay Output Procedures
If Status is Low (LED-ON), try 000 then 001->111 to get High.
\ If Status is High(LED-OFF), try 111 then 110->000 to get Low."; s += "Water Meter Database
https://takashi.xii.jp/watermeter"; s += "
Running: " + String((t - t_start)/3600) + "hr " + String(((t - t_start)%3600)/60) + \ "min since: " + String(data.date) + " on: \"" + String(data.prog_name) + "\" (" + String(data.num) + " times)\n";
// Send the response to the client server.send(200, "text/html", s); /*}else if( reqName == "F") { t = time(NULL); // response String s = "<!DOCTYPE html><html lang=\"jp\"><head><meta charset=\"UTF-8\"><title>Water Meter Realtime Data</title>\ <meta http-equiv=\"refresh\" content=\"1;URL=../waterMeter?F\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\ <meta name=\"robots\" content=\"noindex,nofollow\"><style> table { border-collapse: collapse; text-align: center; border: 1px solid #333; \ background-color: #2c88d9; color: #FFF; width=\"640\" } td{ padding: 3px 7px;} body{ background-color:#dddddd; color : #000000; text-align: center; }\ </style></head><body><table align=\"center\" style=\"margin-bottom:5px;\"> <tr> <td>Pilot Reflection: "; s += digitalRead(InteruptInputPin) ? "<font color=\"black\">▼":"<font color=\"red\">▲"; s += "</font></td> </tr></table><table align=\"center\"> <tr align=\"center\"> <td width=\"100px\">"; s += Flow_Status ? "<font size=\"+1\" color=\"red\"><b>Flow</b>":"<font color=\"white\">Stop"; s += "</td> <td>" + String(Duration_Time / 3600) + "h " + String((Duration_Time % 3600) / 60) + "m " + String((Duration_Time % 3600) % 60) + "s</font></td> </tr> "; s += "<tr align=\"center\"> <td>Relay: " + String(digitalRead(Relay2))+ String(digitalRead(Relay1))+ String(digitalRead(Relay0)) + \ "</td> <td>Count: " + String(Flow_Count) + "</td> </tr></table>"; s += "<br /><b>Running: " + String((t - t_start)/3600) + "hr " + String(((t - t_start)%3600)/60) + \ "min</b> since: " + String(data.date) + "<br />on: " + String(data.prog_name) + " " + String(data.num) + " times</body></html>\n"; server.send(200, "text/html", s);*/ }else{ handleNotFound(); }
}
void handleNotFound(){
String message = "File Not Found\n\n";
message += "URI: ";
message += server.uri();
message += "\nMethod: ";
message += (server.method() == HTTP_GET)?"GET":"POST";
message += "\nArguments: ";
message += server.args();
message += "\n";
for (uint8_t i=0; i<server.args(); i++){
message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
}
server.send(404, "text/plain", message);
}
//*
void autotune(){
Serial.println("AutoTune Start");
relay_out(Relay0, 0);
relay_out(Relay1, 0);
relay_out(Relay2, 0);
delay(1000);
if (digitalRead(InteruptInputPin) == LOW){
relay_out(Relay0, 1);
relay_out(Relay1, 0);
relay_out(Relay2, 0);
delay(1000);
}
if (digitalRead(InteruptInputPin) == LOW){
relay_out(Relay0, 0);
relay_out(Relay1, 1);
relay_out(Relay2, 0);
delay(1000);
}
if (digitalRead(InteruptInputPin) == LOW){
relay_out(Relay0, 1);
relay_out(Relay1, 1);
relay_out(Relay2, 0);
delay(1000);
}
if (digitalRead(InteruptInputPin) == LOW){
relay_out(Relay0, 0);
relay_out(Relay1, 0);
relay_out(Relay2, 1);
delay(1000);
}
if (digitalRead(InteruptInputPin) == LOW){
relay_out(Relay0, 1);
relay_out(Relay1, 0);
relay_out(Relay2, 1);
delay(1000);
}
if (digitalRead(InteruptInputPin) == LOW){
relay_out(Relay0, 0);
relay_out(Relay1, 1);
relay_out(Relay2, 1);
delay(1000);
}
if (digitalRead(InteruptInputPin) == LOW){
relay_out(Relay0, 1);
relay_out(Relay1, 1);
relay_out(Relay2, 1);
delay(1000);
}
Serial.println("AutoTune End");
}
void relay_out(int rel_no, int on_off){
if(rel_no == Relay0){
Serial.println("Reley0(IO-" + String(rel_no) + "), on_off:" + String(on_off));
digitalWrite(Relay0, on_off);
}else if(rel_no == Relay1){
Serial.println("Reley1(IO-" + String(rel_no) + "), on_off:" + String(on_off));
digitalWrite(Relay1, on_off);
}else if(rel_no == Relay2){
Serial.println("Reley2(IO-" + String(rel_no) + "), on_off:" + String(on_off));
digitalWrite(Relay2, on_off);
}
}
void LCD_Display(){
String LCD_disp1 = Flow_Status ? "Flow ":"Stop ";
// 桁表示のズレを修正 LCD_disp1 += String(Duration_Time / 3600) + "h";
String hh = String(Duration_Time / 3600);
String disp_h = " " + hh;
// Serial.println("LCD_disp1: hh=" + hh + "disp_h=" + disp_h);
LCD_disp1 += disp_h.substring(hh.length()-1, hh.length()+3) + "h";
LCD_disp1 += (String((Duration_Time % 3600) / 60).length() == 1) ? "0":"";
LCD_disp1 += String((Duration_Time % 3600) / 60) + "m";
LCD_disp1 += (String((Duration_Time % 3600) % 60).length() == 1) ? "0":"";
LCD_disp1 += String((Duration_Time % 3600) % 60) + "s";
String LCD_disp2 = "R" + String(digitalRead(Relay2)) + String(digitalRead(Relay1)) + String(digitalRead(Relay0)) + " C "+ String(Flow_Count);
//50,000,000***
// lcd.clear();
lcd.setCursor(0, 0);
lcd.print(LCD_disp1);
lcd.setCursor(0, 1);
lcd.print(LCD_disp2);
}
void check_flow_status(){
// detachInterupt();
static int Meter_Rotate_Valid = 0;
if(Flow_Count>= 50000000 ){
// noInterrupts();
Flow_Count = Flow_Count - 50000000;
// interrupts();
}
// 水道メーターのローター回転状態に応じた処理
Duration_Time = GetCounter - Previous_Time;
if( Duration_Time<0 )
Duration_Time += 86400;
// checking flow or stop
if(Flow_Status){ // it is in FLOW STATUS
if( (Previous_Flow_Count == Flow_Count) ){ // change to STOP
++Meter_Rotate;
if(Meter_Rotate>= Meter_Rotate_Stop){
Flow_Status = false;
Meter_Rotate = 0;
if(Alarm_FlowTimeExess){
Alarm_FlowTimeExess = false;
AlarmDisplay = 9;
}
AlarmDisplay = AlarmDisplay | 0x10;// FlowStatusOFF (000100000)
Q_FlowTime = Q_FlowTime + Duration_Time;
if(Q_FlowTime>= 86400) Q_FlowTime = 0;
Duration_Time = 0;
Previous_Time = GetCounter;
// Enable time update
}
}else{ // continue FLOW or start flowing again within Meter_Rotate_Set. Meter_Rotate = 0; if((!Alarm_FlowTimeExess) && (Duration_Time>= Alarm_Set_Time_Flow) ){ Alarm_FlowTimeExess = true; // Enable time update AlarmDisplay = 0xb; } Previous_Flow_Count = Flow_Count; }
}else{ // Flow_Status = false. (停止中 isn't flowing. It's in STOP STATUS.)
if( Flow_Count != Previous_Flow_Count ){ // パルスのON/OFFが継続するのに対する対策⇒累積カウントで6パルス以上でないと流れとみなさない。→(累積カウントとする)→割込みを使わない場合は、カウント値の変化でみる。
++Meter_Rotate; //20210514****通水停止が頻繁になるため、1秒毎のカウントが6以上の通水がMeter_Rotate設定回数(5秒)以上すると通水とする。
Serial.println("Meter_Rotate: " + String(Meter_Rotate) + " Previous_Flow_Count: " + String(Previous_Flow_Count) + " Flow_Count: " + String(Flow_Count)); Previous_Flow_Count = Flow_Count; //**********20210514******** if( (Meter_Rotate>= Meter_Rotate_Flow) ){ // 一定のメーターの回転数が認められる。 Flow_Status = true; Meter_Rotate = 0; Meter_Rotate_Valid = 0; Duration_Time = 0; Previous_Time = GetCounter; if(Alarm_StopTimeExess){ Alarm_StopTimeExess = false; AlarmDisplay = 5; } AlarmDisplay = AlarmDisplay | 0x30;// FlowStatusOFF (00110000) // Enable time update Previous_Flow_Count = Flow_Count; } //*********20210514********* }else{ // 回転していない。 if((!Alarm_StopTimeExess) && (Duration_Time >= Alarm_Set_Time_Stop) ){ Alarm_StopTimeExess = true; // Enable time update AlarmDisplay = 7; } if( (Meter_Rotate >= 1) && (Meter_Rotate_Valid < Meter_Rotate_Flow )) { // 最初の回転を認めた後、一定の周回内で更に回転を認めるため、Meter_Rotateの有効期間を2秒とする(低流量に対応) ++Meter_Rotate_Valid; }else{ Meter_Rotate = 0; Meter_Rotate_Valid = 0; } Previous_Flow_Count = Flow_Count; // 1秒毎のカウントを見るため、リセットする。(累積カウントを認めない)//**********20210514******** }
}
}
void count_down() { // 1秒ごとの処理
one_sec =1;
t = time(NULL);
GetCounter = t - t_start;
TimeDisplay--; // サーバーへ送信のタイミング
}
void roter_count() { // Timer_Interrup msec タイマー割込み処理。Flow_Countカウントを行う。Int_Flag(パルス入力フラグ)をハードウェア割込みでセットして、ここでリセットする。
//static int prev_pin_state = 0;
//static int pin_state = 0;
static int count_down_timer = Count_OneSec;
static int count_down_timer_100ms = Count_100mSec;
static int interrupt_enable_count = 4;
static int adc_average_count = 5;
if(count_down_timer-- <= 0){
count_down();
count_down_timer = Count_OneSec;
}
/*
if(count_down_timer_100ms-- <= 0){ if( (Meter_Rotate>= 1) || Flow_Status ){
led_state = !led_state;
digitalWrite(LED_BLINK, led_state);
}
count_down_timer_100ms = Count_100mSec;
} */
// Flow_Count increment
//** パターン1:タイマー割込みで検出
/* pin_state = digitalRead(InteruptInputPin);
if( pin_state != prev_pin_state ) {
++Flow_Count;
prev_pin_state = pin_state;
}*/
//** パターン2:信号割込みで検出、割込み禁止
/* if( Int_Flag ) {
Int_Flag = !Int_Flag;
attachInterrupt(digitalPinToInterrupt(InteruptInputPin), interupt_led, CHANGE);
}*/
//** パターン3:
/* if(interrupt_enable_count-- <= 0){
interrupt_enable_count = 2; // 1/3の割込み許可
attachInterrupt(digitalPinToInterrupt(InteruptInputPin), interupt_led, CHANGE);
}else{
detachInterrupt(digitalPinToInterrupt(InteruptInputPin));
}*/
//** パターン4:
//全期間の信号割込みでカウントでは、ウォッチドッグリセットがかかるので、使用しない。
// Flow_Count = Flow_Count & 0xfffffff; // 0 - 268435455
//** パターン5:AD変換を行いアナログ入力にて波形から回転を判断する。
// 10msecタイマー割込みでロータリーバッファに取込む(5個)
//50msec毎に平均を取り、値の大⇒小⇒大の波形から回転判断とする。
static int rotary_buf_cnt = 0;
static int sensorValue[5];
static int sensorAve = 0;
static int sensorLarge = 0;
static int sensorSmall = 0;
static bool sensorValDir = true; // 小⇒大でTRUE、大⇒小で、FALSEとする。
sensorValue[rotary_buf_cnt++ %5] = system_adc_read(); if(rotary_buf_cnt>= 5){ // 50msec 周期処理 rotary_buf_cnt = 0; int sum =0; for(int i=0; i<5; i++){ sum += sensorValue[i]; } sensorAve = sum/5; if(sensorValDir){ //小⇒大 if( sensorAve > sensorLarge ){ sensorLarge = sensorAve; }else if(sensorAve < (sensorLarge-50)){ sensorValDir = !sensorValDir; sensorSmall = sensorLarge; ++Flow_Count; } }else{ //大⇒小 if( sensorAve < sensorSmall){ sensorSmall = sensorAve; }else if(sensorAve > (sensorSmall + 50)){ sensorValDir = !sensorValDir; sensorLarge = sensorSmall; ++Flow_Count; } } }
}
void interupt_led(){ // ハード割込みを使った場合、オシロでは信号が振れていないのに、カウントアップして行く。また割込み頻度が多く、多重割込み(?)でウォッチドッグがかかる
/*
detachInterrupt(digitalPinToInterrupt(InteruptInputPin));
Flow_Count++;
Int_Flag = true;*/
}
void load_data() {
EEPROM.get(0, data);
Serial.println("data load: " + String(data.num) + " " +String(data.date) + " " +String(data.prog_name));
if (strcmp(data.prog_name, PROG_NAME)) { //プログラム名バージョンが更新された
data.num = 0;
strcpy(data.prog_name, PROG_NAME);
Serial.println("Program Name was updated!: " + String(data.num) + " " +String(data.date) + " " +String(data.prog_name));
}
}
void wifiConnection(){ // サブプログラムとする
// We start by connecting to a WiFi network
Serial.println();
Serial.println();
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.mode(WIFI_STA);
WiFi.config(ip, gateway, subnet, DNS);
//*
WiFi.disconnect();
//*
WiFi.begin(ssid, password);
Serial.println("");
int cnt_wifi_connect_fail = 30;
while (WiFi.status() != WL_CONNECTED) {
delay(500);
// WiFi接続に問題がある場合、 if(cnt_wifi_connect_fail == 15){ Serial.println("Fail connecting 5 times! disconnectして ssid2 に切替えて試みる!!"); WiFi.disconnect(); WiFi.begin(ssid2, password); // ssid2 に切替える delay(3000); } if(cnt_wifi_connect_fail <= 0){ Serial.println("Fail connecting 10 times! ESP Restart!!"); delay(5000); ESP.restart(); } Serial.print("."+ String(cnt_wifi_connect_fail)); cnt_wifi_connect_fail--;
}
//下記は、上の接続と同じなのかも****
while (WiFi.waitForConnectResult() != WL_CONNECTED) {
Serial.println();
Serial.println("Fail connecting");
delay(5000);
ESP.restart();
}
Serial.println("");
Serial.println("WiFi connected" );
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
delay(100);
//0504追加
if (MDNS.begin("WaterMeter")) {
Serial.println("MDNS responder started");
}
server.on("/waterMeter", handleRoot);
server.on("/", [](){
server.send(200, "text/plain", "Access Denied!");
});
server.onNotFound(handleNotFound);
server.begin();
Serial.println("HTTP server started");
}
//EEPROMへの保存
void save_data() {
EEPROM.put(0, data);
EEPROM.commit(); //大事
}
void setup() {
Serial.begin(115200);
delay(10);
Wire.begin(SDA,SCL); //I2C通信で使用するピンを設定
lcd.init();
lcd.backlight();
pinMode(LED_BLINK, OUTPUT);
pinMode(LED_INTR, OUTPUT);
pinMode(Relay0, OUTPUT);
pinMode(Relay1, OUTPUT);
pinMode(Relay2, OUTPUT);
pinMode(InteruptInputPin, INPUT_PULLUP); //INPUT);
pinMode(ManualTunePin, INPUT);
delay(100);
wifiConnection();
/ここからサブルーチンに
// We start by connecting to a WiFi network
Serial.println();
Serial.println();
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.mode(WIFI_STA);
WiFi.config(ip, gateway, subnet, DNS);
//*
WiFi.disconnect();
//*
WiFi.begin(ssid, password);
Serial.println("");
int cnt_wifi_connect_fail = 30;
while (WiFi.status() != WL_CONNECTED) {
delay(500);
// WiFi接続に問題がある場合
if(cnt_wifi_connect_fail == 15){ Serial.println("Fail connecting 5 times! disconnectして ssid2 に切替えて試みる!!");
WiFi.disconnect();
WiFi.begin(ssid2, password); // ssid2 に切替える
delay(3000); }
if(cnt_wifi_connect_fail <= 0){
Serial.println("Fail connecting 10 times! ESP Restart!!");
delay(5000);
ESP.restart(); }
Serial.print("."+ String(cnt_wifi_connect_fail));
cnt_wifi_connect_fail--;
}
//下記は、上の接続と同じなのかも****
while (WiFi.waitForConnectResult() != WL_CONNECTED) {
Serial.println();
Serial.println("Fail connecting");
delay(5000);
ESP.restart();
}
Serial.println("");
Serial.println("WiFi connected" );
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
delay(100);
//0504追加
if (MDNS.begin("WaterMeter")) {
Serial.println("MDNS responder started");
}
server.on("/waterMeter", handleRoot);
server.on("/", [](){
server.send(200, "text/plain", "Access Denied!");
});
server.onNotFound(handleNotFound);
server.begin();
Serial.println("HTTP server started");
//ここまで */
// ticker1.attach_ms(1000, count_down);
ticker2.attach_ms(Timer_Interrupt, roter_count); // check input pulse level every Timer_Interrup msec
// attachInterrupt(digitalPinToInterrupt(InteruptInputPin), interupt_led, CHANGE); // from RISING 割り込みを設定する
// EEPROM に書込み
EEPROM.begin(1024); //1kbサイズ
load_data();
// NTP同期
configTime( JST, 0, "ntp.nict.jp", "ntp.jst.mfeed.ad.jp"); // esp8266/arduino ver 2.6.3まで有効
// 曜日文字列配列
static const char *pszWDay[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
// 現在時刻の取得
do{
delay(50);
t = time(NULL);
Serial.println("t= " + String(t));
}while(t<50000L); t_start = t; tm = localtime(&t); sprintf(data.date, "%04d/%02d/%02d(%s) %02d:%02d:%02d", tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday,
wd[tm->tm_wday],
tm->tm_hour, tm->tm_min, tm->tm_sec);
data.num++;
Serial.println("data save(1sec): " + String(data.num) + " " +String(data.date) + " " +String(data.prog_name));
save_data();
// Relay Initial settings
autotune();
}
void loop() {
static int relay_cnt;
digitalWrite(LED_INTR, digitalRead(InteruptInputPin) ); //LED_INTRはセンサー入力に従う
if(Flow_Status){
digitalWrite(LED_BLINK, Flow_Count%2 ); // LED_BLINKの点滅はカウント値に従う (流れている時)
}
if(one_sec==1){
check_flow_status(); LCD_Display(); if(!Flow_Status){ led_state = !led_state; digitalWrite(LED_BLINK, led_state ); // LED_BLINKの点滅は、流れてない時は2秒周期 } one_sec = 0;
}
/* If 1sec has passed */
if ( (TimeDisplay <= 0) || (AlarmDisplay & 0x11) ){
int THH, TMM, TSS; /* Compute hours */ THH = Duration_Time / 3600; /* Compute minutes */ TMM = (Duration_Time % 3600) / 60; /* Compute seconds */ TSS = (Duration_Time % 3600) % 60; //Stringを使う。メモリを多く消費するが、バッファーオーバーフロー対策として・・・ String str = "count=" + String(Flow_Count) + "&status="; str += Flow_Status ? "Flow":"Stop"; str += "&duration=" + String(THH) + "h" + String(TMM) + "m" + String(TSS) + "s&qflowtime="; str += Flow_Status ? String(Q_FlowTime + Duration_Time):String(Q_FlowTime); if(AlarmDisplay & 0x11){ Serial.print("***Event*** TimeDisplay = " + String(TimeDisplay)); //データがサーバー上に更新されない時に警報メッセージのタイミングでTimeDisplay値を見る switch(AlarmDisplay & 0xf){ //下4桁がアラーム case 0xb: // Flow Excess On str += "&alarm=FlowTimeExcess&on_off=ON"; break; case 0x9: // Flow Excess Off str += "&alarm=FlowTimeExcess&on_off=OFF"; break; case 0x7: // Stop Excess On str += "&alarm=StopTimeExcess&on_off=ON"; break; case 0x5: // Stop Excess Off str += "&alarm=StopTimeExcess&on_off=OFF"; break; } switch(AlarmDisplay & 0x30){ // 上4桁がイベント case 0x30: // Flow Status On str += "&event=FlowStart"; break; case 0x10: // Flow Status Off str += "&event=FlowStop"; break; } AlarmDisplay = 0; } Serial.print(str ); Serial.println( PROG_NAME); // 水道メーター情報をサーバーへ送る // Use WiFiClient class to create TCP connections WiFiClient client; const int httpPort = 80;
// Serial.println("connection: host= " + String(host) + ", httpPort= " + String(httpPort));
if (!client.connect(host, httpPort)) { Serial.println("TimeDisplay = " + String(TimeDisplay) + ", WM connection failed: host= " + String(host) + ", httpPort= " + String(httpPort)); TimeDisplay = 20; // 2018.08.27 接続エラー発生時、TimeDisplayの値は常にマイナスとなり、接続エラーが継続するため、20秒間隔で繰り返すよう値を20秒にセットする。 delay(1000); if(cnt_wifi_fail-- <= 2){ //WiFi接続エラーが一定回数経過後、再接続する。 wifiConnection(); }else if(cnt_wifi_fail-- <= 0){ //WiFi接続エラーが一定回数経過後、リスタートする cnt_wifi_fail = Cnt_WiFi_Failure; ESP.restart(); } return; } cnt_wifi_fail = Cnt_WiFi_Failure; // *** TEST *** String url = "/watermeter/receive_data_test.php"; *** TEST *** String url = "/watermeter/receive_data.php";
// This will send the request to the server ); // + "Connection: close\r\n\r\n"); , // , httphost, port)
client.print(String("POST ") + url + " HTTP/1.1\r\n" + "Host: " + host + ":80\r\n" + "Connection: close\r\n" + "Content-Type: application/x-www-form-urlencoded\r\n" + "Content-Length: " + str.length() + "\r\n" + "\r\n" + str + "\r\n"); unsigned long timeout = millis(); while (client.available() == 0) { if (millis() - timeout> 5000) { Serial.println(">>> Client Timeout !"); client.stop(); break; } }
// Read all the lines of the reply from server and print them to Serial
while(client.available()){
String line = client.readStringUntil('\r');
int p = line.indexOf(':Data Inserted');
if(p != -1){
TimeDisplay = 65 - line.substring(p-15, p-13).toInt();
// Serial.println( line );
}
}
if(TimeDisplay < 5 || TimeDisplay> 65) TimeDisplay=60;
}
server.handleClient(); // 0504:追加
yield(); //wdtを動作させる(試す)
}
[/code]
レンタルサーバー側データ受信プログラム receive_data.php
<?php
//ini_set('display_errors', 1);
$dt = date("Y/m/d H:i:s"); // "2015/03/10 06:00:00"
// $dt = $_POST['datetime'];
$st = $_POST['status'];
$dr = $_POST['duration'];
$ct = $_POST['count'];
$qf = $_POST['qflowtime'];
//echo "Receive.php:",$dt,$dr,$qf,"\n";
$db = new SQLite3('watermeter.db');
if(!$db){
echo $db->lastErrorMsg();
} else {
// echo "Opened database successfully\n";
}
$results = $db->query("SELECT max(rowid), * FROM data_status"); $row = $results->fetchArray(SQLITE3_ASSOC); $dtp = $row['datetime']; $stp = $row['status']; $ctp = $row['count']; $dct = $ct - $ctp; if( $dct < 0){ $dct = $dct + 50000000; } if( $st == "Flow"){ $fr = $dct/( strtotime($dt) - strtotime($dtp) ); }else{ $fr = 0; }
// var_dump( $row);
$query=sprintf("UPDATE data_status set datetime='%s', status='%s', duration='%s', count='%d', qflowtime='%d', FlowRate='%d'", $dt, $st, $dr, $ct, $qf, $fr);
// echo $query;
$db->exec($query);
if(!$db){
echo $db->lastErrorMsg();
}else{
// echo "Update successful\n";
}
// if(substr($dt, 14, 2)== "00"){ // 0123/56/89 12:45:78
if(substr($dt, 15, 1)== "0"){ // 00???・?i???3???j
$db->exec("INSERT INTO data_log (datetime,status,duration,count,qflowtime)
VALUES ('$dt', '$st', '$dr', '$ct', '$qf') ");
// echo "Data Inserted in LogFile\n";
$lr = $db->lastInsertRowID();
// echo $lr."\n";
// ????O??f?[?^?d?a?o?・ --> 00?a?c?c???Z?l??・????A00?a??f?[?^?d?a?o?・ $dtp = strtr($dt, "/", "-");
//
if( date('i', strtotime($dtp)) == "00"){
$dtp = date('Y/m/d H:i:', strtotime($dtp ." -1 hour")) . "00";
}else{
$dtp = date('Y/m/d H:', strtotime($dtp)) . "00:00";
}
$query=sprintf("SELECT datetime, count, qflowtime FROM data_log where datetime>='%s'", $dtp); $results = $db->query($query);
// echo $query."\n";
$row = $results->fetchArray(SQLITE3_ASSOC);
// var_dump($row);
$dtp=$row['datetime'];
$ctp = $row['count'];
$qfp = $row['qflowtime'];
$dct = $ct - $ctp;
if( $dct < 0){
$dct = $dct + 50000000;
}
$dqf = $qf - $qfp;
if( $dqf < 0){
$dqf = $dqf + 86400;
}
if( $dqf > 3600 ){
echo "Data QFlowTime has been set to 3600, because of Error!";
$dqf = 3600;
}
$query=sprintf("UPDATE data_log set FQ=%d, QFT=%d WHERE rowid = %d", $dct, $dqf, $lr); $db->exec($query);
// echo $query."\n";
}
$al=Null;
$ev=Null;
switch (count($_POST)){ case 5:// event $ev = $_POST['event']; break; case 6:// alarm $al = $_POST['alarm']; $onof = $_POST['on_off']; break; case 7: // event $al = $_POST['alarm']; $onof = $_POST['on_off']; $ev = $_POST['event']; } if($al != Null){ $rs="actual"; $db->exec("INSERT INTO alarm_log (datetime,alarm,on_off,result, count, duration) VALUES ('$dt', '$al', '$onof', '$rs', '$ct','$dr' )"); }
// 前回FlowStopから今回のFlowStopまでのカウント数を前回FlowStartからFlowStopまでの時間で割り、FlowRateを算出する
if($ev != Null){
// $results = $db->query("SELECT max(rowid), count FROM event_log" );
if($ev=='FlowStop'){
$query = sprintf("SELECT * FROM event_log where event='FlowStop' order by rowid desc limit 1" );
$results = $db->query($query);
$row = $results->fetchArray(SQLITE3_ASSOC);
$ctp = $row['count'];
$dct=$ct - $ctp;
if($dct<0) $dct = $dct + 50000000;
$query = sprintf("SELECT * FROM event_log where event='FlowStart' order by rowid desc limit 1" );
$results = $db->query($query);
$row = $results->fetchArray(SQLITE3_ASSOC);
$dtp = $row['datetime'];
$fr = $dct / ( strtotime($dt) - strtotime($dtp)-9 );
}else{
$fr = 0;
}
$db->exec("INSERT INTO event_log (datetime, event, count, qflowtime, FlowRate)
VALUES ('$dt', '$ev', '$ct', '$qf', '$fr')");
}
$db->close();
//***
$dt = date("Y/m/d H:i:s"); echo "$dt:Data Inserted\n";
//*** update Global IP Address *
if(substr($dt, 14, 2)== "00"){ // 0123/56/89 12:45:78
file_put_contents(dirname(FILE) . '/GlobalIpAddress.php', $_SERVER['REMOTE_ADDR']);
echo "Global IP Address was updated!";
}
?>
レンタルサーバー側ブラウザ表示プログラム index.php
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta content="ja" http-equiv="Content-Language" />
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
<meta http-equiv="refresh" content="5">
<meta name="robots" content="noindex,nofollow">
<title>水道メーターモニター画面</title>
<style type="text/css">
.auto-style3 {
text-align: center;
margin-left: 0px;
margin-right: 8px;
color: #C0C0C0;
}
.auto-style5 {
vertical-align: middle;
color: #C0C0C0;
font-family: "MS Pゴシック";
font-size: large;
}
.auto-style6 {
text-align: center;
font-size: x-large;
}
.auto-style7 {
text-align: center;
margin-top: 25px;
}
.auto-style9 {
text-align: center;
font-size: x-large;
color: #FF0000;
}
</style>
</head>
<body style="color: #c0c0c0; background-color: #000000" class="auto-style9">
<?php
define( 'FLOW_TIME_EXCESS' , "15分"); // 1 hour
define( 'STOP_TIME_EXCESS' , "3時間"); // 2 hours
if(count($_GET) != 0){ $dsp = $_GET['inst']; }else{ $dsp = "graph"; } if(substr($dsp, 0, 4) == "time"){ $tmzone = substr($dsp, 4); $dsp = "tmgrph"; }else{ $tmzone = 0; } $db = new SQLite3('watermeter.db'); // 最新データをdata_statusから取出す $results = $db->query("SELECT max(rowid), * FROM data_status"); $row = $results->fetchArray(SQLITE3_ASSOC); $dt=$row['datetime']; $stt=$row['status']; $dr=$row['duration']; $fr=intval($row['FlowRate']); if($stt=="Flow"){ $sta = "<span style='color:red'>通水中 (" . $fr. "p/s)</span>"; }else{ $sta = "<span style='color:green'>" . "停止" . "</span>"; } $dra = str_replace( "h", "時間", $dr); $dra = str_replace( "m", "分", $dra); $dra = str_replace( "s", "秒", $dra); switch($dsp){ case "graph": case "tmgrph": // 最新アラームをalarm_logから取出す $results = $db->query("SELECT max(rowid), * FROM alarm_log"); $row = $results->fetchArray(SQLITE3_ASSOC); $dtal=$row['datetime']; $al=$row['alarm']; $onof=$row['on_off']; //グラフ作成 $graph = array(array(0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23),array(0,1,2)); $dtp = strtr($dt, "/", "-"); $dtp = date('Y/m/d H:i:s', strtotime($dtp ." -1 day")); $query=sprintf("SELECT datetime, QFT FROM data_log where datetime>='%s'", $dtp); $results = $db->query($query); $i=0; $hr = null; while($row = $results->fetchArray(SQLITE3_ASSOC)){ $dtp= $row['datetime']; // $FQ = $row['count']; // $QFT= $row['QFT']; // var_dump($row); //echo $i,$hr,"\n"; // echo $dtp."<br />"; // $graph = array( 'hour', 'elements'); if( $hr == null ){ // 初回のみ $graph[$i][0] = date('G', strtotime($dtp)); $hr = $graph[$i][0]; $graph[$i][1] = $row['QFT']; $graph[$i][2] = $dtp; }elseif($hr == date('G', strtotime($dtp))){ $graph[$i][0] = date('G', strtotime($dtp)); $hr = $graph[$i][0]; $graph[$i][1] = $row['QFT']; }else{ $graph[$i][1] = $row['QFT']; $i = $i + 1; $graph[$i][0] = date('G', strtotime($dtp)); $hr = $graph[$i][0]; $graph[$i][1] = 0; $graph[$i][2] = $dtp; }
//echo "Hour: ",$graph[$i][0], "i= ".$i." Value: ", $graph[$i][1], "<br />";
}
$imax = $i;
if($tmzone < 0){$tmzone = 0;}elseif($tmzone>$imax){$tmzone=$imax;}
if( $onof == 'ON' ){ if($al=="FlowTimeExcess"){ $message = $dtal . "<span style=\"color:red\"> 連続通水時間が" . FLOW_TIME_EXCESS . "を越えました。" . "</span>"; }elseif($al=="StopTimeExcess"){ $message = $dtal . "<span style=\"color:yellow\"> 連続停止時間が" . STOP_TIME_EXCESS ."を超えました。". "</span>"; } }elseif( $onof == 'OFF') { if(substr($al,0,14) == "FlowTimeExcess"){ $message = $dtal . "<span style=\"color:green\"> 連続通水時間は正常に戻りました。" . "</span>"; }elseif($al=="StopTimeExcess"){ $message = $dtal . "<span style=\"color:green\"> 連続停止時間は正常に戻りました。" . "</span>"; } }else{ $message = "ALARM DATA ERROR '" . $dtal . "'"; } if($dsp == "graph"){ break; } case "tmgrph": // start time & end time $graph_tz = array(array(),array(0,1,2)); $stm = date('Y/m/d H:00:00', strtotime($graph[$tmzone][2]."-1 hour")); // start
// $etm = date('Y/m/d H:i:s', strtotime($stm ." 1 hour"));
// $query=sprintf("SELECT datetime, event FROM event_log where (datetime>='%s') AND (datetime<='%s') ", $stm, $etm);
$query=sprintf("SELECT * FROM event_log where (datetime>='%s') ", $stm); $results = $db->query($query); $i=0; while($row = $results->fetchArray(SQLITE3_ASSOC)){ $graph_tz[$i][0] = $row['datetime']; $graph_tz[$i][1] = $row['event']; $graph_tz[$i][2] = $row['FlowRate'];
// echo $graph_tz[$i][0],"[",$graph_tz[$i][1],"] ";
$i++;
}
$imax_tz = $i;
break;
case "alarm": $list = array(array(),array(0,1,2));
// $dtp = strtr($dt, "/", "-");
// $dtp = date('Y/m/d H:i:s', strtotime($dtp ." -3 day"));
// $query=sprintf("SELECT * FROM alarm_log where datetime>='%s'", $dtp);
$query=sprintf("SELECT * FROM alarm_log ORDER BY rowid DESC LIMIT 9");
$results = $db->query($query);
$i=0;
while($row = $results->fetchArray(SQLITE3_ASSOC)){
$dtal=$row['datetime'];
$al=$row['alarm'];
$onof=$row['on_off'];
$list[$i][0] = $dtal;
$list[$i][1] = $al;
$list[$i][2] = $onof;
$i++;
// echo "ALARM: ", $dtal,"/", $al,"/", $onof,"<br />";
} $imax = $i; break; } $db->close();
?>
<h1 class="auto-style7" style="height: 40px; color: #C0C0C0;">水道メーターモニター画面</h1>
<h2 class="auto-style6" style="height: 20px"><?php echo date('n月j日 G:i:s', strtotime($dt)); ?> 現在</h2>
<table align="center" border="1" cellspacing="0" cellpadding="1" style="width: 640px; height: 90px">
<tr>
<td class="auto-style6" width="50%" style="height: 40px; color: #C0C0C0;">通水状態</td>
<td class="auto-style6" style="height: 40px; color: #C0C0C0;">継続時間</td>
</tr>
<tr>
<td class="auto-style6" style="height: 50px"><?php echo $sta ?> </td>
<td class="auto-style6" style="height: 50px; color: #C0C0C0;"><?php echo $dra ?> </td>
</tr>
</table>
<?php
switch($dsp){
case "graph":
echo "<p class='auto-style4'><span class='auto-style3'>アラーム情報</span></p>";
echo "<p class='auto-style4'>", $message, "</p>"; $st = "<table align=\"center\" border=\"0\" cellspacing=\"0\" cellpadding=\"1\" ><caption>各時間帯での通水時間</ caption>"; echo $st; echo "<tr height='100'>"; for( $i=0; $i <= $imax; $i++) { $st=sprintf("<td valign='bottom'><img src='bargraph3.gif' width='20' height='%d'</font></td>", intval($graph[$i][1]/36) ); echo $st; } $st = " </tr> <tr> "; echo $st; for( $i=0; $i <= $imax; $i++) { echo "<td><font size='2'> <a href='index.php?inst=time",$i,"'>", $graph[$i][0], "</a></font></td>"; } echo "</tr> </table>"; break; case "alarm": echo "<div class = \"auto-style4\"><br />アラーム履歴<br />";
// for( $i=$imax-2; $i >= 0, $i > $imax-10; $i--) {
for( $i=0; $i < $imax-1; $i++) {
if( $list[$i][2] == 'ON' ){
if($list[$i][1]=="FlowTimeExcess"){
$message = $list[$i][0] . "<span style=\"color:red\"> 連続通水時間が" . FLOW_TIME_EXCESS . "を越えました 。" . "</span>";
}elseif($list[$i][1]=="StopTimeExcess"){
$message = $list[$i][0] . "<span style=\"color:yellow\"> 連続停止時間が" . STOP_TIME_EXCESS . "を超えました。". "</span>";
}
}elseif( $list[$i][2] == 'OFF') {
if(substr($list[$i][1],0,14) == "FlowTimeExcess"){
$message = $list[$i][0] . "<span style=\"color:green\"> 連続通水時間は 正常に戻りました 。" . "</span>";
}elseif($list[$i][1]=="StopTimeExcess"){
$message = $list[$i][0] . "<span style=\"color:green\"> 連続停止時間は 正常に戻りました 。" . "</span>";
}
}
echo $message."<br />";
}
echo "</ div>";
break;
case "tmgrph": echo "<p class='auto-style4'><span class='auto-style3'>アラーム情報</span></p>"; echo "<p class='auto-style4'>", $message, "</p>"; $st = "<table align='center' border='0' cellspacing='1' cellpadding='1'><caption>" . "<a href='index.php?inst=time". ($tmzone-1) . "'><</a>     "; echo $st; $st= date('n月j日 G', strtotime($graph[$tmzone][2])); // substr($graph[$tmzone][2], 6, 7) ; echo $st; $st= "時の通水状況     <a href='index.php?inst=time". ($tmzone+1) . "'>></a></ caption>"; echo $st; echo "<tr height='100'>"; $tmz0 = date('Y/m/d H:00:00', strtotime($graph[$tmzone][2])); // 表示枠の左端の日時 $tmz1 = date('Y/m/d H:00:00', strtotime($graph[$tmzone][2]."+1 hour")); // 表示枠の右端の日時
//echo "time zone:",$tmz0," ",$tmz1,"<br />";
$wd0=Null; $wd1=Null; $oor = 0; //out of range for( $i=0; $i < $imax_tz; $i++) {
//echo $i, " ", $graph_tz[$i][0], " ", $graph_tz[$i][1], "<br />";
if(($graph_tz[$i][0] < $tmz0)){ continue; }else{ if($wd0==Null){$wd0 = $tmz0;}else{$wd0 = $graph_tz[$i-1][0];} } if($graph_tz[$i][0] > $tmz1){ $wd1 = $tmz1; $oor = 1; }else{ $wd1 = $graph_tz[$i][0]; }
//echo "wd: ", $wd0," - ", $wd1, " ", $graph_tz[$i][1],"<br />";//on-offの時間を表示する
$wd = strtotime($wd1)-strtotime($wd0);
if($graph_tz[$i][1]=="FlowStart"){ $st = sprintf("<td valign='bottom' width='%d' height='80' bgcolor='green'></font></td>", $wd/6-2 ); echo $st; }elseif($graph_tz[$i][1]=="FlowStop"){ $grn = $graph_tz[$i][2]*5; // = 255 / 50;
// echo "[Rate]",intval($graph_tz[$i][2])," ";
if($grn>255){$grn=255;}
$grn = (255-$grn);
$grn = dechex($grn);
if(strlen($grn)==1){$grn = "0".$grn;}
$clr = "#ff".$grn."00";
// echo $clr;
$st = sprintf("<td valign='bottom' width='%d' height='80' bgcolor=%s></font></td>", $wd/6-2, $clr );
echo $st;
} if($oor == 1){ break;} } //echo $st; if($i==$imax_tz){ // 最後のデータを表示した後、現在時間までの表示と残り時間をグレーで埋める if($dt >= $tmz1){ // 現在時間は表示枠の外 $wd0 = $wd1; $wd1 = $tmz1; $wd = strtotime($wd1)-strtotime($wd0); if($stt == "Flow"){ $st = sprintf("<td valign='bottom' width='%d' height='80' bgcolor='red'></font></td>", $wd/6-2 ); }else{ $st = sprintf("<td valign='bottom' width='%d' height='80' bgcolor='green'></font></td>", $wd/6-2 ); } echo $st; }else{ // 現在時間は表示枠内にある(常にこちらか?) if($wd1==Null){$wd0 = $tmz0;}else{$wd0 = $wd1;} $wd1 = $dt; $wd = strtotime($wd1)-strtotime($wd0); if($stt == "Flow"){ $grn = dechex($fr * 5); if(strlen($grn)==1){$grn = "0".$grn;} $clr = "#ff".$grn."00"; $st = sprintf("<td valign='bottom' width='%d' height='80' bgcolor=%s></font></td>", $wd/6-2, $clr );
// $st = sprintf("<td valign='bottom' width='%d' height='80' bgcolor='red'></font></td>", $wd/6-2 );
}else{
$st = sprintf("<td valign='bottom' width='%d' height='80' bgcolor='green'></font></td>", $wd/6-2 );
}
echo $st;
$wd0 = $dt; $wd1 = $tmz1; $wd = strtotime($wd1)-strtotime($wd0); $st = sprintf("<td valign='bottom' width='%d' height='80' bgcolor='gray'></font></td>", $wd/6-2 ); echo $st; } } $st = " </tr> </table>"; echo $st; $st = "<table align='center' border='0' cellspacing='0' cellpadding='1'>"; echo $st; echo "<tr>"; for( $i=0; $i < 12; $i++) { echo "<td width='48' align='left'><font size='2'>", $i*5 ,"</font></td>";
// echo "<td width='100' bgcolor='' </td>";
}
echo "</tr> </table>";
//echo "<table align='center' border='1' cellspacing='1' cellpadding='1'>";
//echo "<tr><td width='600' bgcolor='' </td></tr> </table>";
break; } echo "<br />";
// http://localhost/index.php?inst=
$SvrAdd = file_get_contents("GlobalIpAddress.php");
//echo "this: ". $_SERVER['REMOTE_ADDR'] . " SVR: " . $SvrAdd . " Acc: " . $AccAdd;
?>
<button type="button" class="auto-style6" onclick="location.href='index.php?inst=graph'">グ ラ フ表示</button>
<button type="button" class="auto-style6" onclick="location.href='index.php?inst=alarm'">アラーム履歴</button>
<?php if($_SERVER['REMOTE_ADDR'] == $SvrAdd){ ?>
<button type="button" class="auto-style6" onclick="location.href='http://192.168.1.111/waterMeter?S'">リアルタイム(LAN)</button>
<?php }else{
$url = "http://" . $SvrAdd . "/waterMeter?S" ; ?>
<button type="button" class="auto-style6" onclick="location.href='<?php echo $url; ?>'">リアルタイム(WAN)</button>
<?php } ?>
<button type="button" class="auto-style6" onclick="location.href='../roomenvr/index.php'">室温度湿度</button>
</body>
</html>
「リアルタイム(WAN)」のボタンは外部からアクセスが可能となるので、実際の運用ではパスワードを設け第三者からのアクセスは出来ないようにしている。
また、外部からのアクセスでポートフォワーディングを使用するが、現状はポート番号が80番となっているが、安全のため一般に使われていない番号(例えば、10,000番台など)を使うことが望ましい。
まとめ
これまで、「水道漏水検知システム」としていたが、今回より、「自宅水道メーターWEB監視システム」と変更した。
大きな変更点として、外出時にも水道メーターのパイロット(回転針)が確認出来る点である。
通常では気にならない水道メーターであるが、長期に家を離れた場合には過去の経緯から気になるもので1日に1度は確認している。
メール送信での通知もこれまで行っていたが、それほどチェックしていないのでメールよりブラウザでの確認が容易との判断で現状ではメールは使っていない。
重要な観点として、自宅IoTへのアクセスに自宅グローバルIPアドレスをWEBサイトに貼付けているので、第三者からの不正アクセスを防ぐことが必要である。
このための方法として、パスワード保護や、DDNSを用いた自宅グローバルIPアドレス変換を行わないなど、いくつかの検討が必要である。詳細は、外出先から自宅LANへドメイン名でアクセスする(DDNSを使わない)安全な方法に紹介している。