ラズパイその他工作

ラズパイを使ったコーヒーメーカー制御装置の製作

第3回:Pythonによる制御ソフトウェア

ラズパイを使ってコーヒーメーカーの制御装置を制作する本連載。紹介してくれるのは、これまでもさまざまなメディアで自作派向けの電子工作記事を執筆されてきた、阿矢谷充さんです。連載の第1回では装置の概要を、第2回ではハードウェアについて見て来ましたが、第3回では制御ソフトウェアについて紹介していきたいと思います。

coffee-maker-with-raspberry-pi-03-01

 

目次

  1. 環境設定
  2. Pythonの開発・動作環境
  3. I2C液晶の動作確認モジュール
  4. コーヒーメーカー制御メインプログラム(全体構造)
  5. Pythonによる制御プログラム

 

1. 環境設定

最初に動作環境について説明します。OSはラズパイ標準のRaspbianで、Release10:busterを使用しています。ここではそれらのインストール方法には触れません。

まず必要な設定について解説します。メニューから「Raspberry piの設定」画面で「インターフェース」のタブを表示します。

coffee-maker-with-raspberry-pi-03-02

 

ここで「有効」の設定が必須な項目は、SSH、VNC、I2Cです。SPIは今回の装置では使用していませんがI2Cと同様によく使う外部インターフェースなので有効にしています。シリアルポートとシリアルコンソールも使用しませんが、Wi-Fiなどが不具合を起こした時に設定に使えるので有効にしています。

SSH/VNCですが、本機は一度ケースに入れてしまうとHDMIなどの外部ポートにアクセスできません。そのため設定を全てリモートで行うためにこれらが必須になります。VNCはPCやMac側に「VNC Viewer」等のクライアントソフトが必要なのでインストールをおこなってください。VNCによるリモート動作には、もちろんWi-FiによるIP接続が必要になります。

 

2. Pythonの開発・動作環境

制御ソフトウェアにはPythonを使用します。GPIOにアクセスするライブラリには、最も一般的なRPi.GPIOを選択しました。このRPi.GPIOの基本については、非常にわかりやすい紹介記事「ラズパイとPythonの相性はバッチリ!ライブラリRPi.GPIOをマスターしよう」がこのデバイスプラスの過去記事として掲載されています。

ラズパイ(Raspberry Pi)とPythonの相性はバッチリ!ライブラリRPi.GPIOをマスターしよう

RPi.GPIOに初めて触れられる方は、こちらを事前に参照されることをぜひオススメします。

ラズパイ向けにPythonでの開発をおこなう場合、いろいろなやり方が考えられます。ソースコードを書く場合もラズパイ側でおこなうか、PC/Mac上の使い慣れたエディターでおこなうかなどさまざまです。

今回はラズパイ上で動作する”Thonny”というソフトウェアを使用してみました。Thonnyはエディターと実行環境、デバック環境をまとめたIDE(統合開発環境)です。Raspbianでは最初からインストールされているので、メニューから呼び出してすぐに使用できます。ThonnyのGUIを以下に示します。

coffee-maker-with-raspberry-pi-03-03

 

まずエディターですが、ThonnyはPython専用で、重要なインデントは自動で挿入されるので安心です。あと予約語は色分けされ、CTRL+Spaceでオートコンプリート(予約語の候補が表示される)がサポートされているのでとても便利です。

そしてデバッカーも具備しており、ステップ実行や変数の値の表示もおこなえます。Thonnyはビギナー向けに機能を必要最小限のものに限定しているので、使い方も簡単で、小規模〜中規模くらいのプログラムならこれで十分カバーできると思われます。

私は、このラズパイ上のThonnyにVNCで接続し、プログラム編集と実行をおこなっています。VNCで接続すればRaspbianのGUI画面が仮想化されているだけなので、切断しても実行しているタスクや環境はそのまま維持されています。本機のPython制御プログラムもThonnyで実行してそのまま使用しています。

ラズパイのVNC接続については、下記のサイトに詳しい設定方法が掲載されていますのでご参照ください。

VNCでRaspberry Piにリモートデスクトップ接続 (Windows/Mac/Linux対応)

 

