zplt.net / ブログ
ESP32で4:6メソッドのコーヒー計算機を作った話
こんにちは、zpltです。
Seeed Studio XIAO ESP32C3を使って、コーヒーの4:6メソッド用の計算機を作ってみた話です。お湯の量を選ぶと、豆の量と、何回・何gずつ・いつ注げばいいかを計算して、45秒ごとにブザーで教えてくれるといったものです。最初はArduino Mega 2560 R3で試作して、そのあとESP32に載せ替えました。
やりたかったこと
前にV60レシピ(1人分)の記事を書きましたが、4:6メソッドで淹れるのも好きです。
4:6メソッドは、粕谷哲さんが考案した淹れ方です。お湯を「前半の40%」と「後半の60%」に分けて、前半の注ぎ方で味を、後半の注ぎ方で濃さを決めます。考え方はシンプルなのですが、お湯の量を変えるたびに「1回目は何gで、スケールが何gになるまで注ぐんだっけ」と計算するのが少し面倒でした。
そこで、計算とタイマーを小さなマイコンにまかせることにしました。
4:6メソッドについて
作る前に、4:6メソッドが本当に合っているかを調べ直しました。基本はこんな感じです。
| 項目 | 内容 |
|---|---|
| 豆とお湯の比率 | 1:15(豆20gならお湯300g) |
| 前半40% | 2回に分けて注ぐ。1回目を少なくすると甘く、多くすると酸味が出て明るい味になる |
| 後半60% | 1〜3回に分けて注ぐ。回数が多いほど濃くなる |
| 注ぐ間隔 | 45秒ごと |
| 終わり | 3:30でドリッパーを外す |
基本のレシピは「60gずつ5回」で、前半の120gを2回、後半の180gを3回に分けて注ぎます。1投目は蒸らしも兼ねています。
解説のサイトによっては、味や濃さの向きが逆に書かれているものもありました。今回は、粕谷さんの説明と多くの解説に合わせています。
Megaで試作して、ESP32に載せ替えた
最初は、手元にあったArduino Mega 2560 R3とELEGOOのスターターキットの部品で作りました。画面はLCD1602(16文字×2行の液晶)。
ひととおり動いたのですが、キッチンで電池で使うことを考えると、Megaは基板が大きく、USBの変換チップなど、プログラムを止めても電気を使い続ける部品も載っています。そこで、XIAO ESP32C3に載せ替えることにしました。
| Mega版(試作) | ESP32版 | |
|---|---|---|
| マイコン | Arduino Mega 2560 R3 | Seeed Studio XIAO ESP32C3 |
| 画面 | LCD1602 | OLED 0.96インチ |
| 音 | アクティブブザー | パッシブブザー |
| 節電 | バックライトを消す | ディープスリープで眠らせる |
| Wi-Fi | なし | 時計合わせと天気に使う |
XIAO ESP32C3は親指くらいの大きさで、リポバッテリーの充電回路も付いています。
使ったもの
| 項目 | 内容 |
|---|---|
| マイコン | Seeed Studio XIAO ESP32C3 |
| 画面 | OLED 0.96インチ(SSD1306、128×64ドット、I2C) |
| 操作 | ロータリーエンコーダ(KY-040) |
| 音 | パッシブブザー |
| その他 | 10kΩの抵抗、ブレッドボード、ジャンパーワイヤ |
| 電源 | モバイルバッテリー |
操作
操作は、ロータリーエンコーダ1つだけです。つまみを回すと「カチカチ」と1クリックずつ値が変わって、押し込むとボタンにもなる部品です。
| 操作 | 動き |
|---|---|
| 回す | お湯の量・比率・味・濃さを変える |
| 押す | 決定、タイマーのスタート |
| 長押し | 1つ前の画面に戻る |
| 抽出中に回す | 早送り・巻き戻し(1クリック5秒) |
| 抽出中に押す | 中止 |
| 3秒押し続ける | 電源オフ(もう一度押すと電源オン) |
配線
配線は以下の通りです。
| 部品 | つなぎ方 |
|---|---|
| OLED | VCC→3V3、GND→GND、SDA→D4、SCL→D5 |
| ロータリーエンコーダ | CLK→D1、DT→D2、SW→D3、+→3V3、GND→GND |
| 10kΩの抵抗 | SW(D3)と3V3の間 |
| パッシブブザー | +→D10、−→GND |
ESP32は3.3Vで動くので、エンコーダの「+」もOLEDも3V3につなぎます。
10kΩの抵抗は、眠っているときにつまみを押して、確実に起きるようにするためのものです。ESP32C3で眠りから起こす合図に使えるのはGPIO0〜5だけなので、押すボタン(SW)はD3(GPIO5)にしています。
画面
OLEDで使用したのは、U8g2というライブラリの日本語フォントで、漢字も表示できます。
湯量 ☀ 18℃ 8:15
300g
豆 20.0g
抽出中は、真ん中にスケールの目標を大きく出して、次の注湯までの残り時間をバーで出しています。
2/5回目 00:52
120g
+60g [■■■■■■■□□□□]
できあがると、湯気がゆらゆら上がるコーヒーカップのアニメーションが出ます。
プログラム
プログラムはArduino IDEで書きました。やっていることは、こんな流れになっています。
- お湯の量(150〜900g)を選ぶと、豆の量を計算して表示する
- 比率(1:13〜1:17)、味、濃さを選ぶ
- 何回目に何gずつ注ぐかを計算して、計画を表示する
- スタートしたら、45秒ごとにブザーで知らせて、注ぐ量とスケールの目標を表示する
- 3:30になったら、ドリッパーを外すように知らせる
- しばらくさわらなければ、眠る
注ぐ量を計算する
前半の40%を味の設定で2回に分け、後半の60%を濃さの設定で1〜3回に分けます。スケールを見ながら注ぐので、1回ごとの量だけでなく「スケールが何gになるまで注ぐか」(累計)も出しています。
// 前半40%のうち1投目が占める割合(甘め 5:7 / 標準 6:6 / 酸味 7:5)
const float FIRST_POUR_RATIO[] = {5.0 / 12.0, 0.5, 7.0 / 12.0};
void calcPlan() {
float first40 = water * 0.4;
float p[MAX_POURS];
p[0] = first40 * FIRST_POUR_RATIO[taste];
p[1] = first40 - p[0];
byte laterPours = strength + 1; // 後半は1〜3回
float each = water * 0.6 / laterPours;
pourCount = 2 + laterPours;
for (byte i = 2; i < pourCount; i++) p[i] = each;
// 累計値を丸めてから差分を取り、合計がずれないようにする
float cum = 0;
int prev = 0;
for (byte i = 0; i < pourCount; i++) {
cum += p[i];
pourTotal[i] = (int)(cum + 0.5);
pourAmount[i] = pourTotal[i] - prev;
prev = pourTotal[i];
}
}
エンコーダを読む
エンコーダは、CLKとDTの2本の信号が、回す向きによって少しずれて変化します。2本の「前の状態」と「今の状態」の組み合わせを表にしておいて、そこから右回りか左回りかを判断しています。
DRAM_ATTR static const int8_t ENC_TABLE[16] = {0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0};
void IRAM_ATTR encoderISR() {
portENTER_CRITICAL_ISR(&encoderMux);
encoderState = ((encoderState << 2) | (digitalRead(ENC_CLK_PIN) << 1) | digitalRead(ENC_DT_PIN)) & 0x0F;
encoderSteps += ENC_TABLE[encoderState];
portEXIT_CRITICAL_ISR(&encoderMux);
}
ESP32では、割り込みの関数にIRAM_ATTRを付けて、表もRAMに置く(DRAM_ATTR)ようにしています。また、手元のエンコーダは1クリックで信号が2回変わるタイプだったので、2回分たまったら「1クリック回した」と数えるようにしています。
音
パッシブブザーは、PWMで鳴らす周波数が音の高さ、ONの時間の割合(デューティ比)が音量になります。音の高さと長さを並べておいて、場面ごとにメロディを鳴らすようにしました。
const Note SND_START[] = {{N_C6, 90}, {N_E6, 90}, {N_G6, 90}, {N_C7, 220}}; // 抽出スタート(ドミソド)
const Note SND_POUR[] = {{N_G6, 90}, {REST, 30}, {N_C7, 220}}; // 2投目から(ソ・ド)
void toneOn(uint16_t freq) {
ledcChangeFrequency(BUZZER_PIN, freq, BUZZER_RESOLUTION);
ledcWrite(BUZZER_PIN, (1 << (BUZZER_RESOLUTION - 1)) * VOLUME / 100);
}
次の注湯の3秒前からは、だんだん高くなる「ド・レ・ミ」でカウントダウンします。つまみを回したときだけは、「ピッ」にしています。
ディープスリープ
抽出中以外で3分間さわらないと、画面を消してディープスリープに入ります。つまみを押すと起きます。
void goToSleep() {
u8g2.setPowerSave(1); // 画面を消す
esp_deep_sleep_enable_gpio_wakeup(1ULL << WAKE_GPIO, ESP_GPIO_WAKEUP_GPIO_LOW); // つまみを押すと起きる
esp_deep_sleep_start();
}
ディープスリープから起きると、プログラムは最初から始まります。選んでいたお湯の量などは、RTC_DATA_ATTRを付けた変数に入れておくと、眠っている間も残っていました。
最初は1分で眠るようにしていたのですが、豆を挽いている間に消えてしまったので、3分にしました。
時計と天気
Wi-Fiは、時計合わせと天気にだけ使っています。電源を入れたときに家のWi-Fiにつないで時計を合わせ、Open-Meteoという無料のサービスから天気と気温を取って、すぐに切ります。眠っている間も時計は進み続けるので、合わせるのは電源を入れたときだけです。天気は、起きたときに前回から1時間以上たっていれば取り直します。
天気はアイコンで出しています。晴れ・くもり・雨・雪・雷・霧の6種類を、12×12ドットで描いています。
試作品
以下は試作品です。
