HIROBIRO

HIROBIRO

金銭的、精神的自由を目指すブログ。

炊飯器の残量をメールで通知してくれる機能を作った

f:id:hirokun1735:20180430145747p:plain


以前Arduinoを用いて、炊飯器の蓋の開閉回数から米の残量を調べる残量カウンターを作成しました。
今回はさらにRaspberry Piと連携させ、米が少なくなったことをメールで教えてくれるシステムを追加しました。
この記事は概要の説明に留め、詳細な内容は次回以降の記事に書きます。

Raspberry Pi 3でメール送信

炊飯器の残量カウンターの製作については以下の記事でまとめてあるので参考にしてください。
www.hirobiro-life.com


この残量カウンターではスピーカーでメロディが流れるようになっています。
米残量が少なくなると蓋を開け閉めしたタイミングと1時間に1回音を鳴らして教えてくれます。
しかしご飯を食べる直前まで外に出かけていたりすると当然音が聞けないため、米が少ないことに気付けません。
そういうわけでメール送信は必須の機能なのです。


残量カウンターで用いているArduino Unoだけではメール送信は実現できません。
何らかの方法でネットに接続する必要があります。
そこでRaspberry Pi 3を利用することにしました。
Raspberry Pi 3(以下、ラズパイ3)には標準でWifiが搭載されているのです。


Raspberry Pi 3 MODEL B


全体のシステムを説明すると以下のようになります。


1. ラズパイ3でプログラムを起動しスリープ中のArduinoを外部割込みで起こす
2. ラズパイ3とArduinoをI2Cで通信し、Arduinoのメモリ内にある米残量のデータを取得
3. 米残量が残り2杯分以下ならSMTPでメール送信


実際に送るメールの内容はこちらになります。
残り何杯かという数字のみ変化します。


f:id:hirokun1735:20180430145747p:plain

システムの製作について

I2CやSMTPに関するプログラム作成は以下の本を参考にしました。
また私はラズパイを触るのが初めてだったため、初期設定なども参考にさせてもらいました。
多少ネットで探したこともありますが基本的な内容はこれ一冊で十分カバーされています。


Raspberry Pi クックブック 第2版 (Make:PROJECTS)


今回のシステムの配線図は以下のようになります。
実際にはArduinoには上からLCDシールドを装着しています。
S1は傾斜センサでこれを炊飯器の蓋部分に取りつけます。


f:id:hirokun1735:20180430143503p:plain


以下が実際に配線した様子です。
傾斜センサは炊飯器に装着できるよう、針金で吸盤にくっつけています。


f:id:hirokun1735:20180430155501j:plain


まずArduino側のプログラムですが、前回作ったプログラムを基本としています。
それにラズパイ3からの割込みを受け付けるコードやI2Cによる通信のコードを付け加えています。
少し長いですが全部のコードを書いておきます。

/*
Rice_counter Ver.2
1.残り米量「○○杯分」を設定
2.蓋の開閉ごとにカウントダウン
3.残り米量2杯分になると一定時間ごとに音を鳴らす

Ver.2にてメール送信機能追加
*/

#include <LiquidCrystal.h>
#include <LCDKeypad.h>
#include <avr/sleep.h>
#include <avr/wdt.h>
#include <Wire.h>

#define DEBUG 0
//4分音符=120 (duration=500)
#define BEAT 250   // 音の長さを指定 (8分音符)
#define SOUND 11   // 圧電スピーカを接続したピン番号(D11)
                   // D11しか反応しない?

//Pin assignments for SainSmart LCD Keypad Shield
LiquidCrystal lcd(8, 9, 4, 5, 6, 7); 
//---------------------------------------------

LCDKeypad keypad;

//attachInterruptで指定した関数内で変化する変数にはvolatileをつける
int localKey = -1;
volatile int mode = 1;
int before_mode = 1;
int first_rice = 5;   //米の量、初期設定 単位:合
volatile int open_flag = 0;  //蓋開け閉めのフラグ
int rice = 0;
int sensor = 2;   //センサ信号デジタル入力ピン
int en_input1 = 1; //ボタン入力受付
int en_input2 = 0; //センサ入力受付
int sleepStatus = 0;     // variable to store a request for sleep
int count = 0;   //sleep用カウンタ
volatile int sleep_flg=0;
volatile int counter;   //wdt用カウンタ

int count_max=15;   //4second * 15 = 60 second 
int wait_minutes=60;   //wait_time (minutes)

int SLAVE_ADDRESS = 0x04;
           