3. I2C液晶の動作確認モジュール

前回、ハードウェアについて解説しましたが、初めにI2C液晶の動作確認をおこないます。テストはコマンドとPythonで作製したプログラムでおこないます。また、このテストプログラムはモジュール化されているので、コーヒーメーカー制御本体のプログラムでインポートして利用できるようになっています。

初めにI2Cの接続が問題なくおこなえているかをラズパイのコマンドで確認します。Raspbian上のLXTerminalやSSH接続されたターミナル上のシェルで「i2cdetect -y 1」コマンドを入力します。

pi@raspberrypi:~ $ i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- 3e -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --

I2Cの接続が正常なら、液晶モジュールのアドレス:3eが表示されます。表示されない場合は配線ミスなどの何か問題があるので確認が必要です。

次に、Thonnyで以下の動作確認モジュールを作製します。当方は、/home/piの下にpythonというディレクトリーを作ってプログラムの保存用に使用していますが、これは任意でかまいません。プログラム名は「lcdtest_2x8.py」です。

import smbus
import time

i2c = smbus.SMBus(1)
addr = 0x3e

def initlcd():
    data = [0x38, 0x39, 0x14, 0x78, 0x5f, 0x6a, 0x0c, 0x01, 0x06]
    i2c.write_i2c_block_data(addr, 0, data)
    time.sleep(0.3)

def write_data_to_lcd(upper,lower):
    #write upper
    i2c.write_byte_data(addr, 0, 0x80)
    data1 =["" for j in range(len(upper))]
    for i in range(len(upper)):
        data1[i] = ord(upper[i])
    i2c.write_byte_data(addr, 0, 0x80)
    i2c.write_i2c_block_data(addr, 0x40, data1)
    #write lower
    data2 =["" for j in range(len(lower))]
    for i in range(len(lower)):
        data2[i] = ord(lower[i])
    i2c.write_byte_data(addr, 0, 0xc0)
    i2c.write_i2c_block_data(addr, 0x40, data2)

def clsc():
    i2c.write_byte_data(addr, 0, 0x01)
    time.sleep(0.3)

if __name__ == '__main__':
    initlcd()
    write_data_to_lcd("line1","line2")
    time.sleep(2)
    clsc()
    write_data_to_lcd("--LCD---","**Test**")
    time.sleep(2)
    clsc()
    write_data_to_lcd("Test","==end===")

実行させて正常に動作すると、液晶画面上に3つのテストメッセージが表示されます。このプログラムはpythonのI2C制御用「smbus」と時間用「time」の2つのモジュールをインポートして使用しています。また以下の3つの関数を設定しています。

initlcd()は液晶モジュールの初期化をおこないます。プログラムの最初に一度実行します。モジュールで使用されているAQM0802Aというチップの制御コマンドを送っています。

write_data_to_lcd(upper,lower)は液晶モジュールに文字列を書き込みます。このプログラムは8文字×2行の液晶用で、引数のupperは上、lowerは下の行に表示します。文字列はord()関数を使ってアスキーコードに変換してモジュールに送信します。

clsc()は液晶表示をクリアします。別のプログラムでモジュールとしてインポートすると、これらの関数を参照して利用できます。

 

4. コーヒーメーカー制御メインプログラム(全体構造)

初めに制御プログラムの全体構造を下図に示します。

coffee-maker-with-raspberry-pi-03-04

 

左側のブルーの部分が最初に一度だけ実行される初期セットアップのブロックです。グリーンの部分は、カップ数を決める割り込みルーチン関係のブロックです。前回説明したハードウェアの黒色の押しボタンタクトスイッチで、抽出するコーヒーのカップ数を決定します。スイッチを押すたびに割り込み処理でカップ数が変更されます。

右側のオレンジ色の部分がメインルーチンです。まずスタートボタン(赤色のタクトスイッチ)の入力待ちがあり、以降1st―2nd―3rdの3段階のステップでタイマーループが構成されています。

