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

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

本題 無安定マルチバイブレーター方式(RC方式)を作ります。

完成させるのは想像以上に難しかった!!

前半編で原理的な部分について解説しましたが、実際に試験稼働させてみて、修正すべき場所が
あったので一部変更しました。
この方式で問題が出る部分は、発振周期のブレです。
周期が例えばbaseline周期(降雨が無い時の発振周期)が707usだったとすると、実際には706,708,709
などの周期がランダムに混在観測されます。
これらの発生頻度が少なく、かつbaseline周期から極近い値でランダムな傾向がある場合には問題あり
ませんが、そうでない場合には装置の前提が崩れ使い物にならなくなります。
具体的には、降雨が無い通常の状態時にbaselineの周期が別の周期に飛ぶ現象です。
例えば周期707usの周期が本来のbaseline周期だとすると何でもないのに710usなどにbaselineが転移
する現象です。
実際に今までの試験機ではその状況が実験中発生して都度「だめかな」と諦めかけた事が数回あり
ました、無安定マルチバイブレーターを改め不安定マルチバイブレーターと改名したい位の話です。
最終的に今回以下に示す「測定方法の変更」により従来の方法より改善されましたので発表しましたが
やはりRC発振器というものは不安定な要素が多く、更に自分で発振して自分でその様子を確認する
という手法がより全体を難しくしている原因の様に思います。
今後この方式も破綻する可能性もゼロではありませんが、万一そのような事態が発生したらまた何か
良い方法を見つけて改良するつもりです。
今のところは順調に動いています。
最初はセンサーの問題かと疑っていましたが、これは「発振器とそれを外部から覗く」時の問題で
した。

1, ESPr Developerの発振ポートの変更
変更したきっかけは、原発振周期が予想以上にふらつく為、試験的に変更しました。
しかし、結果は同じだったので実際には変える必要はありません。(どちらでも同じ)

2, 発振周期の測定方法の変更
スクリプト上の話になりますが、以下の様に変更しました。
この方法をとることにより今までで一番安定した周期を捉えることが出来ましたのでこちらに
変更します。

// =========================
// RC発振
// =========================
unsigned long measureOnce() {

  pinMode(12, OUTPUT);//発振回路強制リセット 代替ポート->5
  digitalWrite(12, LOW);
  digitalWrite(14, LOW);//代替ポート->16
  delayMicroseconds(50);

  pinMode(12, INPUT); //充電
  digitalWrite(13, LOW);//代替ポート->4
  unsigned long start = micros();
  digitalWrite(14, HIGH);
  while (digitalRead(12) == LOW) {
    if (micros() - start > 3000) return 0;
    yield();
 }

  //digitalWrite(14, LOW); 放電
  //unsigned long start = micros();
  //digitalWrite(13, HIGH);

  //while (digitalRead(12) == HIGH) {
  //if (micros() - start > 3000) return 0;
  //yield();
  // }

  return micros() - start;
}

スクリプトでは、最終的に充電時間のみ測定しています。
放電処理は削除しました。(コメントアウト部分)
放電処理の代わりに先頭部分で強制的にリセット(放電と同等の働き)しています。

回路図 CPU部 (前半編のポート4,5,16仕様でも問題ありません)







降雨が始まったらヒーターONします。
雨あがりを検出すると、ちょっと時間をおいてセンサーが乾燥してきたらヒーター
OFFします。
ヒーターに並列に入っているLEDはヒーター点火と連動して点灯します。
設定されているデューティー比に応じて点滅します。
雨が降ると同時にヒーターは点火されますのでLEDで降雨中と判断できます。
雨が止んでもヒーターが入っていれば点灯を続けます。

霜や結露、露付なども検出して同様にヒーターによる乾燥処理を行います。
ヒーターはPWM省電力制御を行っています。

火力は、コマンド送信で操作出来ます。(0-100可変可)
あまり弱くすると降雨停止後の雨上がりを検知し難くなりますので調整が必要です。
ヒーターのオンオフは自動機能の検知に応じたON/OFFに委ねられます、JSONデーター内
の「autoHeater」の値 ON->1 OFF->0が状態を知らせます。
設定した数値は内部EEPROMに保存され次回再起動時に読み込まれます。

ヒーターの温度は外気温にかなり影響されるので一概には言えませんが、概ね外気温10度前後で100%
にすると48度前後迄上昇します。
70%に設定すると概ね48 x 70% = 33度前後までしか上がりません。

設定 例  動作
0     ヒーターOFF
50    弱
70    標準
80    強
100    MAX



CPU部の主要必要部品