void setup() 
{ 
  lcd.begin(16, 2);
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Rice Counter");
  lcd.setCursor(0, 1);
  lcd.print("Start");
  delay(2500);

  rice = first_rice;

  Serial.begin(9600);   // 9600 bpsで通信
  Serial.println("Start");   

  pinMode(sensor, INPUT); //デジタルピン2をinputに設定
  pinMode(3, INPUT_PULLUP); //割込み用ピンD3をinputに設定

  counter=0;  //WDTカウンタ初期化

  Wire.begin(SLAVE_ADDRESS);
  Wire.onRequest(sendAnalogReading);
}

///////////////////////////////////////////////////////////////////////////////////////////////
//Main Loop
///////////////////////////////////////////////////////////////////////////////////////////////

void loop() 
{ 
  localKey = keypad.button();
  
  #if DEBUG
  //Serial.println(localKey);
  #endif
  
  if(mode == 1){
    //mode1:カウンタ
    #if DEBUG
    Serial.println("mode1");
    #endif
    
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Rice Counter");
    lcd.setCursor(0, 1);
    lcd.print(rice);

    if(en_input1 == 1){
      if(localKey == 1) {   //UPボタンで増やす
        rice += 1;
        en_input1 = 0;
        count = 0;
      }else if(localKey == 2) {   //DOWNボタンで減らす
        
        #if DEBUG
        Serial.println("down");
        #endif
        
        rice -= 1;
        en_input1 = 0;
        count = 0;
      }
    }else if(localKey == -1){   //UP,DOWNボタンから指を離すと次の入力受付
      en_input1 = 1;
    }

    //デジタル入力、傾斜センサonでLOW
    if(en_input2 == 1){
      if(digitalRead(sensor) == LOW){   
        
        #if DEBUG
        Serial.println("sensor_on");
        #endif
        
        rice -= 1;
        en_input2 = 0;
        count = 0;
        delay(1500);
        if(rice <= 2){  //残り2杯分になると蓋を開け閉めしたときに音を鳴らす
          speaker();
        }
      }
    }else if(digitalRead(sensor) == HIGH){  //傾きが戻るまでカウンタ停止
      en_input2 = 1;
    }

      if(rice == 0) {
        Serial.println("rice=0");
        mode = 2; //残り0で表示固定モードに移行
        count = 0;
      }

    count++;
    Serial.println(count);
    if(count >= 100){      //0.1s*300=30s経過&蓋が閉まっているとsleep
      count = 0;
      if(digitalRead(sensor) == HIGH){
        mode = 3;
      }
    }
    delay(100);
    
  }else if(mode == 2){
    //mode2:米の量が0
    #if DEBUG
    Serial.println("mode2");
    #endif
    
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Rice Empty");
    lcd.setCursor(0, 1);
    lcd.print(rice);
    delay(100);    
    before_mode = 2;

    count++;
    if(count >= 300){      //30s経過でsleep
      count = 0;
      mode = 3;
    }
    delay(100);
    
  }else if(mode == 3){    
      //mode3:スリープモード
      Serial.println("sleep start");
      delay(100);
      
      sleepNow();     // sleep function called here    
      if(open_flag == 1){  //蓋が空いたので米残量をマイナス1
        Serial.println("open_flag");
        if(rice != 0){
          rice -= 1;
        }
        en_input2 = 0;
        open_flag = 0;
      }
      if(rice <= 2){  //米残量2以下なら一定時間ごとに音を鳴らす
        speaker();
      }
  }
}

///////////////////////////////////////////////////////////////////////////////////////////////
//Sleep
///////////////////////////////////////////////////////////////////////////////////////////////

void sleepNow()         //スリープモードの中身
{
  wdt_set(); //watch dog timer set
  sleep_flg=1; //enable on sleep flag
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);   // sleep mode is set here
  ADCSRA &= ~(1 << ADEN); // ADC_OFF
  sleep_enable();          // enables the sleep bit in the mcucr register
                             // so sleep is possible. just a safety pin
  if(before_mode == 1){      //米残量0のときは傾斜センサで割込み入れない                     
    attachInterrupt(0,wakeUpNow1, LOW); // use interrupt 0 (pin 2) and run function
                                       // wakeUpNow when pin 2 gets FALLING
  }
  attachInterrupt(1,wakeUpNow2, FALLING); //PiのGPIOから割込み、米残量0でも発動
  while(sleep_flg){
    // BOD設定変更のため,BODSとBODSEに同時に1を出力.
    MCUCR |= (1 << BODSE)|(1 << BODS);
    // その後4クロック周期内にBODSに1,BODSEに0を出力してBODをOFFにする.
    MCUCR = (MCUCR & ~(1 << BODSE))|(1 << BODS);
    // その後3クロック周期内にスリープ状態へ移行.
    sleep_mode();            // here the device is actually put to sleep!!
                               // THE PROGRAM CONTINUES FROM HERE AFTER WAKING UP
  }
  sleep_disable();         // first thing after waking from sleep:
                             // disable sleep...
  ADCSRA |= (1 << ADEN);  // ADC_ON
  wdt_unset(); //watch dog timer unset
  detachInterrupt(0);      // disables interrupt 0 on pin 2 so the
                             // wakeUpNow code will not be executed
                             // during normal running time. 
  detachInterrupt(1);
}