1st Stepは「初期投入」です。2nd Stepの「蒸らし」に入る前に、コーヒー粉にお湯を行き渡らせます。ここで設定した時間だけコーヒーメーカーがON(通電状態)になります。
2nd Stepは「蒸らし」です。コーヒー粉にお湯が行き渡ったのちにそのまま一定時間保持します。ここではコーヒーメーカーはOFF(非通電状態)になります。
3rd Stepは「抽出」です。ここでは基本はコーヒーメーカーがON(通電状態)になります。付加機能としてPWMによるON/OFF制御を加えています。これはコーヒーメーカーがONになる時間(秒)とOFFになる時間(秒)をそれぞれ設定します。この機能によって所謂「リズム抽出」と呼ばれている方法(お湯を一定量入れて止めるを繰り返す)をソフトウェアで実現しようという試みです。PWM機能の詳細についてはpythonのプログラムリストの方で解説いたします。

以上の設定パラメーターをまとめると以下の表1のようになります。

coffee-maker-with-raspberry-pi-03-05

表1 設定パラメーター一覧表

 

設定時間は(秒)です。今回使用しているコーヒーメーカーのカリタET-102は最大杯数が5です。このソフトウェアは6杯まで設定できるようにしていますが、そこは蒸らしをゼロにして速くコーヒーを淹れられるような「最速モード」にパラメーターを設定して利用しています。

現状パラメータをこの表のイメージでパソコンやスマホのWebブラウザーから設定できるようにしていますが、その機能は連載の次回・第4回で解説いたします。

 

5. Pythonによる制御プログラム

前の章で解説した機能をラズパイのPythonで記述したプログラムについて説明します。
プログラムリストを以下に示します。

# -*- coding: utf-8 -*-
#コーヒーメーカー制御プログラム rev 1.0

#ライブラリのインポート
import RPi.GPIO as GPIO
import time
import lcdtest_2x8

#変数の設定
BUTTON1 = 24
BUTTON2 = 25
START_STOP_LED = 23
AC_OUT_LED = 17
SSR = 18
CUPIDX = 1

#Parameters:初期投入時間、蒸らし時間、抽出時間、ON時間、OFF時間
wtimer = [[60,40,360,60,10],[68,50,480,60,20],[70,50,600,60,20],[74,50,600,60,20],[0,0,600,60,0]]

