[Arduino] お風呂の水温ブザー

#[背景]
今住んでいるアパートのプロパンガス料金が高く、オール電化のために電気で風呂水を加熱できるヒーター([風呂バンス](https://amzn.to/36REeGy "Amazonにジャンプ"))を買った。
![furobance.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530325/51e06613-d7a1-16b7-80a9-83ba16e22ecb.jpeg)


#[問題・課題]

* この風呂バンスには、適温になったら作動するサーモスタットはあるものの、**<font color="red">お知らせする機能がなく、いつ風呂が沸いたのか分からない。</font>**そのため、湯温確認のため、たびたび浴室に行く必要があり面倒。

* このヒーターは消費電力が1000Wもあり、湯が沸いた後に**<font color="red">うっかり長時間放置してしまうと、電気代がムダになる。</font>**

#[対策方針]
風呂の湯が設定した温度になると、ブザーが鳴ってお知らせしてくれる湯温センサーを作る。

#[方法]

* Arduinoに、LCD、NTCセンサー、ブザーを接続。

* 水場近くで100Vを使いたくないので、電源にはモバイルバッテリーを使用する。

* モバイルバッテリーの消耗を防ぐため、水温計測時以外は、Arduinoを**スリープモード**で眠らせる。

#[材料]
各部品の詳細を見たい人は、以下の文字リンクからAmazonへ飛べます。

* [Arduino Nano (中華互換品)](https://amzn.to/2oXOXxM "Amazonへジャンプ")

* [NTCサーミスタ](https://amzn.to/34JznoH "Amazonへジャンプ") (2線仕様DS18B20、湯温測定用)

* [LCD(I2C 1602 Arduino用ブルーバックライトLCD](https://amzn.to/2qBGFfw "Amazonへジャンプ") (湯温表示用)

* [タクトスイッチ](https://amzn.to/2NvE1Rp "Amazonへジャンプ") (スリープ時からピンウェイクアップ用)

* [ピエゾブザー(5V)](https://amzn.to/2K7DYtc "Amazonへジャンプ")

* <del>DAISO製</del>Δ1.モバイルバッテリー(英語圏では、PowerBankと呼ぶらしい)

* その他、配線、100均ケース、消しゴムなど。

#[外観]
![indicate_temperature(noted).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530325/a1761c68-4581-c6a2-c183-2d679ddf5ed7.jpeg "全体図")
![arduino_closeup.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530325/313d7486-3fdd-9617-981b-4ac5d28fed50.jpeg "Arduino周辺拡大図")
![backside.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/530325/3a59328c-f3b9-be58-7fc2-cc2b6d161880.jpeg "背面図")

#[システム作動概要]

* スリープ状態から復帰するためのインタラプトは、**ISR(WDT_vect)**(ウォッチドッグタイマー)と**pinwakeup()**(タクトスイッチからのRISE入力)の2つを使っています。

* タクトスイッチが押されなければ、約6分毎にウェイクアップして湯温を測定して再びスリープし、設定温度を上回った時点でブザーが鳴ります。

* 湯温を確認したいときは、タクトスイッチ(D2ピンに接続)を押せば、ウェイクアップして湯温を表示します。(その後、再度スリープに入る)

* Arduino Nanoの上にある、しらたま団子みたいなものは、長年受験戦争を共に戦ってきてカドの取れてしまった消しゴムです。(笑)
USBケーブルを差し込むときに、Arduinoが逃げてしまうので、ホットボンドと一緒に突っ張ってもらってます。

#[使ってみた感想]

* 使う日により、**ブザーが鳴る日もあれば、なぜかならない日もあった**ので、何度か現場に立ち会って原因解析をしたところ、以下のことが把握できた。

* 浴槽内に垂らしている**センサーの位置バラツキ**(水面に近いほど湯温は高く、低いと湯温も低い。)

* **ブザー発報温度設定のマージン不足**(湯温の推移を確認してみると、湯温が設定温度に近づくと、風呂バンスのサーモスタットで湯温が頻繁に上下するようになるため、上限温度にブザートリガーを設定すると、Arduinoのウェイクアップのタイミングによっては、一時的に設定湯温以下になっているため、ブザーが鳴らず再度スリープしてしまう。)

#[修正履歴]

**Δ1.**　今回のシステムだと消費電力が少なすぎるためか、DAISO製バッテリーを使うと、自動的に送電OFFされてしまったので、他社製に変更。
**Δ2.**　スリープ時にも点灯したままだったLCDを、省エネのため消灯するよう変更。
**Δ3.**　delay()を多用していたので、大部分をmillis()に置き換えてプログラムのフリーズ時間を減らした。
**Δ4.**　省エネ目的で、作動クロック数も落としたが、millis()を使った処理に影響が出てしまったため、クロック数は元に戻した。
**Δ5.**　ブザーの周波数を3kHzに変更。人間の聴覚特性研究によれば、このあたりが聞こえやすいらしい。
**Δ6.**　一旦、ブザーが鳴り始めるとうるさいので、ミュート機能を追加。
**Δ7.**　ミュートモードになっているのか、プログラムに不具合があってブザーが鳴らないのか識別できないので、ミュート作動時には内臓LEDを点灯させるよう変更。
**Δ8.**　一瞬ブザーが鳴っても、風呂バンスのサーモスタット介入により水温が数℃下がり、ブザーが止まってしまうことがあったので、一度設定湯温になったら、湯温が下がってもブザーが鳴り続けるようコードを変更。
**Δ9.**　参考データ用として、プログラムの開始時に、沸かし始めの水温を取得するよう変更。


#[今後の課題]

* 浴槽内の**水温センサー設置高さの安定化**→浴槽の排水用チェーンに目印をつけ、それをセンサーを設置する高さの目安とする。


* 風呂を沸かしているときは、キッチンに湿気が入ってくるので風呂場のドアを閉めたいが、Arduinoを浴室内には置きたくないためキッチンに置いている。そうすると、センサーのコードを浴室のドアに挟んだまま閉めているので、**センサー断線および自分が足をひっかけて転倒する恐れ**がある。

* タクトスイッチによるピンウェイクアップの場合に、一度だけでなく**何度も連続してウェイクアップしてしまう**ことがある。(スイッチのバウンシングが原因か?)

#[今後の展望]

* Arduinoの防水パッケージ化して浴室内に設置できるようにする。

* **センサー部を分離し、WiFi or Bluetoothで無線化**し、浴室内に設置。
(ブザーは浴室外にないと、音が聞こえづらい。**理想はスマホへの通知化**か。)

* **ISR(Interrupt Service Routine)内ではmillis()が更新されないらしい**ので、タクトスイッチによるウェイクアップ時にどんなデバウンス手法があるのか、調査が必要。

コードを少しだけいじれば、熱帯魚水槽の水温、車のエンジン油温、冷蔵庫のドア開放、エアコンの効きすぎ防止、CPUの温度アラームなどにも使えるかもしれません。

#[コード]
初めてQiita形式でコードを掲載してみる。初体験はいつもドキドキするけど、昨日の自分より1%賢くなった。(markdown活用のご指摘、ありがとうございます！)

```cpp:bathtub-temp-buzzer.rb

#include <avr/wdt.h> 
#include <avr/sleep.h>
#include <LiquidCrystal_I2C.h>

// define I2C address(change if needed)
const int i2c_addr = 0x3F;
// define LCD pinout for PCF8574
const int en = 2, rw = 1, rs = 0, d4 = 4, d5 = 5, d6 = 6, d7 = 7, bl = 3;
LiquidCrystal_I2C lcd(i2c_addr, en, rw, rs, d4, d5, d6, d7, bl, POSITIVE);   // Default I2C Slave Address of "PCF8574". Connect Vcc->5v, GND->GND, SDA->A4, SCL->A5 respectively.
// analog pin connected to thermistor
#define THERMISTORPIN 7
// pin connected to Internal LED.(For blink)
#define LEDPIN 13
// pin connected to Piezo Buzzer. The pin should support PWM.
#define PIEZOPIN 3
// pin connected to tactile switch for buzzer toggle
#define SWITCHPIN 2
// pin connected to tactile switch for interrupt. it should be 2 or 3
#define INTERRUPTPIN 2
// resistance at 25 degrees Celsius
#define THERMISTORNOMINAL 10000
// temp. for nominal resistance (almost at 25 C)
#define TEMPERATURENOMINAL 25
// how many samples to take and average, more takes longer but is more 'smooth'
#define NUMSAMPLES 5
// The beta coefficient of the thermistor (usually 3000-4000)
#define BCOEFFICIENT 3950
// the value of the 'other' resistor (in ohm)
#define SERIESRESISTOR 10000
// set beep duration (in milli-sec)
#define BEEPDURATION 2000
// set delay duration after beep (in milli-sec)
#define BEEPDELAY 3000
// set waiting time after sw toggle (in milli-sec)
#define TOGGLEWAIT 1000
// set Hz when beep
#define BEEPHZ 3000
// set temperature where alarm fires (in celsius) default:38.6
#define ALARMTEMP 40.0
// temp difference to complete initial sensor acclimation (in celsius)
#define tempDiff 0.05
// sampling interval for initial sensor acclimation (in milli-sec)
#define waitAccli 3000
// define number of led blink before sleep
#define LEDBLINKNUM 5
// define value for temperature compensation
#define TEMPCOMP 2
// wait_time (minutes)
#define waitMinutes 6
// 4second*15=60second
#define countMax 15


uint8_t j = 0;                // counter for led blink before sleep 
uint16_t samples[NUMSAMPLES]; // measured voltage value divided by 1024 (Range from 0 to 1023)

unsigned long bootTime = 0;       // time duration from arduino booted (msec)
unsigned long oldBeepmillis = 0; // previous timestamp for beep toggle
unsigned long newBeepmillis = 0; // current timestamp for beep toggle
unsigned long oldMutemillis = 0; // previous timestamp for mute toggle
unsigned long newMutemillis = 0; // current timestamp for mute toggle
unsigned long oldLedmillis = 0; // previous timestamp for led toggle
unsigned long newLedmillis = 0; // current timestamp for led toggle

const unsigned long beepInterval = 2000; // interval between beeps
const unsigned long ledInterval = 500;   // interval between led blink
const unsigned long muteInterval = 200;  // interval between mute toggle

boolean beepMute = 0;        // state of beeper, 0:non-mute, 1:mute
boolean beepState = 0;        // state of beep  0:no-beep, 1:beep
boolean ledState = 1;         // state of led  0:off, 1:on
boolean sleepFlag = 0;  // 0:awake, 1:sleep
boolean tempFlag = 0;  // 0: not reach target temperature, 1: reached target temperature

float tempVar = 0;          // variable for temperature
float tempOld = 0;           // temperature for first measurement
float tempNew = 0;           // temperature for second measurement

volatile uint8_t sleepCounter = 0;  // counter for sleep
volatile boolean backLt = 0;        // state of backlight 0:off, 1:on

//arduino pinwakeup interrupt
void pinwakeup()
{
  //wdt_reset(); then end sleeping
  sleepFlag = 0;
  //counter reset
  sleepCounter = 0;
  //toggle backlight state
  backLt = 1; 
}

// watchdog timer setup
void wdt_set()
{
 wdt_reset();
 cli();
 MCUSR = 0;
 WDTCSR |= 0b00011000;            //WDCE WDE set
 WDTCSR =  0b01000000 | 0b100000; //WDIF set scale 4 seconds
 sei();
}

// watchdog timer unset
void wdt_unset()
{
  wdt_reset();
  cli();
  MCUSR = 0;
  WDTCSR |= 0b00011000;   //WDCE WDE set
  WDTCSR =  0b00000000;   //status clear
  sei();
}

// watchdog timer call
ISR(WDT_vect)
{
  //wdt_reset();
  if(sleepFlag == 1)
  {
    sleepCounter++;
    if(sleepCounter >= (countMax * waitMinutes))
    {
      // sleep end
      sleepFlag = 0;
      // counter reset
      sleepCounter = 0;
    }
  }
}

// status reset
void init_status()
{
  sleepCounter = 0;
}

// sleep arduino
void sleep()
{
  wdt_set(); // watchdog timer set
  sleepFlag = 1; // enable on sleep flag
  set_sleep_mode(SLEEP_MODE_PWR_DOWN); // set sleep mode
  attachInterrupt(digitalPinToInterrupt(INTERRUPTPIN), pinwakeup, RISING); // set level interrupt, 0:Pin2 1:Pin3 on Arduino
  while(sleepFlag)
  {
    noInterrupts();  // cli;
    sleep_bod_disable();  // turn off BOD (pwr consumption decrease 27 to 6.5uA)
    sleep_enable();
    interrupts();   // sei();
    sleep_cpu();    // let cpu sleep
    sleep_disable();
  }
  detachInterrupt(digitalPinToInterrupt(INTERRUPTPIN)); // unset level interrupt
  wdt_unset();        // watchdog timer unset
}

// get temperature
void get_temp()
  {
    uint8_t i;
  // take N samples in a row, with a slight delay
  for (i = 0; i < NUMSAMPLES; i++)
  {
    // compensate voltage of 5V on arduino
    samples[i] = analogRead(THERMISTORPIN); //+v_offset/0.0049;
    delay(100);
  }

  // average out all the samples
  float sampAverage = 0;
  for (i = 0; i < NUMSAMPLES; i++)
  {
    sampAverage += samples[i];
  }
  sampAverage /= NUMSAMPLES;
  Serial.print("Average analog reading: ");
  Serial.println(sampAverage);

  // convert the average value to resistance
  sampAverage = 1023 / sampAverage - 1;
  sampAverage = SERIESRESISTOR * sampAverage;
  
  Serial.print("Thermistor resistance: ");
  Serial.println(sampAverage);
 
  tempVar = sampAverage / THERMISTORNOMINAL;     // (R/Ro)
  tempVar = log(tempVar);                  // ln(R/Ro)
  tempVar /= BCOEFFICIENT;                   // 1/B * ln(R/Ro)
  tempVar += 1.0 / (TEMPERATURENOMINAL + 273.15); // + (1/To)
  tempVar = 1.0 / tempVar;                 // Invert
  tempVar -= 273.15;                         // convert to C
  tempVar += TEMPCOMP;                      // compensate the temperature to actual temp
  }

void setup()
{
  Serial.begin(9600);
  pinMode(LEDPIN, OUTPUT);       // set LED pin as output
  pinMode(SWITCHPIN, INPUT);     // set SW pin as input from buzzer toggle
  pinMode(INTERRUPTPIN, INPUT);  // set InterruptPin for wakeup
  lcd.begin(16,2);
  lcd.backlight();
  lcd.display();
  Serial.println("Adjusting Sensor");
  Serial.print("Please Wait...");
  lcd.setCursor(0, 0);
  lcd.print("Adjusting Sensor");
  lcd.setCursor(0, 1);
  lcd.print("Please Wait...");
  bootTime = millis();
  get_temp();
  tempOld = tempVar;
  delay(waitAccli);
  get_temp();
  tempNew = tempVar;
  while(abs(tempNew) - abs(tempOld) > tempDiff) //repeat til temp becomes constant
    {
    tempOld = tempNew;
    delay(waitAccli);
    get_temp();
    tempNew = tempVar;
    }
  lcd.clear();

}

void loop()
{
  get_temp();
  Serial.print("Temperature: ");
  Serial.print(tempVar);
  Serial.println(" *C");

  Serial.print("Start Temp.: ");
  Serial.print(tempNew);
  Serial.println(" *C");
  
  Serial.print("Boot time: ");
  Serial.print(bootTime/1000);
  Serial.println(" Sec.");
  
  if (backLt == 1)
  {
  lcd.backlight();
  lcd.display();
  }
  lcd.setCursor(0, 0);
  lcd.print("Temp: ");
  lcd.print(tempVar);
  lcd.print(" ");
  lcd.print((char)223);
  lcd.print("C");
  lcd.setCursor(0, 1);
  lcd.print("Start: ");
  lcd.print(tempNew);
  lcd.print(" ");
  lcd.print((char)223);
  lcd.print("C");

  if (tempVar < ALARMTEMP && !tempFlag)
  {
    // blink LED before going to sleep
    newLedmillis = millis();
    if ((newLedmillis - oldLedmillis >= ledInterval) && ( j < LEDBLINKNUM * 2))
    {
    digitalWrite(LEDPIN, ledState);
    ledState = !ledState;
    oldLedmillis = newLedmillis;  // update timestamp
    j++;
    }

    if (j >= LEDBLINKNUM * 2){
    j = 0;  // reset led blink counter
    backLt = 0;
    lcd.noDisplay();     // turn off display
    lcd.noBacklight();   // turn off back light
    sleep();
    }
  }

  if (tempVar >= ALARMTEMP)
  {
  tempFlag = 1; //reached target temperature
  backLt = 1;  // toggle backlight state
  }
  
  if (tempFlag && !beepMute)
  {
  newBeepmillis = millis(); // update timestamp
   if(newBeepmillis - oldBeepmillis >= beepInterval)
   {
     if (beepState){
       noTone(PIEZOPIN);  // stop beeping
       beepState = !beepState;
     }
     else{
       tone(PIEZOPIN, BEEPHZ);  //start beeping
       beepState = !beepState;
     }
    oldBeepmillis = newBeepmillis; // update timestamp
    }
  }
    
  if (tempFlag && digitalRead(SWITCHPIN))
  {
      newMutemillis = millis();
      if(newMutemillis - oldMutemillis >= muteInterval)
      {
        noTone(PIEZOPIN);  // stop if beeping
        beepMute = !beepMute;  //toggle mute flag  0:mute-off, 1:mute
        digitalWrite(LEDPIN, beepMute); // toggle led according to mute flag  0:led-off, 1:led-on
        oldMutemillis = newMutemillis;  // update timestamp
      }
  }
}
```