void wakeUpNow1()        // 外部割込み後の復帰
{
  Serial.println("1Wakeup");
  //sleep end
  sleep_flg = 0;
  //counter reset
  counter = 0;
  open_flag = 1;  //蓋が空いたので米残量をマイナス1する
  mode = before_mode;   //前回のモードに復帰、何もなければ10s後にスリープ
}

void wakeUpNow2()        // 外部割込み後の復帰(PiのGPIO)
                         //米残量の変更は無し
{
  Serial.println("2Wakeup");
  //sleep end
  sleep_flg = 0;
  //counter reset
  counter = 0;
  mode = before_mode;   //前回のモードに復帰、何もしなければ10s後にまたスリープ
}

///////////////////////////////////////////////////////////////////////////////////////////////
//Watch Dog Timer
///////////////////////////////////////////////////////////////////////////////////////////////

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

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

//WDTによる復帰
ISR(WDT_vect)
{
  //wdt_reset();
  if(sleep_flg == 1)
  {
    counter++;
    Serial.println("wdt");  
    if( counter >= (count_max * wait_minutes))    
    //sleepさせたい時間をカウント
    {
      //sleep end
      sleep_flg = 0;
      //counter reset
      counter = 0;
      mode = before_mode;   //前回のモードに復帰、何もなければ10s後にスリープ
    }
  }
  else
  {
  }
}

///////////////////////////////////////////////////////////////////////////////////////////////
//Speaker
///////////////////////////////////////////////////////////////////////////////////////////////
void speaker()
{
     tone(SOUND,330,BEAT*1.5) ;  // ミ
     delay(BEAT*1.5) ;
     tone(SOUND,294,BEAT/2) ;  // レ
     delay(BEAT/2) ;
     tone(SOUND,330,BEAT) ;  // ミ
     delay(BEAT*2) ;
     tone(SOUND,294,BEAT*2) ;  // レ-
     delay(BEAT*4) ;
     tone(SOUND,330,BEAT*1.5) ;  // ミ
     delay(BEAT*1.5) ;
     tone(SOUND,294,BEAT/2) ;  // レ
     delay(BEAT/2) ;
     tone(SOUND,330,BEAT) ;  // ミ
     delay(BEAT*2) ;
     tone(SOUND,294,BEAT*2) ;  // レ-
     delay(BEAT*4) ;

     tone(SOUND,262,BEAT) ;  // ド
     delay(BEAT) ;
     tone(SOUND,262,BEAT) ;  // ド
     delay(BEAT) ;
     tone(SOUND,466,BEAT) ;  // シb
     delay(BEAT) ;
     tone(SOUND,466,BEAT) ;  // シb
     delay(BEAT) ;
     tone(SOUND,440,BEAT) ;  // ラ
     delay(BEAT) ;
     tone(SOUND,440,BEAT) ;  // ラ
     delay(BEAT) ;
     tone(SOUND,392,BEAT) ;  // ソ
     delay(BEAT) ;
     tone(SOUND,392,BEAT) ;  // ソ
     delay(BEAT) ;
     tone(SOUND,349,BEAT) ;  // ファ
     delay(BEAT) ;
     tone(SOUND,349,BEAT) ;  // ファ
     delay(BEAT) ;
     tone(SOUND,330,BEAT) ;  // ミ
     delay(BEAT) ;
     tone(SOUND,330,BEAT) ;  // ミ
     delay(BEAT) ;
     tone(SOUND,294,BEAT) ;  // レ
     delay(BEAT) ;
     tone(SOUND,294,BEAT) ;  // レ
     delay(BEAT) ;
     tone(SOUND,311,BEAT) ;  // レ#
     delay(BEAT) ;
     tone(SOUND,311,BEAT) ;  // レ#
     delay(BEAT) ;

     tone(SOUND,330,BEAT*1.5) ;  // ミ
     delay(BEAT*1.5) ;
     tone(SOUND,294,BEAT/2) ;  // レ
     delay(BEAT/2) ;
     tone(SOUND,330,BEAT) ;  // ミ
     delay(BEAT*2) ;
     tone(SOUND,294,BEAT*2) ;  // レ-
     delay(BEAT*4) ;
     tone(SOUND,330,BEAT*1.5) ;  // ミ
     delay(BEAT*1.5) ;
     tone(SOUND,294,BEAT/2) ;  // レ
     delay(BEAT/2) ;
     tone(SOUND,330,BEAT) ;  // ミ
     delay(BEAT*2) ;
     tone(SOUND,294,BEAT*2) ;  // レ-
     delay(BEAT*4) ;

     tone(SOUND,262,BEAT) ;  // ド
     delay(BEAT) ;
     tone(SOUND,262,BEAT) ;  // ド
     delay(BEAT) ;
     tone(SOUND,466,BEAT) ;  // シb
     delay(BEAT) ;
     tone(SOUND,466,BEAT) ;  // シb
     delay(BEAT) ;
     tone(SOUND,440,BEAT) ;  // ラ
     delay(BEAT) ;
     tone(SOUND,440,BEAT) ;  // ラ
     delay(BEAT) ;
     tone(SOUND,392,BEAT) ;  // ソ
     delay(BEAT) ;
     tone(SOUND,392,BEAT) ;  // ソ
     delay(BEAT) ;
     tone(SOUND,349,BEAT*3) ;  // ファ-
     delay(BEAT*3) ;

     delay(3000) ;           // 3秒後に繰り返す
}