スイッチサイエンス ESP-WROOM-02ボード
ESPr Developer ¥2,420
高精度 金属皮膜抵抗 1/4W10kΩ ±0.05%
1本 ¥250 
積層セラミックコンデンサー 0.1μF50V C0G
5mm 10個入り ¥250
三端子レギュレーター 5V1.5A NJM7805FA
購入先 秋月電子 ¥60
NchパワーMOSFET 30V62A IRLB8721PBF 購入先 秋月電子 ¥120
マイクロB USBケーブル5V電源線 購入先 アマゾン ¥999 (オスメス5本セット)
AC to DC 12V 3A アダプター 購入先 アマゾン ¥1,499

ESP-WROOM-02はスイッチサイエンス製のものを使用します、基板のポートに直接CRを取り
付けられますのでこの用途には最適です、最短距離での構築が可能です。

最短で配線出来るメリットがある

発振用の0.1μFコンデンサ、抵抗10kΩは、肝の部品なのでなるべく温度特性の良いものをお使いください。 (推奨 高精度 金属皮膜抵抗 1/4W10kΩ ±0.05%  積層セラミックコンデンサー 0.1μF50V C0G)
センサー部とCPU間の配線は極力短くして下さい。(20cm以下を目指す)
また、センサーとCPU間はコネクターなどを介さず直結して下さい、コネクターなどを介すとトラブル
の原因になります。(トラブル経験済)

センサー・ヒーターブロック必要部品

タカチ SW-T55B 購入先
マルツ 購入価格 ¥198
真鍮シート 154x50x3mm 金属板プレート 購入先 アマゾン ¥1,209
防犯カメラブラケット 360度調整可能 購入先 アマゾン ¥1,599 (4個)
ねじ付き M4 x 10mm 304ステンレス鋼 ナットカバー 購入先 アマゾン ¥964 (6個)
M4 x20mmステンレス 皿頭 小ねじ 購入先 アマゾン ¥600 (20本)
M4 六角ロックナット
購入先 アマゾン ¥599 (50個)
六角穴付きネジ M3x3 mm 100個入り 購入先 アマゾン ¥1,116(100本)
PTCヒータープレート
DC 12V (80℃ 2—5W)
購入先 アマゾン ¥1,478 (2個)
aitendo 感雨センサ [RS-1] 購入先 aitendo ¥110
DS18B20 防水温度センサー  購入先 アマゾン
¥1,118 (3個)
ネオジム磁石 購入先 アマゾン ¥1,699 (10個)
センサーケース固定に使用
コルゲートチューブ
内径7.4mm 10m 購入先 アマゾン ¥1,209
電源ケーブル引き回しに使用

必要な工具・部材

セメダイン 耐熱エポキシ 接着剤 耐熱温度240℃ 購入先 アマゾン ¥936
充填 接着用パテ 耐熱
購入先 アマゾン ¥749
シリコンシーリング材 チューブタイプ ブラック 防水・補修用 購入先 アマゾン ¥598
アサヒペン 高耐久ラッカースプレー 300ml ツヤ消し黒 購入先 アマゾン ¥853
鉄板切れ端 30x30x1.6mm x1 100x30x1.6mmx1 購入先
横山テクノ
新潟精機 セット M3x0.5 ドリル径2.5mm 購入先 アマゾン ¥790
ミッチャクロン マルチ 420ml 
購入先 アマゾン
購入価格 ¥1,700



センサー・ヒーター部の組み立て

センサーの収納ケースというか置台はタカチの耐熱プラケースを使用しています。
ケースの蓋は使用せず、真鍮板50x60mmにヒーターを裏面に接着したものをプラケース内に耐熱
充填パテを使用して押し込めます。

真鍮板の上面には、antendoの雨センサーをM3  3mmねじ4本で固定します。
取り付けの際、センサーの絶縁には特に注意が必要です、ねじがパターンに接触しない様絶縁ワッシャ
などを使用します、リード取り出し部裏面も金属に触れないように絶縁に注意します。
取り付け用ブラケットは上記のカメラブラケットの底部プレートを取り外してアーム部分のみ
使用します、丸い玉とM4ねじナットで固定します。

アーム部分のみ使用する
アームを取り付ける
真鍮版50x60mmに4か所
M3x3mmのねじを切って塗装する
真鍮版 裏側にヒーターを接着する
こうゆう状態にして充填 接着用パテ 耐熱を充填する
(センサーは外した方が良い)

今回使ったパテは硬くて非常に使いずらかった、別に良い物があればそちらにして下さい、お勧めはしません(臭いし、手につく、石鹸では落ちるけど)。
今回使ったパテはぴったり1回分の量だったのでそうゆう意味では使いやすい。

