できること

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

第4回:リモート設定ソフトウェア

第1回:装置の概要とコーヒーの抽出について
第2回:ハードウェアの製作
第3回:Pythonによる制御ソフトウェア

 

ラズパイを使ってコーヒーメーカーの制御装置を制作する本連載。紹介してくれるのは、これまでもさまざまなメディアで自作派向けの電子工作記事を執筆されてきた、阿矢谷充さんです。第3回では制御ソフトウェアについて紹介しましたが、最終回となる第4回はリモート設定ソフトウェアについて解説します。そして最後にまとめとして、本機を使ってコーヒーを淹れる際の使いこなしについても触れてみたいと思います。

coffee-maker-with-raspberry-pi-04-01

 

目次

  1. ラズパイとWebプログラミング
  2. Webプログラミングによる機能の実装方法
  3. サーバサイドの準備
  4. Pythonによるコーヒーメーカー制御プログラムの改版
  5. HTMLとJavaScriptによるクライアントサイドのプログラム
  6. AjaxとPHPによるHTTP POSTを使用した通信
  7. ラズパイ・サーバ上の各プロググラム配置サマリ
  8. 本機の使いこなしについて

 

1. ラズパイとWebプログラミング

前回までは本機の機能や動作について解説してきましたが、このコーヒーメーカー制御機能を単独で動かす場合、Arduino系のようなコントローラの方がラズパイよりもより簡単で安価に実現できます。例えばタイマーや割り込みはArduino系マイコンの方がはるかに柔軟で機能が多く、サポートされる数や精度もラズパイを上回っています。

しかし、ラズパイの最大のメリットは使用しているLinux系マルチタスクOSの機能を生かしたネットワーク連携のWebプログラミングが使えることです。例えばPythonで作られたプログラムを動かしながら、Webサーバを動作させ、クライアントとネットワークを経由して通信するといったことがラズパイなら容易に実現可能になります。これと同じことをArduino系のコントローラで実現するとすれば、かなり困難で、かつ非常に特殊な実装が必要になると思われます。

普通のLinuxサーバと同じ機能が、3000円台で購入できるラズパイ上で動作して使えるようになったということは、非常に驚くべきかつ喜ばしいことなのではないでしょうか?

 

2. Webプログラミングによる機能の実装方法

本機の特徴のひとつとして、コーヒーを淹れる際の各パラメータ(例えば蒸らし時間など)が自由にプログラムできる点があります。このパラメータは本体で変更するよりも、PCやスマホからおこなえるほうが使い勝手がはるかに向上します。そこで、ラズパイのメリットであるWebサーバやWebプログラミングの機能を利用して、このリモート設定を実現してみました。

下図にその全体構造を示します。

coffee-maker-with-raspberry-pi-04-02-02

 

図の左側がラズパイのコーヒーメーカー制御装置で「サーバサイド」になります。右側が「クライアントサイド」で、PCやスマホ等を使用します。この2つのサイドのデバイスは、Wi-Fiを使ってローカル・ネットワーク経由で接続されます。

まず「サーバサイド」ですが、Webサーバの機能は最も一般的な「Apache2」を使用しました。それと連携する形でスクリプト言語であるPHPのプログラムを動作させます。

サーバサイドで動作するプログラミング言語にはPHPの他にもRubyやJavaなど多数ありますが、最も簡単に実装できてクライアントサイドのHTML/Java Scriptとの親和性が高いPHPを選びました。

Pythonで作成したコーヒーメーカー制御プログラムとは、単純なテキストの設定ファイルを介してやりとりします。大規模なアプリならMySQLなどのデータベースを使うところですが、今回の制御プログラムでは設定が25個程度なのでテキストファイルで十分かと思います。

次に「クライアントサイド」ですが、PCやスマートフォンで共通に動作するマルチなGUIを作る場合に、もっとも簡単なのはWebブラウザで動くHTMLベースのプログラムを使用する方法です。Webブラウザさえあれば、PCがMacだろうが、またはスマホやタブレットにおいても単一のプログラムで全て対応できます。

