Amazonでお湯はりセンサー買ったんです。そしたら新品なのに中身が汚れてて、フタを開けたら中から水が出てきたんです。新品なのに。電池を入れても液晶が付きません。新品なのに。Amazon上級者ならもうおわかりですね。この商品を買った人が使って壊して返品したのを、Amazonがそのまま “新品として” 売ってしまったのです。Amazonではこれが普通なのでハズレを引いた感じですね。

ということで、M5StickC Plusを使って自分で作ることにしました。せっかくWi-Fiで接続しているので、便利な機能を付けることにしました。
・お湯が沸いたら、LINEに通知する
・お湯が沸いたら、あの給湯器の音楽を流す
・バッテリーが残量が低下したら通知する

回路構成

回路はとてもシンプルです。G32がセンサー入力ピンで、GNDに1MΩの抵抗でプルダウンしています。VCCはどこにも繋がっていません。通常はプルダウンされているので、Lowが入力されます。

お湯側の端子が水に接触すると、今度はHIGHが入力されます。これは水の電気抵抗の方がプルダウン抵抗よりも低いため、VCCがG32に流れるためです。この仕組みを利用して、お風呂の水位を検出しています。

動作デモ

プログラム

/* 
 * お湯はりセンサー for M5StickC Plus
 * ver.2.0
 */

#include <M5StickCPlus.h>
#include <ssl_client.h>
#include <WiFiClientSecure.h>
#include <Ticker.h>

// 使用するGPIOポート --------
#define WATER_PIN  32 // お湯はりセンサー

// Wi-Fi設定 --------
const char *WIFI_SSID = "ssid";
const char *WIFI_PASS = "pass";

// LINEの設定 --------  https://notify-bot.line.me/my/ でトークン発行する
bool USE_LINE_NOTIFY = true;
const char* LINE_HOST = "notify-api.line.me";
const char* LINE_TOKEN = "yourtoken";

// 設定 --------
const int BATT_LOW_ALERM = 20;  // バッテリー残量アラーム(%) 低下時に通知

// 楽譜データ 1音={ オクターブ, 音階, 長さ(1/n) }、終了は0,0,0
const int MUSIC_BPM = 120;
#define DO  3
#define RE  5
#define MI  7
#define FA  8
#define SO 10
#define RA  0
#define SI  2
#define RR 20 // 休符
const uint8_t music_data_doremi[] = { // ドレミファソラシド(使ってない)
  4,DO,8, 4,RE,8, 4,MI,8, 4,FA,8, 4,SO,8, 4,RA,8, 4,SI,8, 5,DO,8, 0,RR,4,
  5,DO,8, 5,RE,8, 5,MI,8, 5,FA,8, 5,SO,8, 5,RA,8, 5,SI,8, 6,DO,8, 0,RR,4,
  6,DO,8, 6,RE,8, 6,MI,8, 6,FA,8, 6,SO,8, 6,RA,8, 6,SI,8, 7,DO,8, 0,RR,4,
  0,0,0, };
const uint8_t music_data_pipipi[] = { // ピピピッ ピピピッ ピピピッ(もうすぐお風呂がわきます)
  6,RA,16, 0,RR,32, 6,RA,16, 0,RR,32, 6,RA,16, 0,RR,4, 6,RA,16, 0,RR,32, 6,RA,16, 0,RR,32, 6,RA,16, 0,RR,4, 6,RA,16, 0,RR,32, 6,RA,16, 0,RR,32, 6,RA,16, 0,RR,4, 
  0,0,0 };
const uint8_t music_data_ohuro[] = { // お風呂がわきましたの曲
  6,SO,8, 6,FA,8, 6,MI,4, 6,SO,8, 7,DO,8, 6,SI,4, 6,SO,8, 7,RE,8, 7,DO,4, 7,MI,4, 0,RR,4, 
  7,DO,8, 6,SI,8, 6,RA,4, 7,FA,8, 7,RE,8, 7,DO,4, 6,SI,4, 7,DO,2,  
  0,0,0 };
const uint8_t *music_datas[3] = { music_data_doremi, music_data_pipipi, music_data_ohuro };