Fクランプ  購入先 アマゾン ¥2,480
クランプで2箇所固まるまで固定しておく
最後に雨センサーをM3x3mmねじで固定する
完成 
接合部にすき間シール剤で防水処理をする
鉄板 1.6mm厚
85x30mm 1枚 本体固定用
30x30mm 1枚 センサー補強
M4ねじでケースに固定する
小さい方は、センサーをケー
スに固定時に使用する。
大きい方は、ケース底面に
設置用ネオジム磁石に対応
する為の鉄板になります
温度センサーは雨センサー横に耐熱接着剤で固定(クランプして一晩置く)
塗装する前に接着しておく
塗装時にはマスキングテープ
を使用する
雨センサー、温度センサー、ヒーターの線はケース底面から内部に接続する
雨滴が侵入しないよう対策する
取り付け固定部にM4ねじでネオジム磁石を取り付ける

運用の様子

凛々しいお姿


最終完成品の 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";//Your MQTT Server Address
const int mqttPort = 1883;

const char* pubTopic      = "rain_sensor/rc/data";
const char* topicLearn    = "rain_sensor/rc/learning";
const char* topicRainth   = "rain_sensor/rc/rainth";
const char* topicHeater   = "rain_sensor/rc/heater";
const char* topicHeaterth = "rain_sensor/rc/heaterth";
const char* topicReset    = "rain_sensor/rc/reset";

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

// =========================
// EEPROM
// =========================
#define EEPROM_SIZE 64
#define ADDR_RAIN   0
#define ADDR_HEATER 8
#define ADDR_BASE   16
#define ADDR_HPOWER 24

// =========================
// 設定値
// =========================
bool learning = false;
float rainth = 5;
float heaterth = 2;

// =========================
// 状態
// =========================
float baseline = 707;
unsigned long period2 = 0;
float intensity = 0;
bool raining = false;

int heaterPower = 0;    
bool autoHeater = false;
unsigned long heaterOffStart = 0;

// =========================
// フィルタ設定
// =========================
#define FILTER_SIZE 12
#define MAX_SPIKE 300
#define AUTO_LEARN_RATE 0.01
#define STABLE_TH 15
#define DRY_TH 5

float buf[FILTER_SIZE];
int idx = 0;
bool filled = false;

float lastRaw = 0;
bool hasLast = false;

// =========================
// EEPROM
// =========================
void saveBaseline() {
  EEPROM.put(ADDR_BASE, baseline);
  EEPROM.commit();
}

void loadAll() {
  EEPROM.get(ADDR_RAIN, rainth);
  EEPROM.get(ADDR_HEATER, heaterth);
  EEPROM.get(ADDR_BASE, baseline);
  EEPROM.get(ADDR_HPOWER, heaterPower);

  if (isnan(rainth))   rainth = 5;
  if (isnan(heaterth)) heaterth = 2;
  if (isnan(baseline)) baseline = 707;

  if (heaterPower < 0 || heaterPower > 100) {
    heaterPower = 0;
  }
}

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

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

// =========================
// WiFi
// =========================
void setupWiFi() {
  WiFi.config(IPAddress(192,168,1,100), IPAddress(192,168,1,1), IPAddress(255,255,255,0));//Your IP Address
  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];

  if (strcmp(topic, topicLearn) == 0) {
    if (msg == "1") learning = true;
  }

  else if (strcmp(topic, topicRainth) == 0) {
    int v = msg.toInt();
    if (v >= 0 && v <= 50) {
      rainth = v;
      EEPROM.put(ADDR_RAIN, rainth);
      EEPROM.commit();
    }
  }

  else if (strcmp(topic, topicHeater) == 0) {
    int v = msg.toInt();
    if (v >= 0 && v <= 100) {
      heaterPower = v;
      EEPROM.put(ADDR_HPOWER, heaterPower); 
      EEPROM.commit();
    }
  }

  else if (strcmp(topic, topicHeaterth) == 0) {
    int v = msg.toInt();
    if (v >= 0 && v <= 50) {
      heaterth = v;
      EEPROM.put(ADDR_HEATER, heaterth);
      EEPROM.commit();
    }
  }

  else if (strcmp(topic, topicReset) == 0) {
    ESP.restart();
  }
}

void reconnect() {
  while (!client.connected()) {
    if (client.connect("rc_rain_sensor")) {
      client.subscribe(topicLearn);
      client.subscribe(topicRainth);
      client.subscribe(topicHeater);
      client.subscribe(topicHeaterth);
      client.subscribe(topicReset);
    } else {
      delay(1000);
    }
  }
}

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

// =========================
// ヒーター制御
// =========================
void heaterControl() {

  int duty = 0;

  if (autoHeater) {
    duty = heaterPower;
  }

  unsigned long period = 4000;
  unsigned long onTime = period * duty / 100;
  unsigned long t = millis() % period;

  digitalWrite(HEATER_PIN, (t < onTime));
}