ただし、HTMLだけでは機能が足りないため、JavaScriptとライブラリーjQueryで動作するAjaxの仕組みを利用します。AjaxはPHPのスクリプトと連携して動作し、サーバ・クライアント間はHTTPプロトコルのPOSTメソッドを使って通信をおこないます。

この仕組みを実現できるのも、ラズパイがWebサーバの機能を持てるからに他なりません。クライアント側のWebブラウザは、Webサーバに置いたHTML(JavaScriptやAjaxのコードを含む)のプログラムを読み込んで実行します。

 

3. サーバサイドの準備

サーバサイド側ではラズパイにWebサーバ「Apache2」のインストールをおこないます。はじめに使用しているRaspbianを最新の状態に更新します。以下のコマンドをシェルから実行します。RaspbianのバージョンはRelease10:busterを使用しています。

sudo apt update
sudo apt upgrade -y

次にApache2をインストールします。

sudo apt-get install apache2

インストールはこれで終了します。正常にインストールされているかをクライアント側のWebブラウザで確認します。PCなどのWebブラウザに、ラズパイのIPアドレスを入力してアクセスします。

http://[ラズパイのIPアドレス] 例: http://192.168.10.100

ラズパイのIPアドレスは固定で割り付けておいた方が後々便利になります。

ブラウザには以下の「Apache2 Debian Default Page」が表示されます。

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

 

このページのメッセージにあるように、これが表示されればApache2は正常に動作しています。このメッセージがWebサーバにアクセスした時にデフォルトで表示される、index.htmlのファイルになっています。場所は以下に保存されています。

/var/www/html/

このディレクトリに、このあと作成するHTMLやPHPのプログラムと、設定のファイルを保存していきます。そのためにはこのディレクトリの権限を書き込み可能に変更する必要があります(デフォルトはroot以外書き込み不可)。

cd /var/www
sudo chmod 777 html/

これでこのディレクトリで、読み書きと実行が可能になります。

次にPHPをインストールします。コマンドは以下になります。

sudo apt-get install php

正常にインストールされたかどうかを以下のコマンドで確認します。

php -v

インストールされたPHPのバージョン情報が表示されます。
また-rオプションで直接PHP文を実行できます。php infoを表示してみます。

php -r “phpinfo();”

これでサーバ側の準備ができました。ラズパイによるWebサーバの構築については、デバイスプラスに「ラズパイ(Raspberry Pi)がWebサーバに早変わり!? IPが固定できなくてもラズパイでWebサーバが構築できる!」というわかりやすい記事がありますのでぜひご参照ください。

ラズパイ(Raspberry Pi)がWebサーバに早変わり!? IPが固定できなくてもラズパイでWebサーバが構築できる!

 

4. Pythonによるコーヒーメーカー制御プログラムの改版

前回の第3回で解説したPythonによる制御プログラムをリモート設定できるように改版します。設定パラメータは以下のリストに入っています。

#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]]

これを設定ファイルから読み込めるようにプログラムを追加します。
設定ファイルはプレーンなテキストで、以下のフォーマットにしています。

60,40,360,60,10–A–68,50,480,60,20–A–70,50,600,60,20–A–74,50,600,60,20–A–0,0,600,60,0

ファイル名は「wtimer.txt」で、保存場所は前述の「/var/www/html/」です。ファイル中のテキストで「–A–」がセパレータになっています。プログラムではsplit文を使うことで容易にセパレータを分離できます。splitはPHPやJavaScriptでもサポートされており、良く使われる非常に便利な命令です。
以下に改版したPythonのプログラムリストを示します。
ファイル名は「coffee_control_remote_rev1.py」です。

# -*- 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 read_file():
    f = open("/var/www/html/wtimer.txt", "r" , encoding="utf-8")
    s = f.read()
    tbox = s.split("--A--")
    f.close()

    print("tbox len=",len(tbox))
    for idx in range(len(tbox)):
        print(idx,"=",tbox[idx])
        plist = tbox[idx].split(",")
        for idy in range(len(plist)):
            wtimer[idx][idy]=int(plist[idy])
    print("wtimer=")
    print(wtimer)

#杯数設定割り込みルーチン
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.")
        read_file()
        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")

以下追加された部分について解説します。