// デバッグに便利なマクロ定義 --------
#define sp(x) Serial.println(x)
#define spn(x) Serial.print(x)
#define spf(fmt, ...) Serial.printf(fmt, __VA_ARGS__)
#define lp(x) M5.Lcd.println(x)
#define lpn(x) M5.Lcd.print(x)
#define lpf(fmt, ...) M5.Lcd.printf(fmt, __VA_ARGS__)
#define array_length(x) (sizeof(x) / sizeof(x[0]))

// ================================================================================ 

// WiFiに接続する
void connect_wifi() {
  int i, stat;
  WiFi.mode(WIFI_STA);  // ステーションモード
  WiFi.disconnect();
  delay(100);
  WiFi.printDiag(Serial);
  while (true) {
    // 接続を試みる
    WiFi.begin(WIFI_SSID, WIFI_PASS);
    lpn("WiFi connecting...");
    spn("WiFi connecting...");
    for (i = 0; i < 150; i++) {
      stat = WiFi.status();
      spn(stat);
      if (stat == WL_DISCONNECTED) { //6
        spn(".");
        if (i == 149) {
          lpn("! ");
          sp("abort");
        }
      } else if (stat == WL_CONNECT_FAILED) { //2  (WL_NO_SSID_AVAIL=1)
        lpn("<<FAIL>>\n");
        sp(" err=" + String(stat) + " Failed");
        delay(3000);
        break;
      } else if (stat == WL_CONNECTED) { //3
        lp("<<OK>> connected!\n");
        lpn("IP= ");
        lp(WiFi.localIP());
        sp("\nWiFi connected!");
        spn("IP address: ");
        sp(WiFi.localIP());
        break;
      }
      delay(200);
    }
    // 接続できたか?
    if (stat == WL_CONNECTED) {  //接続成功 //3
      break;
    } else {  //接続失敗
      WiFi.disconnect();
      delay(3000);
    }
  }
}

// バッテリー残量簡易計算
int get_battery() {
  return round(((M5.Axp.GetBatVoltage() - 3.0f) / (4.2f - 3.0f)) * 100);
}

// ================================================================================ https POST
// 参考にしたページ https://qiita.com/nnn112358/items/8d2021bce1113d7e60ce

// LINE Notify APIにメッセージを送信する
bool send_line(String message) {
  bool res = false;
  spn("connecting LINE server...");
  WiFiClientSecure sclient;
  sclient.setInsecure();  // 証明書の検証をしない
  if (sclient.connect(LINE_HOST, 443)) {
    String query = "message=" + message;
    String request = String("")
      + "POST /api/notify HTTP/1.1\r\n"
      + "Host: " + LINE_HOST + "\r\n"
      + "Authorization: Bearer " + LINE_TOKEN + "\r\n"
      + "Content-Length: " + String(query.length()) +  "\r\n"
      + "Content-Type: application/x-www-form-urlencoded\r\n\r\n" 
      + query + "\r\n";
    spn("sending...");
    sclient.print(request);
    //sp(request);
    while (sclient.connected()) {
      String buff = sclient.readStringUntil('\n');
      //sp("|"+buff);
      break;
    }
    sp("done");
    res = true;
  } else {
    sp("failed");
  }
  sclient.stop();
  return res;
}

// ================================================================================ タイマー割込み

// BEEP音楽を再生する
Ticker ticker1;
int start_play_music = 0;
void play_music(int musicno) {
  start_play_music = musicno;
}

// バックグラウンドでBEEP音を再生する(10msごとに実行される関数)
void play_background() {
  static int musicno = 0;
  static int step = 0;
  static unsigned long stop_ms = 0;
  M5.Beep.update();

  // 再生開始
  if (start_play_music > 0) {
    musicno = start_play_music;
    start_play_music = 0;
    step = 0;
    stop_ms = 0;
  }

  // 次の音階を設定する
  if (musicno > 0 && stop_ms <= millis()) {
    int oct = music_datas[(musicno-1)][(step++)];
    int oto = music_datas[(musicno-1)][(step++)];
    int kyu = music_datas[(musicno-1)][(step++)];
    if (kyu > 0) {
      int freq = 0;
      int duration = ((60000 / MUSIC_BPM) * 4) / kyu;
      if (oto == RR) {
        M5.Beep.mute();
      } else if (oto <= 12) {
        freq = round(440.0 * pow(2.0, oto/12.0) * pow(2.0, (oto < DO) ? oct-4 : oct-5));
        M5.Beep.setBeep(freq, duration);
        M5.Beep.beep();
      }
      //spf("oct=%d oto=%02d kyu=%02d : freq=%d Hz duration=%d ms\n", oct, oto, kyu, freq, duration);
      stop_ms = millis() + duration;
    } else {
      M5.Beep.mute();
      musicno = 0;
    }
  }
}