#GPIO Setup
GPIO.setmode(GPIO.BCM)
GPIO.setup(BUTTON1,GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.setup(BUTTON2,GPIO.IN, pull_up_down=GPIO.PUD_DOWN)

GPIO.setup(START_STOP_LED,GPIO.OUT,initial=GPIO.LOW)
GPIO.setup(AC_OUT_LED,GPIO.OUT,initial=GPIO.LOW)
GPIO.setup(SSR,GPIO.OUT)
ssr_pwm=GPIO.PWM(SSR,1)
ssr_pwm.start(0)

#杯数設定割り込みルーチン
def button_pushed1(BUTTON1):
    GPIO.remove_event_detect(BUTTON1)
    global CUPIDX
    if CUPIDX == 1:
        print("Pressed!! Cups=3")
        lcdtest_2x8.write_data_to_lcd("Cups=3","")
        CUPIDX = CUPIDX+1
    elif CUPIDX == 2:
        print("Pressed!! Cups=4")
        lcdtest_2x8.write_data_to_lcd("Cups=4","")
        CUPIDX = CUPIDX+1
    elif CUPIDX == 3:
        print("Pressed!! Cups=5")
        lcdtest_2x8.write_data_to_lcd("Cups=5","")
        CUPIDX = CUPIDX+1
    elif CUPIDX == 4:
        print("Pressed!! Cups=6")
        lcdtest_2x8.write_data_to_lcd("Cups=6","")
        CUPIDX = CUPIDX+1
    elif CUPIDX == 5:
        print("Pressed!! Cups=2")
        lcdtest_2x8.write_data_to_lcd("Cups=2","")
        CUPIDX = 1
    time.sleep(0.3)
    GPIO.add_event_detect(BUTTON1, GPIO.RISING, callback=button_pushed1, bouncetime=200)

#初期設定
GPIO.add_event_detect(BUTTON1, GPIO.RISING, callback=button_pushed1, bouncetime=200)
lcdtest_2x8.initlcd()
lcdtest_2x8.write_data_to_lcd("Coffee","Timer")
time.sleep(2)
lcdtest_2x8.clsc()
lcdtest_2x8.write_data_to_lcd("Cups=2","")

#メインループ
try:
    while True:
        print("Waiting for rising edge on BUTTON2")
        GPIO.wait_for_edge(BUTTON2, GPIO.RISING)
        print("Rising edge detected on BUTTON2.")
        GPIO.remove_event_detect(BUTTON1)
        #Set Freqency and DutyCycle
        freq_int = 1/(wtimer[CUPIDX-1][3] + wtimer[CUPIDX-1][4])
        duty = wtimer[CUPIDX-1][3]/(wtimer[CUPIDX-1][3] + wtimer[CUPIDX-1][4])*100
        print("Frequency(Hz) =",freq_int)
        print("Duty cycle(%) =",duty)

        #1st step
        current_time = time.time()
        stop_time = current_time + wtimer[CUPIDX-1][0]
        ssr_pwm.ChangeDutyCycle(100)
        GPIO.output(AC_OUT_LED,1)
        ledflag = True
        while time.time() < stop_time:
            ctime = int(stop_time - time.time())
            print(ctime)
            GPIO.output(START_STOP_LED,ledflag)
            lcdtest_2x8.write_data_to_lcd("","1st:"+str(ctime)+"   ")
            ledflag = not ledflag
            time.sleep(1)

        print("Timer1 out")
        print('Time: ' + str(time.time() - current_time))

        #2nd step: Murashi
        current_time = time.time()
        stop_time = current_time + wtimer[CUPIDX-1][1]
        ssr_pwm.ChangeDutyCycle(0)
        GPIO.output(AC_OUT_LED,0)
        ledflag = True
        while time.time() < stop_time:
            ctime = int(stop_time - time.time())
            print(ctime)
            GPIO.output(START_STOP_LED,ledflag)
            lcdtest_2x8.write_data_to_lcd("","2nd:"+str(ctime)+"   ")
            ledflag = not ledflag
            time.sleep(1)

        print("Timer2 out")
        print('Time: ' + str(time.time() - current_time))

        #3rd step:
        current_time = time.time()
        stop_time = current_time + wtimer[CUPIDX-1][2]
        ssr_pwm.ChangeFrequency(freq_int)
        ssr_pwm.ChangeDutyCycle(duty)
        GPIO.output(AC_OUT_LED,1)
        ledflag = True
        while time.time() < stop_time:
            ctime = int(stop_time - time.time())
            print(ctime)
            GPIO.output(START_STOP_LED,ledflag)
            lcdtest_2x8.write_data_to_lcd("","3rd:"+str(ctime)+"   ")
            ledflag = not ledflag
            time.sleep(1)

        ssr_pwm.ChangeFrequency(1)
        ssr_pwm.ChangeDutyCycle(0)
        GPIO.output(AC_OUT_LED,0)
        GPIO.output(START_STOP_LED,False)
        print("Timer3 out")
        print('Time: ' + str(time.time() - current_time))
        lcdtest_2x8.write_data_to_lcd("","Stop    ")

        GPIO.add_event_detect(BUTTON1, GPIO.RISING, callback=button_pushed1, bouncetime=200)

except KeyboardInterrupt:
    GPIO.remove_event_detect(BUTTON1)
    ssr_pwm.stop()
    GPIO.cleanup()       # clean up GPIO on CTRL+C exit
    print("STOP")

プログラムのブロックごとに動作の要点を解説します。
このプログラムの名前は「coffee_control_rev1.py」です。

#ライブラリのインポート

ここではまず、「RPi.gpio」と「time」の標準ライブラリーをimportしています。そして「I2C液晶の動作確認モジュール」の章で解説した「lcdtest_2x8.py」をライブラリとして読み込んでいます。「lcdtest_2x8.py」はこのメインプログラムと同一のディレクトリ(当方の場合は/home/pi/python/)に置いてください。

#変数の設定

GPIOで使用するBCM番号を名前の付いた変数に入れています。GPIOの設定をおこなう際にわかりやすくするためです。(以降の説明文ではBUTTON1(24)のようにBCM番号で表記します)最後の「CUPIDX」は現在のカップ数(杯数)を保持するためのグローバル変数として使用しています。

#Parameters:設定パラメータのリスト

前述の「表1 設定パラメータ一覧表」の値が2次元リストのwtimerに入ります。値はwtimer[カップ数][項目]のインデックスで参照します。今回のプログラムでは設定値をプログラムで記述していますが、次回はこの値をWebブラウザーから変更して使えるようにアップデートします。

# GPIO Setup

まず、「GPIO.setmode(GPIO.BCM)」で、BCM番号を使用するモードにセットします。タクトスイッチにつながる、BUTTON1(24)、BUTTON2(25)は「GPIO.IN」の入力モードで、内部プルアップ抵抗をプルダウン「GPIO.PUD_DOWN」に指定します。START_STOP_LED(23)、AC_OUT_LED(17)、SSR(18)の3つは、「GPIO.OUT」モードに設定します。

SSR(18)はPWMの機能を使用します。ラズパイはハードウェアで制御するPWMポートを2つ持っていますが、(18)番ポートはその1つです。

PWMの開始は、以下の文でおこないます。
ssr_pwm=GPIO.PWM(SSR,1)
ssr_pwm.start(0)

パラメータは上の数字1が周波数で、単位はHzです。実数で指定できるので1以下の低い周波数でも対応できます。下の数字0がデューティ比で、%で指定します。こちらも実数で設定できます。初期状態はデューティ比=0で、出力がOFFの状態です。

#杯数設定割り込みルーチン

コーヒーの杯数(カップ数)の設定を黒色のタクトスイッチ「BUTTON1(24)」でおこなっています。このボタンが押されたことを検知して、割り込み処理設定に定義された関数を起動します。その割り込み動作ルーチン:関数が「def button_pushed1」です。黒色のタクトスイッチを押すたびに、このルーチン:関数が呼び出されます。

割り込みの設定は、この後の「#初期設定」の冒頭に書かれている以下の文でおこないます。

GPIO.add_event_detect(BUTTON1, GPIO.RISING, callback=button_pushed1, bouncetime=200)
「GPIO.RISING」で入力がL→Hの立ち上がりのエッジで検知するよう指定します。
「callback=button_pushed1」は検知した時に呼び出す関数名です。
「bouncetime=200」はスイッチのチャッタリングによる誤検知を防ぐための不検知ウインドウ時間です。この例では最初の検知から200msの間は動作しません。

そしてこの割り込み関数ルーティンの最初に、割り込みを禁止する「GPIO.remove_event_detect」を入れています。これは割り込み処理中にさらに割り込みがかかる多重割り込みを防ぐ処置です。このルーティンの最後には「GPIO.add_event_detect」を入れて再び割り込みを有効にします。

次に、現在のカップ数を保持する変数「CUPIDX」に「global」指定をおこないます。これが無いとその関数内のローカル変数として扱われるので、メインプログラムで値が参照できなくなります。

あとはif文でスイッチが押されるたびに杯数がインクリメントする処理を行っています。shellと液晶ディスプレイには杯数が表示されます。

#初期設定

前述の割り込みの設定と液晶ディスプレーへの起動メッセージの表示が行われます。

#メインループ

全体構造で解説しましたが、メインループはスタートキーの入力待ちと、3つのタイマーループから構成されています。

まずスタートキー:BUTTON2(25)の入力を待ちます。
GPIO.wait_for_edge(BUTTON2, GPIO.RISING)

「GPIO.RISING」でエッジの立ち上がりを検出します。スタートすると、まず杯数入力の割り込み処理を禁止します。
GPIO.remove_event_detect(BUTTON1)

コーヒー抽出中は、杯数が変更できないようにするためです。3つのタイマーループが終了した最後に、また割り込みを有効にして杯数設定ができるようにします。

#Set Freqency and DutyCycle:PWMのパラメータ設定

メインループ内でPWMを使ったコーヒーメーカーのON/OFF機能を入れています。動作の概要を下図で説明します。

coffee-maker-with-raspberry-pi-03-06-2

 

PWM(パルス幅変調)は周期的にパルスのON/OFF幅を変化させる信号方式で、LEDの明るさを変化させたり、モータの回転数制御などに使用されています。そういった用途では周波数が数十Hz以上、ONのパルス幅がmsオーダーになります。ラズパイのPWMでは周波数とデューティ比で設定しますが、代入は実数で行えます。そのため1Hz以下の非常に低い周波数でも問題なく動作します。
そこで、コーヒーメーカーがお湯を投入する時間と休止する時間を秒単位で設定して、例えばお湯の投入時間(ON)=60秒、休止時間(OFF)=20秒のような抽出方法に応用してみました。この例では、周波数=0.0125Hz、Duty=75%という非常に低い周波数での設定になりますがPWMは問題なく動作しています。

#1st step:初期投入

最初にコーヒー粉にお湯を行き渡らせる、初期投入のタイマーループです。「time.time()」関数で現在時刻を取得します。秒で変数に入ります。これに初期投入時間を加えたものが「stop_time」になります。タイマーのwhileループは現在時刻がこの「stop_time」を超えたら終了します。

while time.time() < stop_time:

このやり方は以降の2nd step、3rd stepでも同じです。
コーヒーメーカーをONにする方法ですが、SSR(18)はPWMの設定になっています。そのため連続的に電源ONにするには、dutyを100%に設定します。

ssr_pwm.ChangeDutyCycle(100)

そして動作中を示すLEDを点灯させています。

GPIO.output(AC_OUT_LED,1)
GPIO.output(START_STOP_LED,ledflag)

「AC_OUT_LED」はコーヒーメーカーがONであることを示します。「START_STOP_LED」はタイマー動作中であることを示し、1秒のインターバルで点灯します。インターバルは「time.sleep(1)」をループ中に入れて、「ledflag = not ledflag」で点滅させています。LCDにはこの1秒インターバルごとにタイマーの残り時間を表示するようにしています。

#2nd step: 蒸らし

プログラムの構成は#1st stepとほとんど同じです。蒸らしではコーヒーメーカーをOFFにするので、SSR(18)のPWMのdutyを0%にします。
ssr_pwm.ChangeDutyCycle(0)

後は「AC_OUT_LED」を消灯しています。

#3rd step:抽出

こちらも構成は#1st step、#2nd stepと同様です。前述のPWMによるON/OFF機能を有効にするために次の文を実行します。
ssr_pwm.ChangeFrequency(freq_int)
ssr_pwm.ChangeDutyCycle(duty)

「freq_int」と「duty」の変数には、前述の#Set Freqency and DutyCycleで計算した値が入っています。タイマーループが終了すると、PWM設定を以下にして、コーヒーメーカーをOFFにします。(周波数=1Hz、duty=0%)
ssr_pwm.ChangeFrequency(1)
ssr_pwm.ChangeDutyCycle(0)

最後に、杯数設定の割り込みを許可してスタートキーの入力に戻ります。
except KeyboardInterrupt:の部分は、CTRL+Cでプログラム中止を行った場合の例外処理で、GPIOのクリーンアップをおこなって全てのポートを初期化します。

以上がpythonによるプログラムリストの解説になります。実際の本機の動作は、以下のYouTubeビデオでご覧いただけます。

 

次回、最終回となる第4回はこのソフトウェアを拡張して、webでリモート設定できるように改版します。ラズパイではwebサーバの機能を動作させ、クライアント側のwebブラウザから設定をおこないます。html、Java Script、Ajax、PHPなどのwebプログラミングを使用してこの機能を実現します。

最後に本機でコーヒーを入れる際の使いこなし方法についても触れる予定ですのでお楽しみに!

AIロボット2018
阿矢谷充

測定器会社、ネットワーク機器ベンダーでシステム・エンジニアに従事。現在自作派向けの電子工作記事を各誌に掲載中。趣味は古楽器・リュートの演奏。日本リュート協会・理事

http://lute.penne.jp/thumbunder/