降雨検知システムを作る4 前半編

降雨検知システムを作る4 前半編

降雨検知システムを作る 市販されていないオリジナルです

私にとって降雨センサーとの付き合いは長い。
コンパレーター方式->アスザック方式->SeeedStudio RG-15->ドップラー方式->無安定マルチバイブ
レーター方式->AC方式(実験中)->究極システムになるのか?AIによる独自方式(実験中)
これ程落ち着きが無い一番の理由は、決定打が無いという事に尽きる。

私の経験から市販品のそれなりの性能の物でもだいたい2年程度しかもたないようだ。
私としては最低でも5年程度の耐久性を望むところだ。
現在実験中のAI方式が完成すれば決定打になる可能性は高い。
Ai方式では、難しいという事もあるが、特に雪のデーターが取れなくて困っている。
西日本の山間部という事もあり、東北や北陸程雪が降らないので実験する機会が限られている
ので1月、2月の数日間を逃すと実験出来ないのだ。
もちろん撮影しておけば良かったのだが、実験中は興奮してそれどころでは無かったのだ。
気が付いて録画を始めたのだが、今年は既に遅くまた来年を待たなければ実験出来なくなった。
AIを使って実風景に雪を降らせるという手法もあるらしいがまだやっていない。
AIでの降雨、降雪の検知の難しさはやってみて判明したが、夜間は比較的簡単で既に完成域に
達している、だが昼間は環境ノイズが多いせいか夜間ほど上手く行かない、昼間独自のアルゴ
リズムが必要になり、実験素材である昼間の雪の映像を熱望している現状がある。
いずれにせよ頑張って何とか生きている内に究極の降雨、降雪システムとして完成させたい。
私の若いころ「壁掛けTV」という言葉があったが、ご存知のように現在では死語である。
また「鉄腕アトム」のような知性を持ったロボットが恐らく数年か数十年で普通にそこら辺を
歩いている時代になってきた。
AIによるお天気カメラの未来は、ごく近々監視カメラにおまけでで付く機能になるかも知れない。

しかし今回は現実に戻ってAI方式以外について今まで実験して来た成果をご紹介しましょう。
実は私は現在のところこの雨センサーが一番好きだ。
単純な構造なのでかえって作るのにかなり苦労させられたせいもあるが、このセンサーは降雨の
状況を肌身の様に感じさせてくれるのが良い。
理由は、センサーが水滴に直接接している構造の為だ、ガラス越しの水滴検知では無理だ。

購入先 アマゾン 購入価格¥2,099
劣化したセンサー表面

初期には、簡単なコンパレーターによる水滴検知システムで、まあおもちゃの部類になる。
もちろん水滴はちゃんと検知出来るが、問題は耐久性である。
1カ月も経つとセンサーが腐食して使えなくなる代物で全く満足できなかった。
この方式のメリットは、構造がシンプルで良い。
最大の欠点は、直流電流によるセンサーの電蝕や電極の耐性だ。

■ アスザック 雨センサー

次にやはりセンサーが肝なのではないか?
と考えプロ用の長野のアイザックス社の雨センサ ヒータ付(夜露非検知型)AKI-1805を購入し
て設置した。
http://www.asuzac-pd.jp/seihin/3heat.htm
センサーの種類は「ガラエポ」「テフロン」「セラミックス」とあるが、さすがにセラミックス¥49,200
は年金生活者には無理なのでせめてテフロン製¥12,800に留めておいた。
この製品は、プロ用ということもあり安定して使用できていた。。。が2年目に突然応答しなく
なって終了という結末となった。
2年も持てば寿命なので良いかとも考えたが、少し残念だった。
せめて5年位は持たせたかったと思う。
センサー部はテフロン製で耐久性があり、まだ使用できる状態だったが内部回路が故障した
模様。この製品は、センサー内に回路が封印された構造なので故障しても修理が出来ない。
直射日光にさらせれる環境では、表面温度も真夏時にはかなりの温度になるはずだし、ヒーターも狭い
空間に同居している構造なので故障しても不思議は無い、センサーは電子回路と分離保護して設置す
る方式にすべきだろう。
内部の回路が無い部品もしくは回路が分離された仕様の販売があるのなら再度注文したかっ
たが、無いようなのであきらめた次第。
構造は、内部から検知処理済の出力が外部リレー用にオープンコレクターで出ているので電源
とリレーを付ければ動作するので説明する程では無いので省略。