「#ファイル読み込み def read_file():」が設定ファイルを読んでリストに代入する関数です。
/var/www/html/wtimer.txtが設定ファイルで、openしてreadで”s”に読み込みます。

最初のsplitで”s”中にある「–A–」のセパレータを取り除きます。

tbox = s.split("--A--")

” tbox”は自動的にリストになりsplitで分割された要素が入ります。
次のステップでtbox各要素のセパレータ「,」を取り除き、wtimerのリストに代入します。forループを2回回してtboxからwtimerの2次元のリストに値を代入しています。

作成したread_file()関数は、以下のようにメインループ冒頭の、スタートボタンが押されたことを検出した直後に呼び出します。

GPIO.wait_for_edge(BUTTON2, GPIO.RISING)
print("Rising edge detected on BUTTON2.")
read_file()

これで最新の更新された値で、コーヒー抽出ルーチンがスタートします。
追加変更した部分は以上で、その他は第3回で紹介したプログラムと同じです。

 

5. HTMLとJavaScriptによるクライアントサイドのプログラム

次にクライアントサイドのプログラムについて解説します。概要説明で触れましたが、Webブラウザーで動作するHTML/JavaScriptを使用しています。ブラウザでの画面イメージを以下に示します。

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

 

杯数ごとの各パラメータは表形式で編集・閲覧します。下の3つのボタンの機能は、編集したデータの送信、現在の設定取得、デフォルト値のセット(画面上のみ)です。一番下のテキスト行は、デバック用にボタン操作時の生データ等を表示させています。

このHTMLプログラムのリストを以下に示します。ファイル名は「coffee_remote.html」です。