// =========================
// フィルタ
// =========================
float filter(float v) {

  if (hasLast && abs(v - lastRaw) > MAX_SPIKE) return lastRaw;

  lastRaw = v;
  hasLast = true;

  buf[idx++] = v;
  if (idx >= FILTER_SIZE) {
    idx = 0;
    filled = true;
  }

  int n = filled ? FILTER_SIZE : idx;

  float temp[FILTER_SIZE];
  for (int i = 0; i < n; i++) temp[i] = buf[i];

  for (int i = 0; i < n - 1; i++) {
    for (int j = i + 1; j < n; j++) {
      if (temp[i] > temp[j]) {
        float t = temp[i];
        temp[i] = temp[j];
        temp[j] = t;
      }
    }
  }

  return temp[n / 2];
}

// =========================
// RC発振
// =========================
unsigned long measureOnce() {

  pinMode(12, OUTPUT);//発振回路リセット 代替->5
  digitalWrite(12, LOW);
  digitalWrite(14, LOW);//代替->16
  delayMicroseconds(50);

  pinMode(12, INPUT); //充電
  digitalWrite(13, LOW);//代替->4
  unsigned long start = micros();
  digitalWrite(14, HIGH);
  while (digitalRead(12) == LOW) {
    if (micros() - start > 3000) return 0;
    yield();
 }

  //digitalWrite(14, LOW); 放電
  //unsigned long start = micros();
  //digitalWrite(13, HIGH);

  //while (digitalRead(12) == HIGH) {
  //if (micros() - start > 3000) return 0;
  //yield();
  // }

  return micros() - start;
}

unsigned long measureStable() {

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

  for (int i = 0; i < N; i++) {
    buf2[i] = measureOnce();
    delayMicroseconds(200);
  }

  for (int i = 0; i < N - 1; i++) {
    for (int j = i + 1; j < N; j++) {
      if (buf2[i] > buf2[j]) {
        unsigned long t = buf2[i];
        buf2[i] = buf2[j];
        buf2[j] = t;
      }
    }
  }

  return buf2[N / 2];
}

// =========================
// 自動制御
// =========================
void autoLogic(void) {

  if (!raining && intensity > rainth) raining = true;
  else if (raining && intensity < (rainth * 0.6)) raining = false;

  if (intensity > heaterth) {
    autoHeater = true;
    heaterOffStart = 0;
  }

  if (autoHeater && intensity <= 0) {
    if (heaterOffStart == 0) {
      heaterOffStart = millis();
    } else if (millis() - heaterOffStart >= 900000UL) {
      autoHeater = false;
      heaterOffStart = 0;
    }
  }

  if (intensity > 0) heaterOffStart = 0;
}

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

  pinMode(12, INPUT);
  pinMode(13, OUTPUT);
  pinMode(14, OUTPUT);
  pinMode(HEATER_PIN, OUTPUT);

  EEPROM.begin(EEPROM_SIZE);
  loadAll();

  sensors.begin();
  setupWiFi();

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