アスザック 雨センサーヒーター付き テフロン
購入先 アイザック ¥12,800




■ 無安定マルチバイブレーター方式(RC方式)

あるときWebをググっていたら面白そうな記事を見つけたのがきっかけでした。
「EDN Japan」の記事である。
以下抜粋

これを見て閃きました、降雨センサーに応用出来ると。
ようするにCPUを使って無安定マルチバイブレーターを構築し、RC発振器のR(つまり雨センサーの抵抗値)を求め抵抗値の変化から雨滴を検出するという仕組みである。

上記の記事の1言1言を注意深くをESPr-Developerの環境に置き換えてプログラムしてみた。
本来周期とは充電-放電までの時間をいうが色々テストしてみたら充電時間はふらつきが大きく放電時間
に比べて利用価値が少ないようだったので比較的ばらつきが無い放電時間のみを利用する事にした。

#include <ESP8266WiFi.h>

const int PB0 = 16;  // GPIO16
const int PB1 =  4;  // GPIO4
const int PB2 =  5;  // GPIO5(コンデンサ電圧監視)

void setup() {
  pinMode(PB0, OUTPUT);
  pinMode(PB1, OUTPUT);
  pinMode(PB2, INPUT);

  Serial.begin(9600);
}

void loop() {
  // 1. 充電フェーズ
  digitalWrite(PB1, LOW);
  digitalWrite(PB0, HIGH);

  // 2. しきい値到達を待つ
  while (digitalRead(PB2) == LOW);  // PB2 が LOW になるのを待つ
  unsigned long chargeTime = micros();  // 充電完了時間を記録

  // 3. 放電フェーズ
  digitalWrite(PB0, LOW);
  digitalWrite(PB1, HIGH);
  // 4. しきい値が下がるのを待つ
  while (digitalRead(PB2) == HIGH);  // PB2 が HIGH になるのを待つ
  unsigned long dischargeTime = micros();  // 放電完了時間を記録

  // 5. 放電時間(period)を計算
  unsigned long period = dischargeTime - chargeTime;

  // 7. 結果を表示
  Serial.print("Period: ");
  Serial.print(period);

  delay(1000);  // 1秒待機
}

上記スクリプト用実験用回路図

前段階の実験

本チャンを作る前に上手く動作するか、テストスクリプトとNODE-RED上で受信したデーター
の処理を行います。
上手く動作したらNODE-RED上でのフィルター処理などを本チャン側(ESPr-Developer)に移設し、
最終スクリプトとして組み込み完成させます。

実は、この段階に至るまで相当の時間を費やした。
最初は簡単だろうと一気に完成させて設置したが、色々な不具合に遭遇して使い物にならず、これ
はもう断念するかと迄も至った経緯が何度もある。
何が難しいかというと、CPUに発振させてそれを自分で測定している関係上、間違った方法をとると
CR発振器自体は正常発振しているのにも関わらず、測定値に偽信号が現れ誤動作するのだ。
CPUでは同時にWifiの処理なども行っているので難しいのは当然と言える。
最後の望みをかけて原点に立ち返り、今回発振信号の可視化という手段を取りセンサーで何が起こっ
ているか詳しく探った結果、最終的に上手く行く方法にたどり着いた次第です。

MQTTで受信したperiod2(放電時間)はセンサーの素データーとしてグラフで表示します。
可視化することにより、頭の中の想像だけでは理解できないセンサーで起こっている状況をはっきり
と把握出来ます。
素データーは色々な要因によって降雨が無い時でもふらつきますので、フィルターを通し比較的長い
期間での安定した周期としての「baseline」データーを得ます。
同時に比較的短い周期でのデーター「filtered」を得ます。
filteredがセンサーの検知した値として参照されます。
素データーには、ある程度のノイズ成分がありますのでこれを軽く除いたデーターになります。
baselineデーターは、降雨が無い場合の基準となる発振器周期でこの基準よりの変移加減を調べ
その値を「intensity」値として得ます。
intensity値は、降雨の有無や露の有無、センサーの乾燥状態を知る指標になります。

無安定マルチバイブレーター方式(RC方式) 回路図

作り方の詳細は次回後半編を参照してください。




TEST用NODE-RED フロー