<!DOCTYPE html>
<!-- コーヒーメーカー制御装置 リモート設定 Rev 1.0 -->
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>コーヒーメーカー制御装置 設定</title>
  <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%20type%3D'text%2Fjavascript'%20src%3D%22https%3A%2F%2Fajax.googleapis.com%2Fajax%2Flibs%2Fjquery%2F1.11.1%2Fjquery.min.js%22%3E%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="&lt;script&gt;" title="&lt;script&gt;" />

  <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%3E%0A%20%20%20%20var%20postdata%3B%0A%20%20%20%20var%20tabaledata%3B%0A%0A%20%20%20%20%2F%2F%E3%83%87%E3%83%BC%E3%82%BF%E9%80%81%E4%BF%A1%20Ajax%0A%20%20%20%20function%20SendAjax()%20%7B%0A%20%20%20%20%20%20getdataid()%3B%0A%20%20%20%20%20%20postdata%20%3D%20tabaledata%3B%0A%20%20%20%20%20%20%24.post('table_rx_s.php'%2C%20%7B%0A%20%20%20%20%20%20%20%20'message'%3A%20postdata%0A%20%20%20%20%20%20%7D%2C%20DataAjax_Return)%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20function%20DataAjax_Return(result)%20%7B%0A%20%20%20%20%20%20alert(%22%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E5%BF%9C%E7%AD%94%3D%3D%22%20%2B%20result)%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20%2F%2F%E3%83%87%E3%83%BC%E3%82%BF%E5%8F%97%E4%BF%A1%E3%80%80Ajax%0A%20%20%20%20function%20SendAjax2()%20%7B%0A%20%20%20%20%20%20%24.post('c_getfile.php'%2C%20%7B%0A%20%20%20%20%20%20%20%20'item'%3A%20%22wtimer.txt%22%0A%20%20%20%20%20%20%7D%2C%20DataAjax_Return2)%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20function%20DataAjax_Return2(result)%20%7B%0A%20%20%20%20%20%20document.getElementById(%22ptag%22).innerText%20%3D%20result%3B%0A%20%20%20%20%20%20var%20getresult%20%3D%20result.split(%22--A--%22)%3B%0A%20%20%20%20%20%20for%20(var%20i%20%3D%200%3B%20i%20%3C%3D%20getresult.length%20-%201%3B%20i%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20var%20cdata%20%3D%20getresult%5Bi%5D.split(%22%2C%22)%3B%0A%20%20%20%20%20%20%20%20for%20(var%20j%20%3D%200%3B%20j%20%3C%3D%20cdata.length%20-%201%3B%20j%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20document.getElementById(%22c%22%20%2B%20i%20%2B%20j).value%20%3D%20cdata%5Bj%5D%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%0A%20%20%20%20%2F%2F%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%81%8B%E3%82%89%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E5%8F%96%E5%BE%97%0A%20%20%20%20function%20getdataid()%20%7B%0A%20%20%20%20%20%20tabaledata%20%3D%20%22%22%3B%0A%20%20%20%20%20%20var%20mgarr%20%3D%20new%20Array()%3B%0A%20%20%20%20%20%20for%20(var%20i%20%3D%200%3B%20i%20%3C%205%3B%20i%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20for%20(var%20num_c%20%3D%200%3B%20num_c%20%3C%205%3B%20num_c%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20mgarr%5Bnum_c%5D%20%3D%20document.getElementById(%22c%22%20%2B%20i%20%2B%20num_c).value%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20tabaledata%20%3D%20tabaledata%20%2B%20mgarr%5B0%5D%20%2B%20%22%2C%22%20%2B%20mgarr%5B1%5D%20%2B%20%22%2C%22%20%2B%20mgarr%5B2%5D%20%2B%20%22%2C%22%20%2B%20mgarr%5B3%5D%20%2B%20%22%2C%22%20%2B%20mgarr%5B4%5D%20%2B%20%22--A--%22%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20tabaledata%20%3D%20tabaledata.slice(0%2C%20tabaledata.length%20-%205)%3B%0A%20%20%20%20%20%20document.getElementById(%22ptag%22).innerText%20%3D%20tabaledata%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20%2F%2F%E3%83%87%E3%83%95%E3%82%A9%E3%83%AB%E3%83%88%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E3%83%AD%E3%83%BC%E3%83%89%0A%20%20%20%20function%20filldefaultdata()%20%7B%0A%20%20%20%20%20%20var%20initdata%20%3D%20%5B%0A%20%20%20%20%20%20%20%20%5B60%2C%2040%2C%20360%2C%2030%2C%2010%5D%2C%0A%20%20%20%20%20%20%20%20%5B68%2C%2050%2C%20600%2C%2040%2C%2020%5D%2C%0A%20%20%20%20%20%20%20%20%5B70%2C%2050%2C%20660%2C%2040%2C%2020%5D%2C%0A%20%20%20%20%20%20%20%20%5B74%2C%2050%2C%20720%2C%2040%2C%2020%5D%2C%0A%20%20%20%20%20%20%20%20%5B0%2C%200%2C%20600%2C%2060%2C%200%5D%0A%20%20%20%20%20%20%5D%3B%0A%20%20%20%20%20%20for%20(var%20i%20%3D%200%3B%20i%20%3C%205%3B%20i%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20for%20(var%20j%20%3D%200%3B%20j%20%3C%205%3B%20j%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20document.getElementById(%22c%22%20%2B%20i%20%2B%20j).value%20%3D%20initdata%5Bi%5D%5Bj%5D%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%20%20window.onload%20%3D%20function()%20%7B%0A%20%20%20%20%20%20%2F%2F%E3%83%AD%E3%83%BC%E3%83%89%E6%99%82%E3%81%AE%E5%AE%9F%E8%A1%8C%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%0A%20%20%20%20%7D%0A%20%20%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="&lt;script&gt;" title="&lt;script&gt;" />

</head>
<table border="1">
  <caption>コーヒーメーカー制御装置 設定</caption>
  <tbody>
    <tr>
      <td style="height: 5px; width: 50px; text-align: center;">杯数</td>
      <td style="width: 90px; height: 5px; text-align: center;">初期投入</td>
      <td style="width: 90px; height: 5px; text-align: center;">蒸らし</td>
      <td style="width: 90px; height: 5px; text-align: center;">抽出時間</td>
      <td style="width: 90px; height: 5px; text-align: center;">ON時間</td>
      <td style="width: 90px; height: 5px; text-align: center;">OFF時間</td>
    </tr>
    <tr>
      <td style="height: 5px; width: 50px; text-align: center;">2</td>
      <td style="width: 90px; height: 5px;"><input id="c00" type="text" style="width: 90px; text-align: center; border-style: none;"></td>
      <td style="width: 90px; height: 5px;"><input id="c01" type="text" style="width: 90px; text-align: center; border-style: none;"></td>
      <td style="width: 90px; height: 5px;"><input id="c02" type="text" style="width: 90px; text-align: center; border-style: none;"></td>
      <td style="width: 90px; height: 5px;"><input id="c03" type="text" style="width: 90px; text-align: center; border-style: none;"></td>
      <td style="width: 90px; height: 5px;"><input id="c04" type="text" style="width: 90px; text-align: center; border-style: none;"></td>
    </tr>
    <tr>
      <td style="height: 5px; width: 50px; text-align: center;">3</td>
      <td style="width: 90px; height: 5px;"><input id="c10" type="text" style="width: 90px; text-align: center; border-style: none;"></td>
      <td style="width: 90px; height: 5px;"><input id="c11" type="text" style="width: 90px; text-align: center; border-style: none;"></td>
      <td style="width: 90px; height: 5px;"><input id="c12" type="text" style="width: 90px; text-align: center; border-style: none;"></td>
      <td style="width: 90px; height: 5px;"><input id="c13" type="text" style="width: 90px; text-align: center; border-style: none;"></td>
      <td style="width: 90px; height: 5px;"><input id="c14" type="text" style="width: 90px; text-align: center; border-style: none;"></td>
    </tr>
    <tr>
      <td style="height: 5px; width: 50px; text-align: center;">4</td>
      <td style="width: 90px; height: 5px;"><input id="c20" type="text" style="width: 90px; text-align: center; border-style: none;"></td>
      <td style="width: 90px; height: 5px;"><input id="c21" type="text" style="width: 90px; text-align: center; border-style: none;"></td>
      <td style="width: 90px; height: 5px;"><input id="c22" type="text" style="width: 90px; text-align: center; border-style: none;"></td>
      <td style="width: 90px; height: 5px;"><input id="c23" type="text" style="width: 90px; text-align: center; border-style: none;"></td>
      <td style="width: 90px; height: 5px;"><input id="c24" type="text" style="width: 90px; text-align: center; border-style: none;"></td>
    </tr>
    <tr>
      <td style="height: 5px; width: 50px; text-align: center;">5</td>
      <td style="width: 90px; height: 5px;"><input id="c30" type="text" style="width: 90px; text-align: center; border-style: none;"></td>
      <td style="width: 90px; height: 5px;"><input id="c31" type="text" style="width: 90px; text-align: center; border-style: none;"></td>
      <td style="width: 90px; height: 5px;"><input id="c32" type="text" style="width: 90px; text-align: center; border-style: none;"></td>
      <td style="width: 90px; height: 5px;"><input id="c33" type="text" style="width: 90px; text-align: center; border-style: none;"></td>
      <td style="width: 90px; height: 5px;"><input id="c34" type="text" style="width: 90px; text-align: center; border-style: none;"></td>
    </tr>
    <tr>
      <td style="height: 5px; width: 50px; text-align: center;">6</td>
      <td style="width: 90px; height: 5px;"><input id="c40" type="text" style="width: 90px; text-align: center; border-style: none;"></td>
      <td style="width: 90px; height: 5px;"><input id="c41" type="text" style="width: 90px; text-align: center; border-style: none;"></td>
      <td style="width: 90px; height: 5px;"><input id="c42" type="text" style="width: 90px; text-align: center; border-style: none;"></td>
      <td style="width: 90px; height: 5px;"><input id="c43" type="text" style="width: 90px; text-align: center; border-style: none;"></td>
      <td style="width: 90px; height: 5px;"><input id="c44" type="text" style="width: 90px; text-align: center; border-style: none;"></td>
    </tr>
  </tbody>
</table>
<p><button type='button' onclick='SendAjax()' style='color: #ffffff; background-color: #dc143c'>Data 送信</button>
  <button type='button' onclick='SendAjax2()' style='color: #ffffff; background-color: #26448D'>現在の設定取得</button>
  <button type='button' onclick='filldefaultdata()' style='color: #ffffff; background-color: #26448D'>Default値</button></p>

<p id="ptag"></p>

</html>

全体の構成ですが、前半のタグで囲われた部分がJavaScriptで記載されたプログラムです。Ajaxを使用しているところは次の項で解説します。

<table border=”1″></table>で囲われた部分が、表形式のデータ入力部です。
5行×5列のテーブルで、1項目のサンプルを以下に示します。

<td style="width: 90px; height: 5px;"><input id="c00" type="text" style="width: 90px; text-align: center; border-style: none;"></td>

<td></td>タグ内に、テキスト入力の<input>タグを入れています。ここにはid=”c00”の様に順番号のidを振っていますが、これはJavaScriptで入力された値を取得するためです。

「function getdataid()」がテーブルのデータを取得するJavaScriptの関数です。
「document.getElementById(“c” + i + num_c).value」がidでその内容を取得するメソッドです。<input>タグのidをforループで回してその値を参照しています。
セパレータとして、”,”と”–A–”を付加して「tabaledata」の変数に入れています。
「tabaledata = tabaledata.slice(0, tabaledata.length – 5);」はループで最後に付加された”–A–”を削除するためにsliceを使用しています。

最後のブロックで3つのボタンを作成しています。以下は「Data 送信」ボタンの例です。

<button type='button' onclick='SendAjax()' style='color: #ffffff; background-color: #dc143c'>Data 送信</button>

ここでは、”onclick”によって呼び出すJavaScriptの関数を登録します。上の例では「SendAjax()」が関数名で、Ajaxを利用したデータ送信ルーチンです。

 

6. AjaxとPHPによるHTTP POSTを使用した通信

Webサーバーとクライアント間でのやり取りに使われるのがHTTPプロトコルです。その中でPOSTメソッドはフォームでの送信などに使われ、サーバからのデータの受信もおこなえます。このPOSTをJavaScriptから行う際に便利なのがAjaxです。

Ajaxは「Asynchronous JavaScript + XML」の略で、非同期にXMLデータをJavaScript上で送受信できる仕組みですが、XMLだけでなく普通のテキスト情報でも問題なく利用できます。非同期ということで、ブラウザの動作と関係なく通信処理ができるので非常に便利です。

このAjaxをJavaScriptで使う時に利用されるのがjQueryです。jQueryはJavaScriptで複雑な機能を容易に記述できるように設計されたライブラリで、広く使用されています。以下jQuery/Ajaxを利用したHTTP POSTについてプログラムを解説します。

まず、jQueryを利用するためにライブラリを読み込みます。ダウンロードしたものを読み込むのとWeb上に公開されているCDNを利用する方法がありますが、後者を使用しています。以下の文で読み込みます。

<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%20type%3D'text%2Fjavascript'%20src%3D%22https%3A%2F%2Fajax.googleapis.com%2Fajax%2Flibs%2Fjquery%2F1.11.1%2Fjquery.min.js%22%3E%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="&lt;script&gt;" title="&lt;script&gt;" />

ここではGoogleのCDNサイトを利用しています。

次にAjaxを使ってPOSTするプログラムを説明します。以下はパラメータを送信する関数です。

//データ送信 Ajax
    function SendAjax() {
      getdataid();
      postdata = tabaledata;
      $.post('table_rx_s.php', {
        'message': postdata
      }, DataAjax_Return);
    }

    function DataAjax_Return(result) {
      alert("サーバー応答==" + result);
    }

はじめに作成した関数の「getdataid()」を呼んで、HTMLのテーブルに入力された値を読み込みます。
$.post以下がAjaxのPOSTライブラリです。送信先のPHPスクリプト「table_rx_s.php」を指定し、「’message’」という名前のキーで、送信するデータの入った変数「postdata」を記載します。「DataAjax_Return」は、サーバからのレスポンスを受け取る関数です。resultにレスポンスが入ります。ここでは内容をポップアップalertで表示しています。
これに対応するサーバ受信側のPHPスクリプト「table_rx_s.php」を以下に示します。

<?php
  $rvmsg = $_POST["message"];
  if (strlen($rvmsg) < 150){
    $lfilename = "wtimer.txt";
    $fp = fopen($lfilename,'w');
    flock($fp, LOCK_EX);
    fwrite($fp, htmlspecialchars($rvmsg,ENT_QUOTES));
    fclose($fp);
    echo "PHP Post Success!!";
  }else{
  	echo "msg > 150 chr";
  }
?>

「$_POST[“message”]」はスーパーグローバル変数という予約語で、POSTメソッドで受信したデータが入ります。”message”のキーを指定して受け取ります。

この受信データを「wtimer.txt」という名前のテキストファイルに保存しますが、ここでセキュリティ対策をおこないます。まず、メッセージの長さ制限をおこなっています。if文で150キャラクター以上の長さのデータを制限しています。あと「htmlspecialchars」はXSS(クロスサイトスプリクティング)を防ぐための関数で、スプリクトで使用される文字の一部をエスケープ処理します。この装置はプライベートネットワーク内で使用されるもので、外部からの接続は考えていませんが、以上のような最低限のセキュリティ対策はおこなっています。

もう一つのAjaxプログラムは現在のパラメータ設定の読み込みです。サーバ側の「wtimer.txt」ファイルを受信してブラウザに表示します。以下がAjaxのプログラムです。

//データ受信 Ajax
    function SendAjax2() {
      $.post('c_getfile.php', {
        'item': "wtimer.txt"
      }, DataAjax_Return2);
    }

    function DataAjax_Return2(result) {
      document.getElementById("ptag").innerText = result;
      var getresult = result.split("--A--");
      for (var i = 0; i <= getresult.length - 1; i++) {
        var cdata = getresult[i].split(",");
        for (var j = 0; j <= cdata.length - 1; j++) {
          document.getElementById("c" + i + j).value = cdata[j];
        }
      }
    }

送信先のPHPスクリプト「c_getfile.php」を指定して、読み込むファイル名「wtimer.txt」をitemのキーで送信します。
受信した「wtimer.txt」の内容は、関数「DataAjax_Return2」で処理してテーブルに表示します。ここでもpythonのプログラムと同様なsplit文で「–A–」と「,」のセパレータを取り除いています。JavaScriptでのHTMLへのテキストデータ書き込みは、

document.getElementById("c" + i + j).value

を使用しています。

対応するサーバ受信側のPHPスクリプト「c_getfile.php」を以下に示します。

<?php
$lfilename =  $_POST['item'];
	if ($lfilename == "wtimer.txt"){
	  $lineout= file_get_contents($lfilename);
	  echo $lineout;
	}
?>

こちらは非常にシンプルで、POST:itemで受け取ったファイル名のファイルを「file_get_contents」関数で読み込んでechoでクライアントに送信します。file_get_contentsはファイル全体を読み込んで文字列で返す関数です。

 

7. ラズパイ・サーバ上の各プロググラム配置サマリ

以上、リモート設定をおこなうための各プログラムについて解説しました。関係するファイルが多いので、それらをラズパイ・サーバ上のどこに配置しているかを以下にサマリします。

ラズパイのディレクトリとファイル:

/var/www/html/
coffee_remote.html (リモート設定用HTMLファイル)
table_rx_s.php (パラメータ設定受信用PHPファイル)
c_getfile.php (パラメータ設定ファイル送信用PHPファイル)
wtimer.txt (パラメータ設定ファイル)

/home/pi/python/
coffee_control_remote_rev1.py (コーヒーメーカー制御のPythonプログラム)
lcdtest_2x8.py (LCD表示用モジュール:連載第3回で説明したもの)

クライアント側ではWebブラウザから「coffee_remote.html」にアクセスします。
http://[ラズパイのIPアドレス]/ coffee_remote.html

当方はパソコンにはMacを使用していますが、Safari、Chromeの最新版で動作確認して特に問題は見られませんでした。Mac版のFirefox(78.8)では、原因は判りませんが入力した値が更新されないという不具合が見られました。ただし、Windows版Firefoxでは正常に動作しています。スマホはiPhoneのsafariで確認し、動作に問題はありませんでした。

 

8. 本機の使いこなしについて

私はほぼ毎日、本機でコーヒーを淹れています。そんな中、自分なりの「本機を使用した場合の美味しいコーヒーを淹れるコツ」が見つかってきましたので、こちらでご紹介します。
コーヒーの好みは十人十色で異なります。また、体調や季節の変化、温度により体が感じる味や香りも変化すると言われています。こちらはあくまでも参考として見ていただければと思います。

今回、本機で制御をおこなったコーヒーメーカーはカリタ社のET-102です。コーヒーの出来上がりはET-102そのものの特性に依存する部分が大きいと思います。ET-102の特徴として以下の2点があります。

(1)抽出湯温が85℃程度以下と低めである。
(2)連続通電で一度に出てくるお湯の量は少なめで、抽出終了まで時間がかかる。

上記の特徴から、ET-102が不得意である、高温(90℃以上)で素早く抽出を終えるやり方は、最近流行りの「浅煎りのスペシャリティー・コーヒー」に向いているものです。こういったコーヒーは、やはりハンドドリップで淹れるのが好ましいと思われます。

逆に上記(1)(2)の特性は、日本伝統のやや深煎りで濃い目のコーヒーを淹れるためには最適です。特に本機を使用すると(2)の特性と組み合わせて、よりゆっくりと時間をかけた濃厚なコーヒー液の抽出を行うことが出来ます。ハンドドリップでは、長時間ポットを持ってお湯をゆっくり注ぐ必要があるため非常に大変です。

そしてハンドドリップでは豆の様子を見ながら微妙に注湯をコントロールするようなデリケートな動作が可能ですが、人間が行う動作ですので、全く同じ動作を繰り返すことは難しいです。
本機では、いつもほぼ同じ抽出動作を繰り返し再現できる点もメリットであり、この点は次にあげるようなドリッパーの違いを比較したりする際には非常に役立ちます。
コーヒー抽出では、使用するドリッパーの選択は非常に重要です。以下の図に当方が所有している各社のドリッパーを透過スピード別に並べてみました。

coffee-maker-with-raspberry-pi-04-05

 

左側は「円錐形」のもので、右側は「台形形」です。左に行くほど透過時間が短く、お湯が早く抜けていきます。円錐形の方が台形形より透過時間が短く、メリタは穴が1つで一番ゆっくりと透過します。本機を使って、同じ時間のインターバルでこれらのドリッパーを変えて抽出してみましたが、驚くほど味が変わります。一般的にお湯を注いでからコーヒーの粉の層を抜ける時間が短い方が雑味の少ない抽出がおこなえると言われています。こういった点では透過の遅いメリタは不利かと思ったのですが、実際に試してみると非常に濃厚な味のコーヒーが抽出できて当方はとても気に入っています。

ただ、焙煎が浅煎り寄りのものには相性が悪く、そちらは円錐形の方が好みの味に抽出できました。また、その円錐形の透過が速いタイプでも、深煎りの豆でインターバルを長めに設定してじっくり抽出することによって、通常手順で淹れたのとはまた違った濃厚な味わいのコーヒーが楽しめています。

そして一番影響があったのが、挽いたコーヒー粉の大きさ(メッシュ)の違いです。本機では抽出時間が長くなるため、例えば市販の既に挽いてあるレギュラーコーヒーの粉では、残念ながらメッシュが細かすぎて雑味が多く感じられてしまいました。現状は私が所有するコーヒーミルで粗挽きの設定(5段階で4.5〜5程度)にして良好な結果を得ています。

以上、4回の連載でラズパイを使ったコーヒーメーカー制御装置の解説をおこなってきました。長期にわたり記事をご覧いただきありがとうございました。私自身、本機を作成して、以前よりも色々な豆や器具の違いを試す機会が多くなり、非常に楽しいコーヒーライフを送っております。本連載が皆様の電子工作やコーヒーライフの一助になれば幸いです。

 

 

今回の連載の流れ

第1回:装置の概要とコーヒーの抽出について
第2回:ハードウェアの製作
第3回:Pythonによる制御ソフトウェア
第4回:リモート設定ソフトウェア(今回)

アバター画像

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

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

学生ロボコン2017 出場ロボット解剖計画