// =========================
// loop
// =========================
void loop() {

  if (!client.connected()) reconnect();
  client.loop();

  heaterControl();
  readTemp();

  period2 = measureStable();
  float filtered = filter(period2);

  if (learning) {
    baseline = filtered;
    saveBaseline();
    learning = false;
  }

  bool stable = abs(filtered - baseline) < STABLE_TH;
  bool dry = (baseline - filtered) < DRY_TH;

  if (!raining && heaterPower == 0 && stable && dry) {
    baseline += (filtered - baseline) * AUTO_LEARN_RATE;
  }

  intensity = baseline - filtered;
  if (intensity < 0.5) intensity = 0;

  autoLogic();

  if (millis() - lastPublish > 1500) {

    char payload[256];
    snprintf(payload, sizeof(payload),
      "{\"intensity\":%.2f,\"period2\":%lu,\"filtered\":%.2f,\"baseline\":%.2f,\"temperature\":%.2f,\"heaterPower\":%d,\"autoHeater\":%d,\"heaterth\":%.2f,\"rainth\":%.2f,\"raining\":%d}",
      intensity,
      period2,
      filtered,
      baseline,
      temperature,
      heaterPower,
      autoHeater ?1 : 0,
      heaterth,
      rainth,
      raining ? 1 : 0
    );

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

  delay(500);
}





最終完成品の
無安定マルチバイブレーター(RC)方式降雨検知システムフロー

[{"id":"95bfc20d892275d7","type":"tab","label":"RC方式 降雨センサー","disabled":false,"info":"","env":[]},{"id":"5788edb49a0011f7","type":"json","z":"95bfc20d892275d7","name":"","property":"payload","action":"obj","pretty":false,"x":310,"y":80,"wires":[["09ca2e8c5f4c2d37","804c05a056b2ccd5","6e4f860b0cbbb4e8","e16265144143572d","5bbcae72f4a39a31"]]},{"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":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":280,"y":140,"wires":[]},{"id":"e169ebf5f49c91a7","type":"mqtt out","z":"95bfc20d892275d7","name":"","topic":"rain_sensor/rc/heater","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"cf1776747da46a7c","x":660,"y":500,"wires":[]},{"id":"0f241fe10a137de9","type":"comment","z":"95bfc20d892275d7","name":"ヒーターデューティ比設定","info":"","x":150,"y":520,"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":390,"y":500,"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":390,"y":580,"wires":[["e169ebf5f49c91a7"]]},{"id":"d567e157de261f8c","type":"mqtt out","z":"95bfc20d892275d7","name":"","topic":"rain_sensor/rc/reset","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"cf1776747da46a7c","x":620,"y":640,"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":390,"y":640,"wires":[["d567e157de261f8c"]]},{"id":"954aaec09adfef7b","type":"comment","z":"95bfc20d892275d7","name":"システムリセット","info":"","x":130,"y":640,"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":390,"y":540,"wires":[["e169ebf5f49c91a7"]]},{"id":"776d073b64516dea","type":"ui-chart","z":"95bfc20d892275d7","group":"a063893cef8e3d33","name":"雨の強さ","label":"雨の強さ","order":1,"chartType":"line","category":"topic","categoryType":"msg","xAxisLabel":"","xAxisProperty":"","xAxisPropertyType":"timestamp","xAxisType":"time","xAxisFormat":"","xAxisFormatType":"auto","xmin":"","xmax":"","yAxisLabel":"","yAxisProperty":"payload","yAxisPropertyType":"msg","ymin":"","ymax":"","bins":10,"action":"append","stackSeries":false,"pointShape":"crossRot","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":false,"width":6,"height":8,"className":"","interpolation":"linear","x":680,"y":180,"wires":[[]]},{"id":"a7b4713ad1462286","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":370,"y":300,"wires":[["057abde2e90771b0"]]},{"id":"057abde2e90771b0","type":"mqtt out","z":"95bfc20d892275d7","name":"","topic":"rain_sensor/rc/learning","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"cf1776747da46a7c","x":660,"y":300,"wires":[]},{"id":"74c2dba53ac1f645","type":"inject","z":"95bfc20d892275d7","name":"雨閾値","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"5","payloadType":"str","x":370,"y":340,"wires":[["768b837fa3be1080"]]},{"id":"768b837fa3be1080","type":"mqtt out","z":"95bfc20d892275d7","name":"","topic":"rain_sensor/rc/rainth","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"cf1776747da46a7c","x":660,"y":340,"wires":[]},{"id":"6ff75cda6961eb7b","type":"inject","z":"95bfc20d892275d7","name":"露閾値","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"3","payloadType":"str","x":370,"y":380,"wires":[["fd97dce4326d9a3a"]]},{"id":"fd97dce4326d9a3a","type":"mqtt out","z":"95bfc20d892275d7","name":"","topic":"rain_sensor/rc/heaterth","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"cf1776747da46a7c","x":670,"y":380,"wires":[]},{"id":"9fd214509d13d29e","type":"comment","z":"95bfc20d892275d7","name":"ベースライン学習","info":"","x":130,"y":300,"wires":[]},{"id":"0b688cec9f658ed8","type":"comment","z":"95bfc20d892275d7","name":"雨閾値設定","info":"","x":100,"y":340,"wires":[]},{"id":"5bcf03282360c3a9","type":"comment","z":"95bfc20d892275d7","name":"露ヒーター閾値設定","info":"","x":130,"y":380,"wires":[]},{"id":"09ca2e8c5f4c2d37","type":"function","z":"95bfc20d892275d7","name":"temperature","func":"var data = msg.payload.temperature;\nmsg.payload = data;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":510,"y":140,"wires":[["0ed4379b893acdcc"]]},{"id":"0ed4379b893acdcc","type":"ui-chart","z":"95bfc20d892275d7","group":"a063893cef8e3d33","name":"センサー温度","label":"センサー温度","order":4,"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":"crossRot","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":false,"width":6,"height":8,"className":"","interpolation":"linear","x":700,"y":140,"wires":[[]]},{"id":"804c05a056b2ccd5","type":"function","z":"95bfc20d892275d7","name":"rain","func":"let rain = msg.payload.raining;\nif(rain){\n    flow.set(\"rain_status\", \"1\");\n}else{\n    flow.set(\"rain_status\", \"0\");\n}\n","outputs":0,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":490,"y":220,"wires":[]},{"id":"30ada0086d93994e","type":"inject","z":"95bfc20d892275d7","name":"雨 ->\"0\"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"0","payloadType":"str","x":910,"y":420,"wires":[["de2ba44f79baa293"]]},{"id":"448b79a53106666e","type":"inject","z":"95bfc20d892275d7","name":"快晴 ->\"1\"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"str","x":920,"y":460,"wires":[["de2ba44f79baa293"]]},{"id":"904079669d4b775c","type":"inject","z":"95bfc20d892275d7","name":"晴れ ->\"2\"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"2","payloadType":"str","x":920,"y":500,"wires":[["de2ba44f79baa293"]]},{"id":"be6075b6fee650e8","type":"inject","z":"95bfc20d892275d7","name":"曇天 ->\"3\"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"3","payloadType":"str","x":920,"y":540,"wires":[["de2ba44f79baa293"]]},{"id":"f88246dbc2444a5f","type":"inject","z":"95bfc20d892275d7","name":"雪 ->\"4\"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"4","payloadType":"str","x":910,"y":580,"wires":[["de2ba44f79baa293"]]},{"id":"de2ba44f79baa293","type":"function","z":"95bfc20d892275d7","name":"天候画像変換","func":"var icon;\nvar weather = msg.payload;\nswitch(weather){\n    case \"0\":\n        icon = \"/Weather/rain.png\";\n        break;\n    case \"1\":\n        icon = \"/Weather/sunny.png\";\n        break; \n    case \"2\":\n        icon = \"/Weather/sunny_cloudy.png\";\n        break; \n    case \"3\":\n        icon = \"/Weather/cloudy.png\";\n        break;\n    case \"4\":\n        icon = \"/Weather/snow.png\";\n        break;\n}\nmsg.payload = icon;\nreturn msg","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":1140,"y":420,"wires":[["c21c4d4869673654"]]},{"id":"4a8d400bb086977a","type":"mqtt in","z":"95bfc20d892275d7","name":"","topic":"sky_pat","qos":"2","datatype":"auto-detect","broker":"ab768bff89bc7714","nl":false,"rap":true,"rh":0,"inputs":0,"x":990,"y":360,"wires":[["e3189619b292d4a6"]]},{"id":"e3189619b292d4a6","type":"function","z":"95bfc20d892275d7","name":"雨以外は通過","func":"var tenki = String(msg.payload);\nvar rain =flow.get(\"rain_status\");\nif(rain ==\"1\"){\n     data = \"0\";\n}\nvar data = \"0\";\nif (rain == \"0\" && tenki != \"5\"){\n     var temp = tenki;\n     flow.set(\"sun_status\",temp);\n     data = temp;\n}\nif(rain == \"0\" && tenki ==\"5\"){\n     data = flow.get(\"sun_status\");\n}\nmsg.payload = data;\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1140,"y":360,"wires":[["de2ba44f79baa293"]]},{"id":"c21c4d4869673654","type":"ui-template","z":"95bfc20d892275d7","group":"6038346d88573885","page":"","ui":"","name":"今の天気","order":1,"width":0,"height":0,"head":"","format":"<template>\n    <div style=\"text-align:center\">\n        <img\n      :src=\"msg.payload\"\n      alt=\"weather_icon\"\n      width=\"92\"\n      height=\"92\"\n    />\n    </div>\n</template>","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1320,"y":420,"wires":[[]]},{"id":"d82aa761f21c9651","type":"inject","z":"95bfc20d892275d7","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"60","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"5","payloadType":"str","x":910,"y":300,"wires":[["e3189619b292d4a6"]]},{"id":"6e4f860b0cbbb4e8","type":"function","z":"95bfc20d892275d7","name":"Period2","func":"var temp = msg.payload.period2;\nmsg.payload = temp;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":500,"y":40,"wires":[["767d27bc48633b72"]]},{"id":"e16265144143572d","type":"function","z":"95bfc20d892275d7","name":"intensity","func":"var intensity = msg.payload.intensity;\nmsg.payload = intensity;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":500,"y":180,"wires":[["776d073b64516dea"]]},{"id":"18d57f4a3aa454ac","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":400,"y":460,"wires":[["e169ebf5f49c91a7"]]},{"id":"767d27bc48633b72","type":"ui-chart","z":"95bfc20d892275d7","group":"a063893cef8e3d33","name":"Period2","label":"Period2","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":"","ymax":"","bins":10,"action":"append","stackSeries":false,"pointShape":"crossRot","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":false,"width":6,"height":8,"className":"","interpolation":"linear","x":680,"y":40,"wires":[[]]},{"id":"5bbcae72f4a39a31","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":490,"y":80,"wires":[["ba89c5155509369e"]]},{"id":"ba89c5155509369e","type":"ui-chart","z":"95bfc20d892275d7","group":"a063893cef8e3d33","name":"filtered","label":"filtered","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":"","ymax":"","bins":10,"action":"append","stackSeries":false,"pointShape":"crossRot","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":false,"width":6,"height":8,"className":"","interpolation":"linear","x":670,"y":80,"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":2,"showTitle":false,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"ab768bff89bc7714","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":"6038346d88573885","type":"ui-group","name":"天気","page":"586bd0f0c91d94eb","width":6,"height":1,"order":1,"showTitle":true,"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":"c2e2f2a8c22ed8d2","type":"global-config","env":[],"modules":{"@flowfuse/node-red-dashboard":"1.30.2"}}]

NODE-RED上の作業

データーはJSON式のかたまりで送信されて来ます。
個々の情報を取り出す際にはまずJSONノードを置き「動作」の項目を「常にjavaScriptオブ
ジェクトに変換」に設定します。
その後にfunctionノードを置き「var temp = msg.payload.rain);」のように必要な
情報を引き出して後に続けます。
これらの情報の内、実際にダッシュボードに表示させる為のデーターは、今雨が降っている
という事を伝える rain: 1 (または rain:0) をグラフィックのアイコンで表示する。
雨の勢いを表す intensity = baseline – filtered  の値をグラフにしたものです。
センサーが湿ったり、降雨があるとグラフが上昇して行きます。
センサーが乾燥した状態で0にならない場合には、baselineの調整が必要です。
設置時にはずれている可能性があるので必ず行います。
またセンサーの温度もグラフで表示します。

ダッシュボードに表示しないその他の情報は、デバッグ用のものです、これらよりセンサーの現在の
状態を把握できます。

フロー上には、センサーの降雨が無い状態のbaselineを学習するボタンがあります。
雨が降っていないセンサーが乾いている時に実行します。
設置時には必ず実行します。
そうしないと雨が降っていないのにも関わらず雨を検出してしまします。
降雨グラフの数字が0以外の位置にあれば設定がずれていますので必要になります。
実行すると内部EEPROMに保存されて再起動時に読み込まれます。
センサーの表面の状態は、外部要因によって変化しますので、この操作が必要になります。
実際にはどの程度変化して行くというデーターが無いのではっきりとした事は言えません。
使用して行く内に当初の発振周波数より大幅にずれる場合には、センサーの絶縁不良などが考えられ
ます、最悪交換が必要です、デジタルマルチメーターで抵抗値を測って∞出ない場合には交換して下さ
い。
微調整程度なら、センサーが乾燥している状況をとらえて自動学習する機能が組み込まれていいます。
大幅にずれる場合には、絶縁不良やセンサー不良を疑ってください。

その下方に、雨の降り始めの閾値設定ボタンがあります。
雨の降り始めと露との差別化にも重要です。
実際の動作にはバタつき防止のため、雨上がりに対して若干のヒステリシスを内部に設けています。
設置時には必ず行います。

同様に露を検知してセンサーを乾燥させる為のヒーター点火の閾値heaterthボタンもあります。
設置時には必ず行います。

ヒーターの火力を手動で指定する機能があります。
コマンドを送信して火力を指定します。
MQTT送信データーとしては以下の情報になります。
トピック rain_sensor/rc/heater
payload 0 ~ 100
この数値は、PWMのデューティ制御に使用される値です。
100でデューティ比100%になります。 
0を入れると実質ヒーター停止と同じになります。
ヒーターの温度とセンサー表面の乾燥具合は、ダッシュボードに表示される、intensityとヒーター温度
のグラフから判断できます。
尚、実際のヒーターON/OFF操作は、CPUが判断して行います。

上記の各設定値は内部EEPROMに書き込まれ、再起動時に読み込まれます。
設置時には必ず行ってください。

次に、ダッシュボードに表示する手順をご説明します。

事前にRaspberry Pi4やPi5にNODE-RED とMQTTブローカーを下記の記事を参考に設定して準備します。
https://marginalvillage.com/page/4/

NODE-REDで右上にある三のアイコンをクリックして「パレットの管理」を開き
「ノードを追加」で「@flowfuse/node-red-dashboard」を追加して下さい。

雨以外の天気の「曇り」、「晴れ」、「快晴」の情報も無いと意味をなさ無いのでこのブログ内にある
「AI空模様告知システム」を作成運用してからになります。
https://marginalvillage.com/page/2/
作成できな場合には、晴れ、曇天などのボタンをクリックすればとりあえずシュミレーション
することは可能です。

まず、空模様のアイコンを準備します。
自分で作成するか、フリーの素材を活用させてもらうか、最近はAIソフトが作成してくれるとい
う選択肢もあるようです
面倒な場合にはこの下にあるアイコンを画面コピーしても良いですね。

快晴
晴れ
曇天

自作する場合には以下のような無料サイトを利用すると良いでしょう。
https://www.photopea.com/l/ja_jp/
https://forest.watch.impress.co.jp/library/software/inkscape/
https://www.adobe.com/jp/express/

用意した天気の画像を入れるフォルダーは決まり事で通常 /home/pi/Public になります。
2026/3時点のRaspberry PI OS (other)->Legasy,64bit OS Bookwormではこのフォルダが漢字「公開」になっている場合があります。(最新のDebian Trixleでは検証していません)
その場合には、「Public」にフォルダー名を変えてください、そのままだと使用不可です。
このフォルダー内に画像を作って入れておくわけですが、デフォルトではその機能が無効に
なっていますのでまずは機能を有効にします。

Publicフォルダーを有効にする手順
NODE-REDサーバーを動かしているRaspberry Piの.「.node-red」フォルダーの中に「setting.js」というのがあります。
このファイルをMouspadで開き適当なところへ追加で書き加えます。
尚、「.node-red」は隠しファイルなので見つからない場合には、「Ctrl-H」を押して表示させて下さい。
「setting.js」に「httpStatic: ‘/home/pi/Public/’,」を加筆する。

    /**  
     * All static routes will be appended to httpStaticRoot
     * e.g. if httpStatic = "/home/nol/docs" and  httpStaticRoot = "/static/"
     *      then "/home/nol/docs" will be served at "/static/"
     * e.g. if httpStatic = [{path: '/home/nol/pics/', root: "/img/"}]
     *      and httpStaticRoot = "/static/"
     *      then "/home/nol/pics/" will be served at "/static/img/"
     */
    //httpStaticRoot: '/static/',
    httpStatic: '/home/pi/Public/',
    

最後にある「,」を忘れないようにして下さい、無いとNODE-REDが起動しません。
加筆したら保存してからNODE-REDを再起動させます。
画像は一旦「Weather」フォルダーを作成してその中に入れます。

雨センサー以外にお天気センサーも組み込みが必要にになりますので、ここでは原理的な解説
に留めます。
このサンプルでは、雨が止んでも他のセンサーからの情報が入らないので一旦雨を検知すると
雨のまま更新されませんので注意して下さい。
デバッグ用の天気ボタンをクリックすれば、ダッシュボード上に天気アイコンが表示されます。

降雨無し状態(通常) 上が原発振の散布図 時折ランダムな周期の乱れが見て取れます。
下が、フィルターを掛けて確率的に発生度の高い周期のみを捉えています。
これが、降雨無し時の基準(baseline)になります。
ぽつぽつと降雨開始の場面です。
一番上の折れ線グラフが0以上閾値(5)を超えているので降雨と検知されます。
2番目の散布図は原発振で降雨によりセンサーの抵抗値が下がり始め発振周期が数値の少ない方
つまり発振周波数は高い方向に振られてゆきます。
3番目はフィルターを掛けた後の周期の値ですが、若干周期の短い方へのブレが観測されます。
一番下はセンサーの温度を示します。
降雨検知により温度が急激に上昇に転じています。
更に降雨が続きます。
ヒーターは33度程度で飽和に転じています。
これは現在ヒーターのデューティー比が70%に設定されている為です。
もっと上げたい場合には、例えば100%にすると48度前後まで上昇します。
更に降り続く様子です。
発振波形は波長の短い方向へ大きく乱れます。
センサーはONのままで最大値付近の温度を維持します。
雨が止みました。
一番上のintensityは0を示し、発振器の周期もbaselineの707付近で一定になります。
センサー温度も降雨停止から15分程度でOFFになり下降して行くのが分かります。

上記の様に詳細な表示が不要の場合には、NORE-REDフローから不要なグラフアイコンを削除すれば
もっとシンプル表示に出来ます。

以上の様にセンサー表面のリアルな湿り具合や乾燥状態が視覚で確認できます。
他のガラス越しのセンサーより優れています。

上記降雨のグラフを1時間で積分しその値を実際の雨水桝の実測値と比較し一定の関係式が
求まれば1時間あたりの降雨量表示も可能になる。
また、霧や、露、霜、降雪時のセンサーの振る舞いなどは、年単位の観察が必要となるので今すぐには
お知らせ出来ないので分かった時点で加筆する予定です。

雨量計 購入先 アマゾン ¥995


そのような目的で、年単位での温度、湿度、気圧、照度、降雨量、センサー温度などの詳細データー
をエクセルに記録するNODE-REDフローを作成したのでいずれはこのデーターを使った分析と活用方
法も考えて行きたいです。(生きていれば)