[{"id":"95bfc20d892275d7","type":"tab","label":"RC方式 TEST","disabled":false,"info":"","env":[]},{"id":"5788edb49a0011f7","type":"json","z":"95bfc20d892275d7","name":"","property":"payload","action":"obj","pretty":false,"x":310,"y":80,"wires":[["92447aad70a5b173","db239e86d5bd2e15"]]},{"id":"9e54309fa87035ff","type":"mqtt in","z":"95bfc20d892275d7","name":"","topic":"rain_sensor/rc/data","qos":"2","datatype":"auto-detect","broker":"cf1776747da46a7c","nl":false,"rap":true,"rh":0,"inputs":0,"x":130,"y":80,"wires":[["705f56f3d6a9775e","5788edb49a0011f7"]]},{"id":"705f56f3d6a9775e","type":"debug","z":"95bfc20d892275d7","name":"debug 5","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":280,"y":140,"wires":[]},{"id":"0ae9367b10070b41","type":"inject","z":"95bfc20d892275d7","name":"デューティ比  0% OFF","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"0","payloadType":"str","x":420,"y":540,"wires":[["e169ebf5f49c91a7"]]},{"id":"e169ebf5f49c91a7","type":"mqtt out","z":"95bfc20d892275d7","name":"","topic":"rain_sensor/rc/heater","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"cf1776747da46a7c","x":680,"y":540,"wires":[]},{"id":"0f241fe10a137de9","type":"comment","z":"95bfc20d892275d7","name":"ヒーターデューティ比設定","info":"","x":170,"y":600,"wires":[]},{"id":"92447aad70a5b173","type":"function","z":"95bfc20d892275d7","name":"Temperature","func":"var data = 0.0;\ndata = msg.payload.temperature;\nmsg.payload = data;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":510,"y":460,"wires":[["b3c5e16ad05f3f0a"]]},{"id":"b3c5e16ad05f3f0a","type":"ui-chart","z":"95bfc20d892275d7","group":"a063893cef8e3d33","name":"センサー温度","label":"センサー温度","order":5,"chartType":"line","category":"topic","categoryType":"msg","xAxisLabel":"","xAxisProperty":"","xAxisPropertyType":"timestamp","xAxisType":"time","xAxisFormat":"","xAxisFormatType":"auto","xmin":"","xmax":"","yAxisLabel":"","yAxisProperty":"payload","yAxisPropertyType":"msg","ymin":"-5","ymax":"60","bins":10,"action":"append","stackSeries":false,"pointShape":"circle","pointRadius":4,"showLegend":true,"removeOlder":1,"removeOlderUnit":"3600","removeOlderPoints":"","colors":["#0095ff","#ff0000","#ff7f0e","#2ca02c","#a347e1","#d62728","#ff9896","#9467bd","#c5b0d5"],"textColor":["#ffffff"],"textColorDefault":false,"gridColor":["#e5e5e5"],"gridColorDefault":true,"width":6,"height":8,"className":"","interpolation":"linear","x":700,"y":460,"wires":[[]]},{"id":"cc7ca50e2ce19715","type":"inject","z":"95bfc20d892275d7","name":"デューティ比  50%","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"50","payloadType":"str","x":410,"y":580,"wires":[["e169ebf5f49c91a7"]]},{"id":"0688c239e6d6c6bb","type":"inject","z":"95bfc20d892275d7","name":"デューティ比 100%","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"100","payloadType":"str","x":410,"y":660,"wires":[["e169ebf5f49c91a7"]]},{"id":"db239e86d5bd2e15","type":"function","z":"95bfc20d892275d7","name":"period2抽出","func":"var data = msg.payload.period2;\nmsg.payload = data;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":510,"y":80,"wires":[["61ccbc2a92f28a64","7a4646fe703bd9b9"]]},{"id":"d567e157de261f8c","type":"mqtt out","z":"95bfc20d892275d7","name":"","topic":"rain_sensor/rc/reset","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"cf1776747da46a7c","x":640,"y":720,"wires":[]},{"id":"34ac5d8ca4507a29","type":"inject","z":"95bfc20d892275d7","name":"システムリセット","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"str","x":410,"y":720,"wires":[["d567e157de261f8c"]]},{"id":"954aaec09adfef7b","type":"comment","z":"95bfc20d892275d7","name":"システムリセット","info":"","x":150,"y":720,"wires":[]},{"id":"cc485195fa55e976","type":"inject","z":"95bfc20d892275d7","name":"デューティ比  70%","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"70","payloadType":"str","x":410,"y":620,"wires":[["e169ebf5f49c91a7"]]},{"id":"6b2300514726620d","type":"function","z":"95bfc20d892275d7","name":"filtered 抽出","func":"var temp = msg.payload.filtered;\nmsg.payload = temp;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":710,"y":260,"wires":[["776d073b64516dea","e2f43a773e8fd0f6","20162b337f7bc38d"]]},{"id":"776d073b64516dea","type":"ui-chart","z":"95bfc20d892275d7","group":"a063893cef8e3d33","name":"フィルター後","label":"フィルター後","order":2,"chartType":"scatter","category":"topic","categoryType":"msg","xAxisLabel":"","xAxisProperty":"","xAxisPropertyType":"timestamp","xAxisType":"time","xAxisFormat":"","xAxisFormatType":"auto","xmin":"","xmax":"","yAxisLabel":"","yAxisProperty":"payload","yAxisPropertyType":"msg","ymin":"500","ymax":"1400","bins":10,"action":"append","stackSeries":false,"pointShape":"crossRot","pointRadius":"1","showLegend":false,"removeOlder":1,"removeOlderUnit":"3600","removeOlderPoints":"","colors":["#0095ff","#ff0000","#ff7f0e","#2ca02c","#a347e1","#d62728","#ff9896","#9467bd","#c5b0d5"],"textColor":["#ffffff"],"textColorDefault":false,"gridColor":["#e5e5e5"],"gridColorDefault":false,"width":6,"height":8,"className":"","interpolation":"linear","x":940,"y":260,"wires":[[]]},{"id":"20162b337f7bc38d","type":"function","z":"95bfc20d892275d7","name":"intensity","func":"var data = msg.payload;\nvar base = global.get(\"baseline\");\nif(data == base){\n    msg.payload = 1074 - data;\n    return msg;\n}","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":680,"y":380,"wires":[["0b9a737690fca301","af53d3c029264437"]]},{"id":"99d656e2b0d3a124","type":"ui-chart","z":"95bfc20d892275d7","group":"a063893cef8e3d33","name":"baseline","label":"baseline","order":3,"chartType":"scatter","category":"topic","categoryType":"msg","xAxisLabel":"","xAxisProperty":"","xAxisPropertyType":"timestamp","xAxisType":"time","xAxisFormat":"","xAxisFormatType":"auto","xmin":"","xmax":"","yAxisLabel":"","yAxisProperty":"payload","yAxisPropertyType":"msg","ymin":"500","ymax":"1400","bins":10,"action":"append","stackSeries":false,"pointShape":"crossRot","pointRadius":"1","showLegend":false,"removeOlder":1,"removeOlderUnit":"3600","removeOlderPoints":"","colors":["#0095ff","#ff0000","#ff7f0e","#2ca02c","#a347e1","#d62728","#ff9896","#9467bd","#c5b0d5"],"textColor":["#ffffff"],"textColorDefault":false,"gridColor":["#e5e5e5"],"gridColorDefault":false,"width":6,"height":8,"className":"","interpolation":"linear","x":920,"y":220,"wires":[[]]},{"id":"61ccbc2a92f28a64","type":"ui-chart","z":"95bfc20d892275d7","group":"a063893cef8e3d33","name":"センサー発振器 period2","label":"センサー発振器 period2","order":1,"chartType":"scatter","category":"topic","categoryType":"msg","xAxisLabel":"","xAxisProperty":"","xAxisPropertyType":"timestamp","xAxisType":"time","xAxisFormat":"","xAxisFormatType":"auto","xmin":"","xmax":"","yAxisLabel":"","yAxisProperty":"payload","yAxisPropertyType":"msg","ymin":"500","ymax":"1400","bins":10,"action":"append","stackSeries":false,"pointShape":"crossRot","pointRadius":"1","showLegend":false,"removeOlder":1,"removeOlderUnit":"3600","removeOlderPoints":"","colors":["#0095ff","#ff0000","#ff7f0e","#2ca02c","#a347e1","#d62728","#ff9896","#9467bd","#c5b0d5"],"textColor":["#ffffff"],"textColorDefault":false,"gridColor":["#e5e5e5"],"gridColorDefault":false,"width":6,"height":8,"className":"","interpolation":"linear","x":890,"y":80,"wires":[[]]},{"id":"e2f43a773e8fd0f6","type":"debug","z":"95bfc20d892275d7","name":"debug 1","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":920,"y":300,"wires":[]},{"id":"57f342306b14977d","type":"function","z":"95bfc20d892275d7","name":"filtered 抽出","func":"var temp = msg.payload.baseline;\nglobal.set(\"baseline\",temp);\nmsg.payload = temp;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":710,"y":220,"wires":[["0958b9c16b33a64a","99d656e2b0d3a124"]]},{"id":"0958b9c16b33a64a","type":"debug","z":"95bfc20d892275d7","name":"debug 2","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":920,"y":180,"wires":[]},{"id":"0b9a737690fca301","type":"ui-chart","z":"95bfc20d892275d7","group":"a063893cef8e3d33","name":"Intensity","label":"Intensity","order":4,"chartType":"scatter","category":"topic","categoryType":"msg","xAxisLabel":"","xAxisProperty":"","xAxisPropertyType":"timestamp","xAxisType":"time","xAxisFormat":"","xAxisFormatType":"auto","xmin":"","xmax":"","yAxisLabel":"","yAxisProperty":"payload","yAxisPropertyType":"msg","ymin":"-100","ymax":"300","bins":10,"action":"append","stackSeries":false,"pointShape":"crossRot","pointRadius":"1","showLegend":false,"removeOlder":1,"removeOlderUnit":"3600","removeOlderPoints":"","colors":["#0095ff","#ff0000","#ff7f0e","#2ca02c","#a347e1","#d62728","#ff9896","#9467bd","#c5b0d5"],"textColor":["#ffffff"],"textColorDefault":false,"gridColor":["#e5e5e5"],"gridColorDefault":false,"width":6,"height":8,"className":"","interpolation":"linear","x":920,"y":380,"wires":[[]]},{"id":"7a4646fe703bd9b9","type":"function","z":"95bfc20d892275d7","name":"フィルター","func":"// =========================\n// RC降雨センサー安定フィルタ\n// MQTT 1.5秒間隔向け\n// =========================\n\n// ---- 設定 ----\nconst MAX_SPIKE = 300;         // 突発スパイク除去閾値\nconst SHORT_WINDOW = 12;       // 短期中央値(1.5秒×12=18秒)\nconst LONG_WINDOW = 540;       // 長期履歴(1.5秒×360=9分)\nconst BIN_WIDTH = 1;           // 最頻値ビン幅\nconst FOLLOW_RATE = 0.05;      // ベースライン追従率(0.01〜0.1)\n\n// ---- 入力 ----\nlet current = Number(msg.payload);\n\n// ---- 前回値取得 ----\nlet lastRaw = context.get(\"lastRaw\");\n\n// 初回\nif (lastRaw === undefined) {\n    context.set(\"lastRaw\", current);\n    context.set(\"shortBuffer\", [current]);\n    context.set(\"longBuffer\", [current]);\n    context.set(\"baseline\", current);\n\n    msg.payload = {\n        raw: current,\n        filtered: current,\n        baseline: current,\n        rain_diff: 0\n    };\n    return msg;\n}\n\n// ---- スパイク除去 ----\nif (Math.abs(current - lastRaw) > MAX_SPIKE) {\n    return null;\n}\n\ncontext.set(\"lastRaw\", current);\n\n// ---- 短期バッファ(中央値用)----\nlet shortBuffer = context.get(\"shortBuffer\") || [];\nshortBuffer.push(current);\nif (shortBuffer.length > SHORT_WINDOW) {\n    shortBuffer.shift();\n}\ncontext.set(\"shortBuffer\", shortBuffer);\n\n// ---- 短期中央値 ----\nlet sortedShort = [...shortBuffer].sort((a, b) => a - b);\nlet median = sortedShort[Math.floor(sortedShort.length / 2)];\n\n// ---- 長期バッファ(最頻値用)----\nlet longBuffer = context.get(\"longBuffer\") || [];\nlongBuffer.push(median);\nif (longBuffer.length > LONG_WINDOW) {\n    longBuffer.shift();\n}\ncontext.set(\"longBuffer\", longBuffer);\n\n// ---- ヒストグラムで最頻値 ----\nlet histogram = {};\n\nfor (let v of longBuffer) {\n    let bin = Math.round(v / BIN_WIDTH) * BIN_WIDTH;\n    histogram[bin] = (histogram[bin] || 0) + 1;\n}\n\nlet mode = null;\nlet maxCount = 0;\n\nfor (let key in histogram) {\n    if (histogram[key] > maxCount) {\n        maxCount = histogram[key];\n        mode = Number(key);\n    }\n}\n\n// ---- ベースライン緩やか追従 ----\nlet baseline = context.get(\"baseline\") || mode;\n\n// 雨っぽくないときだけベース更新\nif (Math.abs(median - baseline) < 50) {\n    baseline = baseline + (mode - baseline) * FOLLOW_RATE;\n}\n\ncontext.set(\"baseline\", baseline);\n\n// ---- 雨判定差分 ----\nlet rain_diff = median - baseline;\n\n// ---- 出力 ----\nmsg.payload = {\n    raw: current,                       // 生値\n    filtered: Number(median.toFixed(1)),// 短期安定値\n    baseline: Number(baseline.toFixed(1)), // 長期ベース\n    rain_diff: Number(rain_diff.toFixed(1)),\n    mode: mode,\n    samples: longBuffer.length\n};\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":530,"y":220,"wires":[["6b2300514726620d","57f342306b14977d"]]},{"id":"af53d3c029264437","type":"debug","z":"95bfc20d892275d7","name":"debug 3","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":920,"y":440,"wires":[]},{"id":"cf1776747da46a7c","type":"mqtt-broker","name":"","broker":"192.168.1.200","port":1883,"clientid":"","autoConnect":true,"usetls":false,"protocolVersion":4,"keepalive":60,"cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthRetain":"false","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closeRetain":"false","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willRetain":"false","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"a063893cef8e3d33","type":"ui-group","name":"今日の天気","page":"586bd0f0c91d94eb","width":6,"height":1,"order":1,"showTitle":false,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"586bd0f0c91d94eb","type":"ui-page","name":"気象","ui":"f1eb2e2bd0370077","path":"/page1","icon":"home","layout":"grid","theme":"0f03d87fca4bccfc","breakpoints":[{"name":"Default","px":"0","cols":"3"},{"name":"Tablet","px":"576","cols":"6"},{"name":"Small Desktop","px":"768","cols":"9"},{"name":"Desktop","px":"1024","cols":"12"}],"order":1,"className":"","visible":true,"disabled":false},{"id":"f1eb2e2bd0370077","type":"ui-base","name":"画面名","path":"/dashboard","appIcon":"","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"headerContent":"page","navigationStyle":"default","titleBarStyle":"default","showReconnectNotification":true,"notificationDisplayTime":1,"showDisconnectNotification":true,"allowInstall":true},{"id":"0f03d87fca4bccfc","type":"ui-theme","name":"気象","colors":{"surface":"#505050","primary":"#505050","bgPage":"#505050","groupBg":"#5f5f5f","groupOutline":"#cccccc"},"sizes":{"density":"default","pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}},{"id":"cce2065d5fdeb76b","type":"global-config","env":[],"modules":{"@flowfuse/node-red-dashboard":"1.30.2"}}]

TEST用 ESPr-Developer用スクリプト

#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <EEPROM.h>


// =========================
// WiFi
// =========================
const char* ssid = "your_ssid";
const char* password = "your_passwd";

// =========================
// MQTT
// =========================
const char* mqttServer = "192.168.1.200";
const int mqttPort = 1883;

const char* pubTopic      = "rain_sensor/rc/data";
const char* subHeater     = "rain_sensor/rc/heater";      // 手動PWM 0-100
const char* topicReset    = "rain_sensor/rc/reset";

// =========================
// ピン定義
// =========================
#define TEMP_PIN   2
#define HEATER_PIN 15

// =========================
// 発振器
// =========================
unsigned long period2 = 0;

// =========================
// 温度センサ
// =========================
OneWire oneWire(TEMP_PIN);
DallasTemperature sensors(&oneWire);
float temperature = 0;

// =========================
// MQTT / WiFi
// =========================
WiFiClient espClient;
PubSubClient client(espClient);
static unsigned long lastPublish = 0;

// =========================
// ヒーター変数
// =========================
float period = 0;
int heaterPower = 100;

// =========================
// WiFi接続
// =========================
void setupWiFi() {
WiFi.config(IPAddress(192, 168, 1, 100), IPAddress(192, 168, 1, 1), IPAddress(255, 255, 255, 0));
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
  }
}

// =========================
// MQTT受信
// =========================
void callback(char* topic, byte* payload, unsigned int length) {
  String msg = "";
  for (unsigned int i = 0; i < length; i++) msg += (char)payload[i];

  String t = String(topic);

  if (t == subHeater) {
    int v = msg.toInt();
    if (v >= 0 && v <= 100) {
      heaterPower = v;
    }
  }
    else if (t == topicReset) {
    ESP.restart();
  }
}

void reconnect() {
  while (!client.connected()) {
    if (client.connect("sensor_test_node")) {   
      client.subscribe(subHeater);
      client.subscribe(topicReset);
    }
    else {
      delay(1000);
    }
  }
}

// =========================
// 温度読み取り
// =========================
void readTemp() {
  sensors.requestTemperatures();
  temperature = sensors.getTempCByIndex(0);
}

// =========================
// PWMヒーター制御
// =========================
void heaterControl() {
  unsigned long period = 4000;
  unsigned long onTime = period * heaterPower / 100;
  unsigned long t = millis() % period;

  if (t < onTime)
    digitalWrite(HEATER_PIN, HIGH);
  else
    digitalWrite(HEATER_PIN, LOW);
}

// =========================
// RC発振
// =========================

unsigned long measureOnce() {

  // 強制放電
  pinMode(5, OUTPUT);
  digitalWrite(5, LOW);
  digitalWrite(16, LOW);
  delayMicroseconds(50);

  pinMode(5, INPUT);

  // 充電開始
  digitalWrite(4, LOW);
  digitalWrite(16, HIGH);
  
 
  // 充電完了待ち
  while (digitalRead(5) == LOW) ;

  // 放電開始
  digitalWrite(16, LOW);
  unsigned long start= micros();
  digitalWrite(4, HIGH);

 
  // 放電完了待ち
  while (digitalRead(5) == HIGH) {
    if (micros() - start > 3000) return 0; // タイムアウト
    yield();
  }
  // 放電時間測定
  return micros() - start;
}


// =========================
// 複数回測定→中央値
// =========================
unsigned long measureStable() {

  const int N = 5;
  unsigned long buf[N];

  for (int i = 0; i < N; i++) {
    buf[i] = measureOnce();
    delayMicroseconds(200); // 少し間隔
  }

  // ソート(簡易)
  for (int i = 0; i < N - 1; i++) {
    for (int j = i + 1; j < N; j++) {
      if (buf[i] > buf[j]) {
        unsigned long t = buf[i];
        buf[i] = buf[j];
        buf[j] = t;
      }
    }
  }

  return buf[N / 2]; // 中央値
}

// =========================
// setup
// =========================
void setup() {

  pinMode(16, OUTPUT);
  pinMode(4, OUTPUT);
  pinMode(5, INPUT);

  pinMode(HEATER_PIN, OUTPUT);
  digitalWrite(HEATER_PIN, LOW);

  sensors.begin();
  setupWiFi();

  client.setServer(mqttServer, mqttPort);
  client.setCallback(callback);
}

// =========================
// loop
// =========================
void loop() {
  if (!client.connected()) reconnect();
  client.loop();

  heaterControl();
  readTemp();
 
  period2= measureStable();
  
  unsigned long nowMillis = millis();
  // 500msごとに更新
  if (nowMillis - lastPublish >= 1500) {
    char payload[256];
    snprintf(payload, sizeof(payload),
           "{\"period2\":%lu,\"temperature\":%.2f}",
           period2,
           temperature
          );

    client.publish(pubTopic, payload);
    lastPublish = millis();
  }

  delay(500);
}

TEST波形 降雨無しから降雨開始と終了一連の流れ

降雨無しセンサー素、filtered、baseline波形

信号の散布図になります、いずれのグラフも1074usで一定の波形となっている。
実際には、素データーには、細かいばらつきがかなりあり、波長の長い方にランダムに発生している。
filteredとbaselineはフィルターにより除去され安定な数値となります。
実際の全体周期は1874us程度で周波数で言えば533Hz程度となる。
雨センサーとしては、数kHz程度が理想と思えるが、あまり周波数を高くするとESPr-Developer側が
悲鳴を上げてちゃんと動かなくなるのでこの辺で使用することとする。

降雨検知時の波形(降り始め)

素データーとfilteredの波形の右端が低下して来ました。(ぽつぽつ程度の雨)
これは状況としてセンサーが降雨を検知し、センサーが水滴により抵抗値が下がり始めた状況
を示しています。
baselineは長い周期の反応なのでまだ変化はありません。
通常baselineは降雨が続けば、当然変化(下がって行く)しますが、センサー乾燥時の値を内部で
保持しておりこれを基準として判断しますので降雨検知には問題ありません。
尚、センサーは劣化して表面の状態が変化し、抵抗値も微妙に変わる可能性がありますのでこの
baselineも適宜状況に合わせて更新する必要があると思われます。(baseline学習機能の付加の必要性)
降雨あり無しは、intensity = baseline – filtered で判断します。
降雨の場合には、filtered値は下がるのでintensityは雨足により正の値をとります。
露などの場合は、この値は少しだけ上がります。(降雨時程ではない)

更に降雨があった場合の波形

更に降り続くと、baselineも移動して来たことが分かる。(上図最下段右方向に注目)

ヒーターを入れた場合の波形 filtered とヒーター温度の関係

ヒーターを入れてセンサーを加熱すると抵抗値が乾燥時の値に戻って行く様子が観察できる。
ヒーターは、雨を検知すると、自動で入り加熱する、降雨が終了しても20分程度は加熱を続けセンサ
ー表面を乾燥させる。

降雨終了とセンサー表面の乾燥終了

降雨が終了し、センサーもヒーターにより乾燥すると、filteredとbaselineの値も元に戻って行く様子が
分かる。
ヒーターが無いとこの過程が自然乾燥のみなのでセンサーの傾き角度なども影響を受けてすぐには乾燥
しない、結果雨の終了告知が遅れる、ヒーターが付いていれば上図で一目のように問題は解決できる。

本センサーのメリットとデメリット

この種の方法では、「電蝕」という問題があります。
例えば安価なコンパレーター式等では直流電圧が印加されるため、センサー表面で電気分解が
起こります、これはセンサーの寿命を著しく短くしてしまう主な原因になります。
本方式では、センサー電極の極は交互に変わるため交流駆動となります。
測定の度に電流の向きを逆にすることで、電極に付着するイオンが中和され、金属が溶け出
す「電蝕」を劇的に抑えられるのです。

その他、方式による欠点としては、高抵抗値を測定している関係で雨によるセンサーの絶縁状態
の低下が直接測定に影響するのでCPUのポートに繋がる線はコネクターなどを経由せず直接配線
しないとトラブルのもとになります。
実際製作した最初の試作機はコネクター経由で繋がっていたため、雨の侵入で動作不良を起こし
ました、最終的なモデルではセンサーからの線は直結に変更しました。

不満もありますが、実は大きな利点もあるのです。
それは、じわじわ降る雨も検知できるという点です。
実は、他に市販されているRG-15のような高価な降雨センサーでもじわじわ雨は検知出来ま
せん、光学式は最近車のワーパー自動起動などでも使用されていますが、人肌で感じる湿った
何とも言えない感じは検出出来ません、理由はガラス越しのセンサーを使っている為です。
地面は濡れて明らかに雨が降っているのにセンサーが反応しないという場面は実は多いです。
その点唯一このセンサーは反応しますので捨てがたい存在です。
感度が良い反面、結露や夜露、霧などに反応し易く厄介な面もあります。
ヒーター装置などセンサー表面を適宜乾かせておく仕組みは必須になります。

センサーの設置角度は実際の雨で調整が必要です、角度が大きいと検出出来ない事があります。
逆に角度が緩やかだと、降雨のストップした事が検出出来なくなります。(雨粒溜りが発生する)
雨滴センサーの設置における最適な角度は、設置場所や用途によって多少異なりますが、一般的に水平面に対して約20度の傾斜が最適とされています。
この20度という角度は、以下の理由で推奨されています。(ヒーター無しではもう少し角度が必要)
汚れにくい: 雨水とともに汚れが流れやすいため、センサーの感度低下を抑えられます。 
適度な排水性: 水滴がセンサー表面に長時間留まりすぎず、かつ素早く流れ去りすぎないため、安定した検出が可能です。

私の経験では、ガラエポのプリント基板式は,直流駆動の場合、1カ月程度すると表面が劣化し
てそのままでは使用困難になります、従って交流駆動は必須です。

劣化したセンサー表面

ここ迄はRC方式降雨センサーの準備段階でした。
このセンサーは、構造が単純なだけに、逆に正常に機能させるには相当な苦労がありました。
しかし、最終的にセンサーの振る舞いを視覚化することにより解決に至りました。
ここでは視覚化という事が大事な要素だと痛感した次第です。
次回はいよいよRC降雨センサーを完成させます。