この記事はVASILY DEVELOPERS BLOGにも同じ内容で投稿しています。よろしければ他の記事もご覧ください。
あけましておめでとうございます。
バックエンドエンジニアの塩崎です。
今年の抱負として「テクノロジー系の同人誌を書く!」と言ったら、「アニメの女の子が出てくるエッチな漫画」のことだと勘違いされてしまいました。
新年会用に低温料理器具を作った話を紹介します。
はじめに
今年のVASILYの新年会は「各地の温かいもの」を持ち寄るという企画を行いました。
しかし、僕は実家に帰らずにアキバ近辺をうろうろしていました。
アキバで温かいもの言ったら、「おでん缶」か「アニメ店長」くらいしか思いつかないため、温かいもの探しに困っていました。
そんな時に、秋月電子でいいものを見つけました。
これです。
アキバ名物(?)メタルクラッド抵抗です。
これに電流を流せばジュール熱が発生するので、お土産の要件は満たしています。
さらに、ボディがアルミ製なので、熱伝導率もバツグンです。
何を作るか
この抵抗は人間が食べることができないので、抵抗を使って何か食べられるものを作る必要があります。
お題が「温かいもの」なので、加熱調理を行う調理器具を作ることにしました。
とはいえ、ただ加熱するだけですとティファールの下位互換品です。
なので、この前amazonで調べてみて目玉が飛び出るほど高かった低温調理器具を自作します。
低温調理とは
低温調理とは、タンパク質が変性・凝固する温度である55°Cから68°Cを下回る温度で長時間(1時間〜)加熱することで、肉や魚全体を均一に加熱する調理方法です。
肉が硬くなってしまう温度以下で加熱するため、ぱさぱさにならず調理が可能です。
この温度を長時間維持することを手動で行うのは厳しいため、専用の調理器具が市販されています。
もしくは簡易的に魔法瓶のような断熱性の良い容器に食材を入れて余熱で火を通すということも行われています。
システム全体図
以下にシステムの全体構成図を示します。
また、以下に動作中の写真を示します。
水を入れた鍋の底にメタルクラッド抵抗を配置して鍋を温めます。
湯温は温度センサーによってリアルタイムに監視され、Micro Controller Unit(MCU)が温度を元に半導体スイッチのON-OFFをし、フィードバック制御を行います。
また、温度ログはMCUからPCにWiFiを経由して送られ、温度の時系列的な変化のデータをブラウザで確認できます。
鍋の中に入っている黄色い部品は攪拌用の水中モーターです。
ちなみに、制御基板にはトランスが搭載されていますが、これはAC電源のゼロクロス検知ができたらいいなと思ってつけたはいいが企画倒れしたものです。
無駄に皮相電力を大きくさせているだけの部品です。
ハードウェア部分
まずは、ハードウェア部分の部品選定についてお話しします。
Micro Controller Unit(MCU)
まずは一番重要な部品であるMCUです。
MCUの選定には以下のことを重視しました。
- CPU、メモリ、IOがワンチップに収まっている
- WiFiで接続可能
- リアルタイム制御が可能
- 温度センサーとのインターフェースを備えている(SPI、I2C、ADC、etc.)
- 小型
- 安価
現在発売されているマイコンでこれらを満たすものとして、ESP-WROOM-02Cを選択しました。
わずか数百円のボードの中にWiFi機能が詰まっています。
さらに、SPI、I2C、ADC機能もそれぞれ1chずつ搭載されています。
プログラムの書き込みはArduino SDKで行うことができます。
これの他に、Arduino、Raspberry Pi、Intel Edisonなども検討しましたが、どれもESP-WROOM-02Cの小型さ、安価さには敵いませんでした。
ヒーター
100Ω 50Wのメタルクラッド抵抗4つを直並列に接続して、100Ω 200Wの抵抗にしました。
ここにAC 100Vを供給し発熱をさせるため、この抵抗での商品電力は100W(実効値)です。
メタルクラッド抵抗と鍋底との間の接着には熱伝導性の良いシリコン接着剤を使用しました。
温度センサー
温度センサーにはADT7310を使用しました。
MCUとのインターフェースがSPIなためESP-WROOM-02Cで簡単に読み出すことができます。
また、温度校正が不要なため使用するのが非常に楽です。
この温度センサーを温水中に投入するため、ホットボンドで全体をモールドしました。
耐水性試験、耐熱性試験は行っていませんが、今の所は壊れていないので、とりあえずはこれで良しとします。
半導体スイッチ
AC 100VのON OFF制御を行いために、トライアックを利用します。
今回はスナバレスタイプを使用し、また、誘導性負荷の制御も行わないため、バリスタは省略しました。
MCUからの制御信号と高電系の間はフォトトライアックで絶縁しました。
フォトトライアックを使用することで、AC 100Vが正極性の時でも負極性の時でもトライアックをONすることができます。
ソフトウェア部分
MCU側
温度センサーから温度を読んでヒーターのON OFF制御を行う部分はミッションクリティカルなため、MCU側に実装しました。
これはWiFi接続が不安定になってしまってもスタンドアローンで動作できるようにするためです。
温度制御
温度センサーからを現在の温度を読み、目標温度以上か以下かによって、ヒーターのON OFFを行います。
void control_heater() {
if(temperature > target_temperature) {
heater_off();
} else {
heater_on();
}
}
この処理を1秒ごとに処理するためにTickerモジュールを利用しました。
Tickerモジュールは一定時間ごとにメインループとは別の処理を同時に行うためのモジュールで、裏ではタイマ割り込みが使われています。
温度ログ
温度ログの保存にはmilkcocoaを使用しました。
milkcocoaはIoT用のBaaSでシンプルなPubSub APIをそなえています。
milkcocoa以外のBaaSとしてFirebaseやPubNubも検討しましたが、APIのシンプルさと日本語の情報量の豊富さからmilkcocoaを選定しました。
以下のようなコードでmilkcocoaに温度ログをPublishします。
#include <ESP8266WiFi.h>
#include <Milkcocoa.h>
#define MILKCOCOA_APP_ID "XXXXXXXX"
#define MILKCOCOA_SERVERPORT 1883
WiFiClient client;
const char MQTT_SERVER[] PROGMEM = MILKCOCOA_APP_ID ".mlkcca.com";
const char MQTT_CLIENTID[] PROGMEM = __TIME__ MILKCOCOA_APP_ID;
Milkcocoa milkcocoa = Milkcocoa(&client, MQTT_SERVER, MILKCOCOA_SERVERPORT, MILKCOCOA_APP_ID, MQTT_CLIENTID);
void milkcocoa_data_push()
{
DataElement elem = DataElement();
elem.setValue("temperature", temperature);
elem.setValue("temperature_target", temperature_target);
milkcocoa.push("nabe", &elem);
}
この処理もTickerに登録し、5秒ごとに温度データの送信を行います。
目標温度設定
目標温度の設定部分にもmilkcocoaを使用しました。
先ほどの温度ログの送信機能とはデータの流れが逆で、MCU側がSubscribeします。
目標温度が更新された時のコールバック関数を登録しておきます。
void control_param_changed(DataElement *elem) {
float target_val = elem->getFloat("temperature_target");
// 小数部がない数値を送るとint型にされてしまうため
if(target_val == 0.0) {
target_val = elem->getInt("temperature_target");
}
temperature_target = target_val;
}
void setup() {
milkcocoa.on("nabe_control", "push", control_param_changed);
}
HTTPサーバー
MCUにHTTPサーバーを立てて、温度ログを表示するための画面のホスティングを行います。
ESP-WROOM-02CのSDKにはHTTPサーバーのサンプルがあるため、それを利用しました。
#include <ESP8266WebServer.h>
ESP8266WebServer server(80);
const char* index_html() {
return "indexの内容をここに書く";
}
void setup() {
server.begin();
server.on("/", [](){
server.send(200, "text/html", index_html());
});
}
mDNSサーバー
このままですと、このサーバーにアクセスするためにはIPアドレスを直に指定する必要があり面倒なので、mDNSサーバーもMCUにたてます。
nabe.local
のドメイン名が自分自身に名前解決されるようにします。
#include <ESP8266mDNS.h>
void setup() {
MDNS.begin("nabe");
}
PC側
温度ログのグラフ
ブラウザでのグラフの表示にはHighchartsを使用しました。
簡単にリッチなグラフを書くことができ、非商用ならば無料で使用することができます。
ページのロード時にmilkcocoaのAPIからデータを取得し、グラフにプロットします。
Highcharts.setOptions({
global: {
useUTC: false
}
});
var milkcocoa = new MilkCocoa('XXXXX.mlkcca.com');
var nabe_datastore = milkcocoa.dataStore('nabe');
function initial_draw(data_set) {
var temperature_nabe = data_set.map(function(d) {
return [d.datetime.getTime(), d.value.temperature];
});
var temperature_target = data_set.map(function(d) {
return [d.datetime.getTime(), d.value.temperature_target];
});
myChart = Highcharts.chart('container', {
chart: {
type: 'spline'
},
animation: Highcharts.svg,
title: {
text: '鍋温度モニター'
},
xAxis: {
type: 'datetime',
title: {
text: 'time'
}
},
yAxis: {
title: {
text: 'temperature (°C)'
}
},
tooltip: {
headerFormat: '<b>{series.name}</b><br>',
pointFormat: '{point.x:%H:%M:%S}: {point.y:.2f} ℃'
},
legend: {
layout: 'vertical',
align: 'right',
verticalAlign: 'middle',
borderWidth: 0
},
series: [
{
name: '鍋温度',
data: temperature_nabe
},
{
name: '目標温度',
data: temperature_target
}
]
});
}
nabe_datastore.stream().size(300).next(function(err, data) {
var data_set = data.map(function(datum) {
return {
datetime: new Date(datum.timestamp),
value: datum.value
};
}).filter(function(datum) { // 古すぎるデータは表示しない
var diff = new Date().getTime() - datum.datetime.getTime();
return diff < 3 * 60 * 60 * 1000;
});
initial_draw(data_set);
}
また、新たなデータが追加された時にはそのイベントをmilkcocoaから取得して、グラフの更新を行います。
nabe_datastore.on('push', function(pushed) {
var datetime = new Date(pushed.timestamp).getTime();
var temperature = pushed.value.temperature;
var temperature_target = pushed.value.temperature_target;
myChart.series[0].addPoint([datetime, temperature], true, true);
myChart.series[1].addPoint([datetime, temperature_target], true, true);
});
また、このページをホスティングしているMCUは貧弱なため、HTTPリクエスト数はなるべく減らしたいです。
そのため、jsのファイル分離をせずに、1つのHTMLファイル内に全ての処理を書きました。
目標温度の設定
目標温度の設定は以下のコードで行いました。
milkcocoaのAPIがシンプルなおかげでとても簡潔に書くことができました。
function set_target_tamperature() {
var temperature_target = parseFloat(document.nabe_controll.temperature_target.value);
milkcocoa.dataStore('nabe_control').push({
temperature_target: temperature_target
});
}
<form name="nabe_controll" id="nabe_controll" action="">
<input type="number" step="0.1" min="50.0" max="60.0" name="tamperature_target" id="temperature_target" value="55.0"/>
<input type="button" value="目標温度設定" onclick="set_target_tamperature();"/>
</form>
調理してみた結果
とりあえず、牛肉のステーキを作ってみます。
牛肉の両面にクレイジーソルトをまぶした後に、ジップロックに入れます。
調理中に肉からドリップが出てくるので少し多めにまぶすといいです。
ジップロックの中に空気が入ってしまうと熱の通りが悪くなってしまうため、水中に沈めながら空気抜きを行いました。
あとはこれを鍋の中に入れて加熱すればいいだけです。
加熱時間はこちらのサイトを参考にしました。
http://www.douglasbaldwin.com/sous-vide.html
肉の厚みが25mmだったため、55 °Cで2時間45分加熱しました。
加熱が終わった後の牛肉は全体的に茶色になっています。
表面の殺菌と香りづけを兼ねて、表面に焦げ目をつけます。
先ほどの低温調理で中まで十分に加熱されているため、軽い焦げ目がついたらOKです。
表面だけに焦げ目をつけるため、内側はまだ赤いままのミディアムレアな焼き加減です。
食べてみると肉の内側までとても柔らかくジューシーでした。
また、サケの香草焼きも同様に作ってみました。
サケの厚みが20mmだったため、60 °Cで60分間加熱しました。
ぱさぱさになりがちなサケを中までジューシーに調理することができました。
改善点
今回、低温調理器具を作ってみて、温度が振動する問題とmilkcocoaが不安定な問題がありました。
温度が振動する
温度が完全には一定にならず、±0.5°C程度の範囲で振動することがありました。
これは制御対象の系に位相遅れを引き起こすような成分が含まれているためです。
この程度の温度変化であれば実用上は問題ないです。
もっと作り込んで、この振動を消すのならば、PID制御を行うことが考えられます。
milkcocoaが不安定
開発中に何回かmilkcocoaが不安定になることがありました。
詳しく調べてみると、milkcocoaのサーバーが500番台のエラーを返していることがわかりました。
今回は簡単にプロトタイプを作る目的でmilkcocoaを採用しましたが、もっとしっかり作るのならばFirebaseなどのBaaSに乗り換えもありだと思います。
まとめ
今年の抱負は「テクノロジー系の同人誌を書く」なので、この鍋の完成度を上げて製作記を同人誌にするかもしれません。