技術開発部の原田です。普段は業務ハッカーへの道を記載しています。
今回は趣向を変えて原田の自宅で構築した、低温調理のためのIoTデバイスについて記載します。
TL;DR ※まとめです
- Raspberry Pi、Groveリレー、1wire温度計で一定温度を保つ料理用デバイスを自作した
- フィードバック制御の一つであるPID制御にて温度を制御した(オーバーシュートも防いだ)
- 温泉たまごや均一な焼き具合の肉ができ、美味!料理に再現性の確保をした
はじめに
唐突ですが、食用の肉を低温で長時間調理すると、均一な焼き具合になるようなので、非常に美味しく感じるとのことを情報として得ました。
これは是非自分でも実現してみたい。ということで低温調理を実施することにしました。
最初にAmazonで低温調理器を検索すると、Anova Precision Cooker 19400円が。高いです。価格の理由で却下。
そこで、そもそもやろうとしていることを整理して考えてみました。考えてみると、実のところ仕組みは至ってシンプルなわけです。
食材を袋に入れて水を張ったポットの中に投入。ポット中の水温は温度計で常時計測。
ポットをヒーターで温めて、温度が上がりすぎないようにマイコンからヒーターのON/OFF制御を行う。
これだけです。イメージができたところで機材を用意しましょう。
用意した機材
- 食材の袋、ジップロックです。自宅にありました。
- ヒーター&ポット、あるじゃないか。ティファールの湯沸かし器です(JUSTINE)。スイッチを入れると沸騰するまで沸かすタイプ。保温機能なし。自宅にありました。
- 温度は温度計で測れば良いです。1Wireで防水のものが必要です。こちらはAmazonで5個セット1080円を購入。(HiLetgo DS18B20サーマルプローブセンサー)
- ON/OFF制御するスイッチです。Groveリレー。(Grove SPDT Relay(30A))共立エレショップで1490円。
- ティファールのケーブルを傷めないよう、0.1mも延長コードも仕入れました。Amazonで3個セット691円を購入。(Elecom T-ADR1WHX3電源ケーブル0.1m延長コード)
- マイコンとしてRaspberry Pi及び回路周りの部品、自宅にありました。2B+でも十分です。
ON/OFF制御は、家庭用電源に指す電源ケーブルを一部剥き、スイッチとなるリレーを差し込む形です。後述します。
というわけで追加コストは以下の通り。
1080+1490+691 = 3261円。安い!w
パーツを用意できたところで、さあ作りましょう。レッツIoT料理w
構成図
いきなりですが、これが装置の一式です。
簡単に言えば、水温を取得するための温度計、電源をON/OFF制御するためのリレーをそれぞれマイコンに接続します。
温度計→Raspberry PiのGPIO20 マイコン側にとってインプットに使う
リレー→Raspberry PiのGPIO21 マイコン側にとってアウトプットに使う
回路図は画像をあわせて後述します。
以下のような形で動作します。画面中央のリレーが通電(LEDが付く)すると、ティファールの電源が付きます。
動画
以下解説では主にマイコン中でのソースコードを解説します。
解説
ラズパイの中でGPIO制御をしています。実装はPython – jupyter notebookで行っています。
jupyter notebookを参考までにダウンロード可能にしておきます。
#!/usr/bin/env python # coding: utf-8 # # 低温調理する。 # # 基本的には以下の通りにする。 # # * GPIO21にリレーを接続する # * GPIO20に温度計を接続する # # # In[1]: import RPi.GPIO as GPIO import time import glob import sys # In[2]: # Settings for Thermometer. DEVICES=glob.glob('/sys/bus/w1/devices/*/w1_slave') DEVICE_FILE=DEVICES[0] # In[3]: # Settings for Relay. GPIO_RELAY = 21 GPIO.setwarnings(False) GPIO.setmode(GPIO.BCM) GPIO.setup(GPIO_RELAY, GPIO.OUT) # In[4]: # Power off manually. # Remain for safety reason. Use it when you want to stop heating. GPIO.output(GPIO_RELAY, GPIO.LOW) # In[5]: # Read Temparature. using DS18B20 def read_temparature(): file = open(DEVICE_FILE) lines = file.readlines() temp = lines[1].split('t=')[1] return float(temp) / 1000 # In[6]: # Heating # power : <0 then just sleep 10 sec. >0 then power on t sec and off (1-t) sec. # example : Given power=0.4, Then heating 4sec and Not heating 6sec. def heating(power): if power < 0: GPIO.output(GPIO_RELAY, GPIO.LOW) time.sleep(10) return if power > 1: power = 1 on = power * 10 off = (1-power) * 10 if on > 0: GPIO.output(GPIO_RELAY, GPIO.HIGH) time.sleep(on) if off > 0: GPIO.output(GPIO_RELAY, GPIO.LOW) time.sleep(off) # In[7]: # Proportional Controller def p(temp, target, kp): d = target - temp if d < 0: return 0 power = d / target * kp return power # In[8]: # Integral Controller def i(hist, target, ki): s = 0 for i in range(len(hist) - 1): d1 = target - hist[i] d2 = target - hist[i+1] s += (d1 + d2) / 2 * ki return s # In[9]: # Differential Controller def d(hist, target, kd): if len(hist) < 2: return 0 d1 = target - hist[1] d2 = target - hist[0] d = d2 - d1 if d == 0: return 0 return d * kd / 10 # パラメータ指定する # # * target_temp : 保ちたいtarget温度(℃) # * limit_time_sec : 調理時間(秒) # * kp : 偏差ゲイン # * ki : 積分ゲイン # * kd : 微分ゲイン # # In[10]: # Init params. target_temp = float(58) limit_time_sec = 360 kp = 2.7 ki = 0.01 kd = 3 # In[11]: # Main Exec. hist = [] started_time=time.time() print("START Cooking!") while True: # Check elapsed time. elapsed_time = time.time() - started_time if elapsed_time > limit_time_sec: print("DONE Cooking!") break # Read temparature. try: temp = read_temparature() except Exception as e: print(e) time.sleep(1) continue # Calcurate power. hist.insert(0, temp) if len(hist) > 7: hist.pop() pg = p(temp, target_temp, kp) ig = i(hist, target_temp, ki) dg = d(hist, target_temp, kd) power = pg + ig + dg print(elapsed_time, ',', temp, ',', power) # Do heating.(Output power.) heating(power)
Pythonのコード動作を説明します。
ヒーターを制御して温度を一定にするため、フィードバック制御(PID制御)が入っています。
PIDについては備考にて後述します。また、さらにPIもしくはPID制御で試した際の実験ログノートも記載します。
計算された出力値powerに応じて、ヒーターをON/OFFします。
まず10秒を1つのセットとして考えます。出力値に応じてt秒間電源ON, 10-t秒間電源OFFにします。
こうしてON/OFFの時間を制御することで、ON/OFFのデジタル値を擬似的な連続値に変換しています。
あとは調理時間limit_time_secを設定すると、その時間を超えるまでwhileループの内容が実行されます。
最後に
IoTで料理ができるということは、季節や外部環境によったパラメータ調整を除けば、
料理に再現性を求めることができるでしょう。
数度の温度差で味の変わってしまう茶でも、このような仕組みを用いれば味の最適解が出せるのかもしれません。
また、ヒーターの容器を変えると煮物などにも応用できる可能性はあります。
本記事では、温度センサー、リレー、マイコンだけでも応用次第で身近な課題を解く可能性を示しました。
課題の解決を求められていたときに、普段からソフトウェアのことだけを考えていると、
つい発想が機械学習/Deep Learningなど、ソフトウェアでの解決に入り込んでしまいます。
それですぐ解決できるのであればそれでも良いでしょう。
しかし、「意外にセンサーでできないか」「実世界で現象を検知できるものが無いか」
という「楽な」解決方法を探索することも大事ではないでしょうか。
検知できればあとはルールベースで解決するだけです。
原田が考えるに、センシングは意外に簡単な課題解決を導くと思います。
ここまで読んでいただきありがとうございました。
以下はもっとマニアックな話題になります。ご注意ください。
オチ
調理準備中。原田は半笑いで用意しています。黄身の凝固温度は65-70℃、白身のそれは70-75℃らしいです。そのため67℃にして黄身の凝固のみ狙ったことになります。
その後パラメータを調整し、いよいよ本番のステーキ肉です。近所のスーパーで購入。
ジップロックに入れられ下味を付けた肉。この時点で原田は爆笑しています。
先程同様ティファール内で58℃60分間で低温調理すると・・・できました!(赤い肉汁が出ていますが問題ないらしいです。実験レポート参照)
見た目はそれほどぱっとしませんが、ムラが無く端から端まで均一に熱が通っています。
これはもう味のIT革命・・・やめとこw
補足
フィードバック(PID)制御について
フィードバック制御の一つである、PID制御について少し解説します。より詳しくはWikipediaを参照願います。GIFがあって非常に理解しやすいです。
PID制御とありますが、Proportional-Integral-Differential Controllerの頭文字をとってPIDと付けられています。
日本語では「ターゲット値との偏差」、「その積分」、「その微分」による効果を重みを付け足し合わせて制御する値を決めることになります。
言葉で書くならば以下の通りです。
与える出力 = kp x ターゲット値との偏差 + ki x 偏差の時間積分 + kd x 偏差の時間微分
ここでkp,ki,kdはそれぞれ別の値をもった定数です。
偏差の時間積分は過去6回測定データを用いた台形の面積の和、偏差の時間微分は過去1回のデータを用いた傾きで近似しています。
与える出力によってヒーターがONになる時間が決定され、温度がそれに比例して上がり下がりします。
今回、PID制御を用いて防ぎたかった現象が3つあります。
- オーバーシュート
- ハンチング
- オフセット
オーバーシュートとは、ヒーターで制御したが、目標とする温度(ターゲット温度)を超えてしまうことです。今回は初回のPI制御で強く発現しました。
ハンチングとは、ターゲット温度を中心に大きく温度が波打ってしまうことです。外気温との差があまり大きくない&水自体の温度が金属などに比べ温度変化しにくいためか、今回はほぼ発生していません。
オフセットとは、ターゲット温度に達するための出力が足りず、いつまで経ってもターゲット温度に近づかない現象です。今回は発生していません。
これらの現象を防ぐためには、kp,ki,kdの値を決めていくことになりますが、今回はkp=2.7, ki=0.01, kd=3で実施するのが良いと判断しました。
電源ケーブルを一部剥き、スイッチとなるリレーに差し込む
この点少々電気的には危険のため、注意が必要です。
※ 電源ケーブルを一部剥いてしまうことは、延長コード販売元の保証対象外になります。
※ 同様のことを行う場合は、必ず電源を切った状態で実施してください。
※ 被服を剥いた箇所については隙間なく絶縁テープを巻いてください。熱収縮チューブなどによる保護も有効です。
※ 制御する電化製品は定格ワット数以内となるようにしてください。リレーに大電流が流れると熱を持ち発火の恐れがあります。ご自身で計算の上、使用ください。
※ 濡れた場所、埃のある場所での使用は禁止です。発火の恐れがあります。
※ 本記事による電源のショートによる発火及びその他事故については当方では責任を負いかねます。
電源ケーブルはただの2本の導線であるため、導線の片方の線だけを切断し、切断面の片側をリレーのCOMポートに。もう片方をNOに差し込みます。
今回はNCとNOにつないでもどちらでも問題ありません。逆につないだ場合にはGPIOの出力でON/OFFするHIGH/LOWが入れ替わるという影響が出るだけです。
回路図
詳細は画像で。この赤い基盤にあるピンがそのままRaspberry Piに接続されているので、後はこの白い板(ブレッドボード)の中で回路を組みます。
画面右側の塊4本線(赤黒黄白)がリレーに接続されます。
画面左側の塊3本線(赤黒黄)はそのまま温度計です。
ブレッドボード左端の赤縦列は3.3V,その隣の青縦列はGNDです。
ブレッドボード右端の赤縦列は5V,その隣の青縦列はGNDです。
リレーの信号線(黄)をGPIO21に接続します。VCC(赤)は5V、GND(黒)はGND、白は何も接続しなくて良いためGNDに落としておきます。
温度計の信号線(黄)をGPIO20に接続します。VDD(赤)は3.3V、GND(黒)はGNDです。
※ 温度計のVDDとDQの間に抵抗がありますが説明は割愛します。
最後に、考察を含めて実験の記録を残しておきます。実験は全3回行いました。
実験ログノート
実験第一回(温泉たまごを作る)
- 日時:10/13 21:12より開始
- 場所:自宅7F 自室、木製机上
- 室温:25℃ 湿度不明
- 電熱器:T-fal JUSTIN
- 投入物:卵:白色 冷蔵庫にて長時間保存、取り出し後すぐ電熱器に投入し実施。通常のゆで卵同様、水に直接卵を投入している
- 制御条件:PI制御
- 制御パラメータ:以下の通り
- ターゲット温度:67℃
- トータル制御時間1500sec(25分)
- 水1.1リッター(開始直後22.5℃)
- kp 2.7
- ki 0.01
実験結果
縦軸:温度℃ 青:水温 赤:ターゲット温度
横軸:時間sec
考察
初回は生卵を温泉たまごにすることを目標とし、PI制御で実施した。ターゲット67℃に対して7℃のオーバーシュートが発生した。T-falの電熱器は熱板部分が広く、熱の伝わりが早いためか、ki値を大きく設定しなければオーバーシュートは防げないと思われる。オーバーシュートを防ぐと傾きが穏やかになる分加熱時間を伸ばす必要があるか。10/15追記、調査を行ったところ、立ち上がりのオーバーシュートを防ぎたいのであれば、D制御(微分)を追加して増やせば良さそうである。kp,kiのパラメータを変えるよりもD制御を実装した方が良いと判断できる。10/16追記、立ち上がりの温度上昇が早いため、立ち上がりの2分間ほどは細かく制御点を取得すると良いか?ともかくD制御を実装してみることにする。
所感:
食べ物としての評価としては申し分なかった。卵黄はムラなく固まり、卵白は固まらず。狙い通りに温泉たまごが出来上がった。醤油かけてウマー
/////////////
実験第二回 (ソフトウェア変更による影響を確認する※D制御のパラメータ確認)
- 日時:10/20 16:45より開始
- 場所:自宅7F 自室、木製机上
- 室温24℃ 湿度不明
- 電熱器:T-fal JUSTIN
- 投入物:なし。水のみで実施。
- 制御条件: PID制御
- 制御パラメータ:以下の通り
- ターゲット温度:67℃
- トータル制御時間1500sec(25分)
- 水1.1リッター(開始直後24.5℃)
- kp 2.7
- ki 0.01
- kd 1
実験結果
縦軸:温度℃ 青:水温 赤:ターゲット温度
横軸:時間sec
考察
D制御ロジックを入れた結果、オーバーシュートは5℃になった。他のパラメータは変えていない。D制御ロジックを入れると確かに急激な変化を抑える方向ではある。ただしオーバーシュートは現状発生している。これを防ぐためには2案考えられる。1.kd値を上げることで、急激な変化が発生した際に強く抑える形にする、2.kp値を下げることで、温度の立ち上がり部分において確実に出力を下げることを行う。
WikipediaのPID制御の項目を見ると、実験的に決めていくしかないとのこと。次回は1を変化させた上で牛肉で実施してみることとする。2はその結果を受けて必要・不必要を考慮すると良さそうだ。なお、2を実施するとパワー不足になり、いつまでもターゲット温度に到達しないオフセットが発生する可能性がある。2はどうしてもという場合にのみ実施したほうが良いものと考えられる。
ちなみに前回も今回もハンチングは発生していないので、系を不安定にするような積分項のゲインは増やさない。kiは0.01で次回も行う。
/////////////
実験第三回 (ステーキ肉を低温調理する(本番))
- 日時:10/20 17:57より開始
- 場所:自宅7F 自室、木製机上
- 室温24℃ 湿度不明
- 電熱器:T-fal JUSTIN
- 投入物:肉:近所のスーパーで購入、アメリカ産牛肩ロースステーキ用、正味量252グラム、塩コショウ、にんにくペーストを片面に塗り、真空調理(ジップロックにいれた)
- 制御条件: PID制御
- 制御パラメータ:以下の通り
- ターゲット温度:58℃ (ミディアムを意図)
- トータル制御時間3600sec(60分)
- 水1.5リッターほど。目盛りなく目分量にて計測(開始直後22.8℃)
- kp 2.7
- ki 0.01
- kd 3
実験結果
縦軸:温度℃ 青:水温 赤:ターゲット温度
横軸:時間sec
※1500sec以降非表示
考察
PID 制御を行い、kd値を3に設定したことで、温度の急な立ち上がりに対して抗う効果が働いた。大きな進歩として、オーバーシュートが発生しなかった(+0.125℃だが、温度計の分解能が±0.5℃のため)。ターゲット温度への到達時間も200秒ほどと以前との温度上昇ペースとほぼ変化なし。狙い通りkdを増やすことでターゲット温度-10℃程度からの温度上昇が緩やかになった。初回からそうだったが、積分項の影響はもともと少なくしていたのもあり、ターゲット温度を上下するようなハンチングも起きていない。理想的なPID制御による温度上昇、定温操作ができたのではないだろうか。
今回は投入した牛肉の大きさの都合により、水量が増えた。その点温度上昇レートにも影響したものと思われるが、今回は水量の増分による検証は行わない。
水量以外にも、ターゲット温度・外部温度によってもパラメータ設定は変わると考えられる。今後行いたい低音調理に応じて都度ターゲット温度に対するオーバーシュート、ハンチングの有無を確認することととし、今回の実験及び考察を終了する。
所感
調理結果について。低音調理の結果、食肉から赤い肉汁が出ていた。調査によるとミオグロビンによるもので、心配いらないとのこと。切開するとステーキのレアのような仕上がりになっていた。今回はミディアムを狙っていたが、思わぬことにレアのような状態になった。温度計、及び水温は58℃であったが、食肉はジップロックにくるまれているため、1-2℃ほど水温よりも低いという事態が起こっていたかもしれない。正確に温度変化を捉えるには食肉に別途温度計をつける必要があるかもしれないが、食肉にするため少し抵抗はある。おそらく不要だろう。同様の調理法であれば、ターゲット温度は1-2℃上げても良いかもしれない。
結局食肉と中の肉汁をフライパンに開け、30秒ほど焼き色をつけるために加熱した。肉汁は旨味の成分が含まれているかどうか不明だが、もしかしたら不要であったかもしれない。味は非常に良く、肉の旨味が凝縮されているようであった。最初から最後の一切れまで味のムラがなく、均一に楽しめたのも大きい。IoTで料理するプログラムを組んでしまえば再現性の高い料理を楽しめるため、最高の調理方法を求めて行うことができるだろう。あといい肉食べたい。
(11/12追記)大学の教授に尋ねたところ、ソフトウェアとしてPIDのパラメータを自動で決定してくれるものがあるらしい。学生がモノを考えなくなっちゃうよ~と嘆いていたのが印象的w