// ================================================================================ セットアップ

void setup() {
  M5.begin();
  M5.Beep.begin();
  Serial.begin(115200);

  // LED設定
  pinMode(M5_LED, OUTPUT);
  digitalWrite(M5_LED, HIGH); //OFF

  // 水位センサー ピン設定
  pinMode(WATER_PIN, INPUT);

  //ディスプレイ&フォント設定
  M5.Lcd.setRotation(3);  // 横向き・ホームボタンが左
  M5.Lcd.fillScreen(TFT_BLACK);
  M5.Lcd.setTextColor(TFT_WHITE);
  M5.Lcd.setTextFont(1);
  M5.Lcd.setTextSize(1);
  M5.Lcd.setCursor(0, 2);

  // BEEP設定
  M5.Beep.setBeep(2000, 100);
  M5.Beep.beep();

  // タイマー割込みの開始:
  ticker1.attach_ms(10, play_background);

  // WiFi接続
  connect_wifi();
  delay(1000);
  M5.Beep.beep();
  delay(200);
  M5.Beep.beep();
}

// ================================================================================ メインループ

void loop() {
  static int mode = 0;
  static int detect_cnt = 0;
  int i;
  bool res;
  M5.update();

  // 1秒おきにフラグを立てる
  static uint32_t tm = 0;
  static uint32_t old_ms = 0;
  bool refresh = false;
  if (tm == 0 || tm+1000 < millis()) {
    refresh = true;
    tm = millis();
  }
  if (millis() < old_ms) tm = 0;
  old_ms = millis();

  // メインボタンを1回押したらお湯はり開始
  if(mode == 0 && M5.BtnA.wasPressed()) {
    sp("Oyuhari Start!");
    M5.Beep.beep();
    res = send_line("%0D%0Aお湯はりをします");  // LINEに通知する
    if (!res) lp("Line Error!");
    mode = 1;
    refresh = true;
  } else if(mode >= 1 && M5.BtnA.wasPressed()) {
    // メインボタンをもう1回押したら終了
    sp("Shutdown...");
    M5.Beep.beep();
    mode = 3;
    refresh = true;
  }

  // お湯の状態をチェック
  int oyu = digitalRead(WATER_PIN);
  digitalWrite(M5_LED, oyu ? LOW : HIGH);
  if (mode == 1) {
    if (oyu) detect_cnt ++;
    if (detect_cnt == 1) {
      play_music(2);  // ピピピッ(もうすぐお風呂がわきます)
    }
    if (detect_cnt >= 500) {  // 5秒連続して検出したら (100=1s)
      mode = 2;
      refresh = true;
      sp("Oyuhari Finished");
      play_music(3);  // お風呂がわきました
      send_line("%0D%0Aお風呂がわきました");  // LINEに通知する
    }
  }

  // モードに応じた画面表示
  static bool battnotified = false;
  if (refresh) {
    M5.Lcd.fillScreen(TFT_BLACK);
    M5.Lcd.setCursor(0, 4);
    M5.Lcd.setTextSize(2);
    int battery = get_battery();  // バッテリー残量取得
    lp("Battery "+String(battery)+"%\n\n");
    switch (mode) {
      case 0:
        lp("Push to Start\n");
        break;
      case 1:
        lp("OYU checking...\n");
        M5.Lcd.setTextSize(1);
        lp("count="+String(detect_cnt)+"\n");
        break;
      case 2:
        lp("OYU Waita!");
        break;
      case 3:
        lp("Shutdown...");
        break;
    }
    // バッテリー残量警告
    if (!battnotified && battery < BATT_LOW_ALERM) {
      battnotified = true;
      send_line("%0D%0Aバッテリー残量が低下してます "+String(battery)+"%25");  // LINEに通知する
    }
  }

  // 電源オフ
  if (mode == 3) {
    WiFi.disconnect();
    delay(2000);
    M5.Axp.PowerOff();  // 電源オフ
  }

  // サイドボタンでモード戻す
  if(M5.BtnB.wasPressed()) {
    M5.Beep.beep();
    mode = 0;
    detect_cnt = 0;
    battnotified = false;
  }

  delay(10);
}