///////////////////////////////////////////////////////////////////////////////////////////////
//I2C Communication
///////////////////////////////////////////////////////////////////////////////////////////////
void sendAnalogReading()
{
  Wire.write(rice);
}

次にラズパイ3側のプログラムです。
プログラムを起動するとGPIO18ピンを叩いてArduinoを起動し、起動後のタイミングを狙ってArduinoにI2C通信のリクエストを送信します。
その後Arduinoから米残量のデータを取得し、残量が2以下なら登録したアドレスにメールを送信します。

import RPi.GPIO as GPIO
import time
import smbus
import smtp

GPIO.setmode(GPIO.BCM)
GPIO.setup(18, GPIO.OUT)

# for RPI version 1, use "bus = smbus.SMBus(0)"
bus = smbus.SMBus(1)

# This must match in the Arduino Sketch
SLAVE_ADDRESS = 0x04

GPIO.output(18, False)
GPIO.cleanup()
time.sleep(0.5)

reading = int(bus.read_byte(SLAVE_ADDRESS))
print(reading)
if reading <= 2:
    print('send')
    title = 'オコメ.. ナイヨ..'
    text = 'オコメ.. タリナイヨ..\nノコリ.. '\
			 + str(reading) + 'ハイ ダヨ..'
    smtp.send_email('@gmail.com', title, text) #送信先のアドレスを入力

下記のプログラムはSMTP(Simple Mail Transfer Protocol)によってメールを送信するプログラムです。
上のプログラムのsmtp.send_emaiはこのプログラム内に書いています。
gmailを使ってメールを送信しており、アドレスやパスワードを入力する必要があります。

#smtp.py

import sys
sys.path.append("/home/users/mymodule")
import smtplib_utf8

GMAIL_USER = '@gmail.com' #送信元アドレス入力
GMAIL_PASS = '' gmailパスワード入力
SMTP_SERVER = 'smtp.gmail.com'
SMTP_PORT = 587

def send_email(recipient, subject, text):
    smtpserver = smtplib_utf8.SMTP(SMTP_SERVER, SMTP_PORT)
    smtpserver.ehlo()
    smtpserver.starttls()
    smtpserver.ehlo
    smtpserver.login(GMAIL_USER, GMAIL_PASS)
    header = 'To:' + recipient + '\n' + 'From: ' + GMAIL_USER
    header = header + '\n' + 'Subject:' + subject + '\n'
    msg = header + '\n' + text + '\n\n'
    smtpserver.sendmail(GMAIL_USER, recipient, msg)
    smtpserver.close()

まとめ

前回作った炊飯器の残量カウンターにメール送信機能を追加しました。
ラズパイ3と組み合わせ、SMTPでメールを送信します。
今回のシステムにおいての詳細は次回以降の記事にまとめます。