プログラムの説明

電源を入れるとWi-Fiに接続し、ボタンを押すとお湯はりの監視がスタートします。スタート時にLINEに通知します。お湯を検知するとLEDが光り、「ピピピッ」とあの給湯器のような音が鳴ります。お湯の検知が5秒間続くと、LINEにお湯が沸いたことを通知して、あの給湯器のお風呂が沸いたときの音楽を流します。

LINEへの通知

LINEのMessaging APIを使ってます。あらかじめLine Notifyのサービスに登録して、トークンを取得しておく必要があります。こちらのページを参考にさせていただきました。メッセージの送信はhttpでPOSTするだけです。今までもLINEへの通知はやってたのですが、今回なぜか送信できませんでした。
どうやらM5StickC Plusのライブラリ(?)のルート証明書が古いようで、sclient.setInsecure(); を付けて証明書の検証をしないようにしたら正常に接続できました。

バックグラウンドで音楽再生する

音楽の再生はBEEPで行います。通常はsetBeep()で音程を決めて、beep()とdelay()関数を使って決まった長さの音を再生すると思います。ですがdelay()を使うとその間に他の処理が止まってしまいます。そこで今回は、タイマー割込みを使って、メインルーチンとは別に処理する形にしました。
setup()内の ticker1.attach_ms(10, play_background); で、10msごとに play_background() を実行するように指定してます。play_background() では、指定時間が経過したら、楽譜データから次の音を選んで再生することで曲を再生します。

楽譜データ

今回使ったあの給湯器の音楽は簡単な曲なので、単純に周波数のデータを配列で並べるだけでもよかったのですが、今後BEEPで音楽を鳴らすときに便利なので、楽譜データを作りやすいようにしてみました。

楽譜データの例
const uint8_t music_data_doremi[] = { 4,DO,8, 4,RE,8, 4,MI,8, 4,FA,8, 0,0,0, };

3つの数値で1つの音になっていて、この例は「ドレミファ」になります。データの1つ目はオクターブ、2つ目は音階、3つ目は長さです。音階は数値ではなくDO RE MIのように書いてますが、これは#defileで定義しているだけです。半音上げたい場合は DO+1 のように書けばいいはずです(未確認)。長さは、たとえば4なら四分音符、8なら8分音符です。これならいちいち周波数を調べて書かなくても、そのまま楽譜から入力できますね。

周波数は以下のように計算しています
freq = round(440.0 * pow(2.0, oto/12.0) * pow(2.0, (oto < DO) ? oct-4 : oct-5));

バッテリー残量のチェック

M5StickC Plusのバッテリーはそれほど長くないので、お湯はりをしている途中でバッテリーが切れてしまい、気づいたらお湯が溢れていた…なんてことになりかねません。そこでバッテリー残量が少ない場合も、LINEで通知するようにしました。

バッテリー残量の計算
round(((M5.Axp.GetBatVoltage() – 3.0f) / (4.2f – 3.0f)) * 100);

この方法ではあまり正確には取得できませんが、おおまかな量を知ることができます。今回は20%を下回ったらLINEに通知するようにしました。こういった機能を実装するときは、1回通知したら2回目以降は通知しないという仕組みを入れることが重要です。それを忘れると、loop()の間でDOS ATTACKしてしまいますのでご注意を。

今後の予定

自動湯沸かし機能と、追い炊き機能が付いた給湯器に変えてほしい、と要望を出す。

LINEで送る
Pocket