#概要
M5Stack Core2を使って低温調理器を作ってみました。
本記事は以前投稿した「M5Stackで低温調理器を作ってみた」で作成したモノの改良版です。
#完成物
ヒーター、撹拌用のポンプ、水温センサー、M5Stack Core2、リレーで構成されてます。
機器構成ですが、以前作ったやつのM5StackをM5Stack Core2に交換しただけです。以前の機器構成はこちらを参照ください。
#調理してみたもの
低温調理器といえばローストビーフ。温度や時間はBONIQのサイトを参考にさせて頂きました。
#機能
・水温を設定温度まで加熱し、一定に保つ
・タイマー機能。設定時間が経過したら音でお知らせすると共にLINEで通知する ← NEW
・温度設定、時間設定はM5Stack Core2のタッチパネルで可能 ← NEW
・温度変化をAmbientでグラフ表示可能 ← NEW
#LINE通知
調理完了時に音を出すようにしてますが、音だけだと聞き逃すこともあるかもしれないのでLINEに通知するようにしてみました。
LINEが提供しているLINE Notifyというサービスを使用しています。
サービスの登録方法、M5Stackへの実装方法は以下の記事を参考にさせて頂きました。
M5Stackから位置情報をambientに記録し、LINEに結果を通知してみた。
補足として、WiFiClientSecureの使い方がいつの間にやら変わっていたみたいで、次の一文が必要になってます。
WiFiClientSecure client;
client.setInsecure(); // ←この一文が必要
client.connect(host, 443);
参照元:Arduino core for the ESP32 1.0.5でWiFiClinetSecureの使い方が変更されて、HTTPS(TLS)接続前に証明書の検証方法の明示が必要になっていた
#Ambientによるグラフ表示
計測した水温をAmbientというクラウドサービスに上げてグラフ表示してみました。
ローストビーフを作ったときの温度は諸事情により取ってなかったので、このグラフは後日温泉卵を作ったときのAmbientグラフです。
このときは設定温度を65℃にしたのですが、グラフを見ると+1℃ほど加熱し過ぎてますね。
これは設定温度に達したときにヒーターを止めてもヒーターの余熱により熱し過ぎてしまうためですね。
(オーバーシュートと言うそうです。テレビでやってました。)
これを防ぐには設定温度に達する前に早めにヒーターをOFFしたり、こまめにヒーターのON/OFFを切り替えるなどして温度の上がり方を調整する必要があります。
今回はそこまでやってませんが、市販の低温調理器なんかはそのあたり実装しているんでしょうね。
サービスの登録方法、M5Stackへの実装方法は先ほどと同じ記事を参考にさせて頂きました。
M5Stackから位置情報をambientに記録し、LINEに結果を通知してみた。
とくに補足することもないので説明は割愛させて頂きます。
#ソースコード
開発環境:Arduino IDE 1.8.13
ボードはesp32 1.0.6を使用。この時点の最新版2.0.0ではコンパイルエラーが取れず断念。
画面のチラつき対策として画面描画を約1秒に1回に制限しています。
#include <OneWire.h>
#include <DS18B20.h>
#include <M5Core2.h>
#include <driver/i2s.h>
#include <AXP192.h>
#include "sound.h"
#include "StopWatch.h"
#include <WiFiClientSecure.h>
#include <ssl_client.h>
#include <HTTPClient.h>
#include "Ambient.h"
// ピンアサイン
#define ONE_WIRE_BUS 27 // 水温センサーピンアサイン
#define RELAY_PIN 32 // Core2ではGroveのピンアサインは32(黄)、33(白)
// スピーカー関連
#define CONFIG_I2S_BCK_PIN 12
#define CONFIG_I2S_LRCK_PIN 0
#define CONFIG_I2S_DATA_PIN 2
#define CONFIG_I2S_DATA_IN_PIN 34
#define Speak_I2S_NUMBER I2S_NUM_0
#define MODE_MIC 0
#define MODE_SPK 1
#define DATA_SIZE 1024
// 画面描画関連
int disp_draw_count = 0;
// 温度センサー関連
OneWire oneWire(ONE_WIRE_BUS);
DS18B20 sensor(&oneWire);
// 温度制御関連
float settei_temp = 60; // 設定温度
float saiKanetsu_temp = 59.8; // 再加熱温度
bool totatsu = false; // 設定温度に到達したか
bool relay = false; // リレーをONにしているかどうか
float temp = 0; // 現在温度
StopWatch sw(60 * 60); // ストップウォッチ
// バイブレーション関連
AXP192 power;
// ボタン関連
// Defines the buttons. Colors in format {bg, text, outline}
ButtonColors on_clrs = {BLUE, WHITE, WHITE};
ButtonColors off_clrs = {BLACK, WHITE, WHITE};
Button btn1(0, 170, 76, 40, false ,"Time +", off_clrs, on_clrs);
Button btn2(81, 170, 76, 40, false, "Time -", off_clrs, on_clrs);
Button btn3(162, 170, 76, 40, false, "Temp +", off_clrs, on_clrs);
Button btn4(243, 170, 76, 40, false, "Temp -", off_clrs, on_clrs);
// Wifi関連
const char* ssid = "*******";
const char* password = "*******";
// Ambient関連
WiFiClient client;
Ambient ambient;
unsigned int channelId = *******; // AmbientのチャネルID
const char* writeKey = "*******"; // ライトキー
int sendTimerCount = 0;
void vibration();
bool InitI2SSpeakOrMic(int);
void SpeakInit();
void DingDong();
void eventDisplay(Event&);
void task0(void*);
void line_notify(String);
void RefreshDisplay();
void setup(void)
{
M5.begin();
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setCursor(10, 10);
M5.Lcd.setTextColor(WHITE, BLACK);
M5.Lcd.setTextSize(1);
// Serial.begin(115200);
Serial.println(__FILE__);
Serial.print("DS18B20 Library version: ");
Serial.println(DS18B20_LIB_VERSION);
M5.Buttons.addHandler(eventDisplay, E_ALL - E_MOVE);
sensor.begin();
pinMode(RELAY_PIN, OUTPUT);// RELAY Pin setting
SpeakInit();
M5.Buttons.draw();
// Wifi接続
M5.Lcd.printf("Wifi connect.");
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while(WiFi.status() != WL_CONNECTED){
M5.Lcd.printf(".");
delay(500);
}
// Ambient接続
ambient.begin(channelId, writeKey, &client); // チャネルIDとライトキーを指定してAmbientの初期化
// 水温取得スレッド起動
xTaskCreatePinnedToCore(task0, "TempSensor", 4096, NULL, 1, NULL, 1);
M5.Lcd.printf("Wake up!");
delay(1000);
M5.Lcd.fillRect(0, 0, 320, 169, BLACK);
}
void loop(void)
{
M5.update();
// Aボタンでタイマースタート
if (M5.BtnA.wasPressed()) {
sw.Start();
Serial.println("Pushed Start.");
// 音鳴らす
vibration();
DingDong();
}
// Bボタンでタイマーストップ
else if (M5.BtnB.wasPressed())
{
sw.Stop();
Serial.println("Pushed Stop.");
vibration();
}
// Cボタンでタイマーリセット
else if (M5.BtnC.wasPressed())
{
sw.Reset();
Serial.println("Pushed Reset.");
vibration();
settei_temp -= 1;
saiKanetsu_temp = settei_temp - 0.2;
}
// 温度制御
bool kanetsu = false;
if (temp < saiKanetsu_temp)
{
totatsu = false;
kanetsu = true;
}
else if (temp < settei_temp)
{
if (totatsu)
{
kanetsu = false;
}
else
{
kanetsu = true;
}
}
else
{
totatsu = true;
kanetsu = false;
}
// リレー制御して加熱
if (kanetsu)
{
if (!relay)
{
digitalWrite(RELAY_PIN, HIGH);
Serial.println("HIGH");
relay = true;
}
}
else
{
if (relay)
{
digitalWrite(RELAY_PIN, LOW);
Serial.println("LOW");
relay = false;
}
}
// タイマー制御
if (sw.GetIsStart())
{
if (sw.GetRemainTime() <= 0)
{
// 出来上がり
sw.Stop();
// LINEに通知
line_notify("Dekiagari!!");
// 音鳴らしてお知らせ
DingDong();
DingDong();
DingDong();
DingDong();
DingDong();
}
}
// 画面描画
disp_draw_count++;
if (disp_draw_count > 100)
{
disp_draw_count = 0;
RefreshDisplay();
}
delay(10);
}
// バイブレーション
void vibration() {
power.SetLDOEnable(3, true); // 3番をtrueにしてバイブレーション開始
delay(150); // バイブレーションの長さ(ms)はお好みで調整
power.SetLDOEnable(3, false); // 3番をfalseにしてバイブレーション修了
}
// スピーカー初期化
bool InitI2SSpeakOrMic(int mode)
{
esp_err_t err = ESP_OK;
i2s_driver_uninstall(Speak_I2S_NUMBER);
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER),
.sample_rate = 44100,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // is fixed at 12bit, stereo, MSB
.channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT,
.communication_format = I2S_COMM_FORMAT_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 2,
.dma_buf_len = 128,
};
if (mode == MODE_MIC)
{
i2s_config.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM);
}
else
{
i2s_config.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX);
i2s_config.use_apll = false;
i2s_config.tx_desc_auto_clear = true;
}
err += i2s_driver_install(Speak_I2S_NUMBER, &i2s_config, 0, NULL);
i2s_pin_config_t tx_pin_config;
tx_pin_config.bck_io_num = CONFIG_I2S_BCK_PIN;
tx_pin_config.ws_io_num = CONFIG_I2S_LRCK_PIN;
tx_pin_config.data_out_num = CONFIG_I2S_DATA_PIN;
tx_pin_config.data_in_num = CONFIG_I2S_DATA_IN_PIN;
err += i2s_set_pin(Speak_I2S_NUMBER, &tx_pin_config);
err += i2s_set_clk(Speak_I2S_NUMBER, 44100, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO);
return true;
}
// スピーカー初期化
void SpeakInit(void)
{
M5.Axp.SetSpkEnable(true);
InitI2SSpeakOrMic(MODE_SPK);
}
// ステキな短い音を鳴らす
void DingDong(void)
{
size_t bytes_written = 0;
i2s_write(Speak_I2S_NUMBER, previewR, 120264, &bytes_written, portMAX_DELAY);
}
// ボタン処理
void eventDisplay(Event& e) {
Serial.printf("%-12s finger%d %-18s (%3d, %3d) --> (%3d, %3d) ",
e.typeName(), e.finger, e.objName(), e.from.x, e.from.y,
e.to.x, e.to.y);
Serial.printf("( dir %d deg, dist %d, %d ms )\n", e.direction(),
e.distance(), e.duration);
if (strcmp(e.typeName(),"E_TOUCH") == 0)
{
if (strcmp(e.objName(), "Time +") == 0)
{
sw.SetRemainTime(sw.GetRemainTime() + 1 * 60);
}
else if (strcmp(e.objName(), "Time -") == 0)
{
sw.SetRemainTime(sw.GetRemainTime() - 1 * 60);
}
else if (strcmp(e.objName(), "Temp +") == 0)
{
settei_temp += 1;
saiKanetsu_temp = settei_temp - 0.2;
}
else if (strcmp(e.objName(), "Temp -") == 0)
{
settei_temp -= 1;
saiKanetsu_temp = settei_temp - 0.2;
}
RefreshDisplay();
}
}
// 水温取得スレッド
void task0(void* arg) {
while (1) {
// 温度取得
sensor.requestTemperatures();
while (!sensor.isConversionComplete()); // wait until sensor is ready
temp = sensor.getTempC();
// 5秒間隔でAmbientに温度送信
sendTimerCount++;
if (sendTimerCount >= 5)
{
sendTimerCount = 0;
ambient.set(1, String(temp).c_str());
ambient.set(2, String(settei_temp).c_str());
ambient.set(3, String(relay ? 1 : 0).c_str());
ambient.send();
}
delay(1000);
}
}
// LINE通知
void line_notify(String msg) {
const char* host = "notify-api.line.me";
const char* token = "***********"; // ご自分のLINEトークンに書き換え
WiFiClientSecure client;
client.setInsecure();
Serial.println("Try");
//LineのAPIサーバに接続
if (!client.connect(host, 443)) {
Serial.println("Connection failed");
return;
}
Serial.println("Connected");
//リクエストを送信
String query = String("message=") + msg;
String request = String("") +
"POST /api/notify HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"Authorization: Bearer " + token + "\r\n" +
"Content-Length: " + String(query.length()) + "\r\n" +
"Content-Type: application/x-www-form-urlencoded\r\n\r\n" +
query + "\r\n";
client.print(request);
//受信終了まで待つ
while (client.connected()) {
String line = client.readStringUntil('\n');
Serial.println(line);
if (line == "\r") {
break;
}
}
String line = client.readStringUntil('\n');
Serial.println(line);
}
// 画面描画
void RefreshDisplay()
{
M5.Lcd.fillRect(0, 0, 320, 169, BLACK);
M5.Lcd.setTextColor(WHITE, TFT_BLACK);
// 現在温度表示
Serial.print("Temp: ");
Serial.println(temp);
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(10, 40);
M5.Lcd.printf("Temp:%.2f",temp);
// 設定温度表示
int rhour = 0;
int rmin = 0;
int rsec = 0;
// 残り時間計算
unsigned long remain = sw.GetRemainTime();
rhour = (int)(remain / 60 / 60);
rmin = (int)(remain % (60 * 60) / 60);
rsec = (int)(remain % 60);
M5.Lcd.setCursor(10, 90);
M5.Lcd.printf("%dh%dm%ds", rhour, rmin, rsec);
M5.Lcd.setTextSize(1);
M5.Lcd.setCursor(10, 130);
M5.Lcd.printf("Target temp:%.2f",settei_temp);
M5.Lcd.setCursor(10, 150);
M5.Lcd.printf("Re-heat temp:%.2f",saiKanetsu_temp);
M5.Lcd.setCursor(40, 230);
M5.Lcd.printf("v Start");
M5.Lcd.setCursor(150, 230);
M5.Lcd.printf("v Stop");
M5.Lcd.setCursor(250, 230);
M5.Lcd.printf("v Reset");
if (relay)
{
M5.Lcd.setTextSize(2);
M5.Lcd.setTextColor(RED);
M5.Lcd.setCursor(200, 150);
M5.Lcd.printf("Heat");
}
}
#ifndef _STOPWATCH_H_
#define _STOPWATCH_H_
class StopWatch{
public:
StopWatch(unsigned long);
void Start();
void Stop();
void Reset();
unsigned long GetRemainTime();
unsigned long GetSetteiTime();
void SetRemainTime(unsigned long);
void SetSetteiTime(unsigned long);
bool GetIsStart();
private:
unsigned long targetTime;
unsigned long setteiTime;
bool isStart;
unsigned long remainTime;
};
#endif
#include <M5Core2.h>
#include "StopWatch.h"
StopWatch::StopWatch(unsigned long t)
{
setteiTime = t;
remainTime = t;
isStart = false;
}
void StopWatch::Start()
{
targetTime = remainTime + millis() / 1000;
isStart = true;
}
void StopWatch::Stop()
{
remainTime = GetRemainTime();
isStart = false;
}
void StopWatch::Reset()
{
isStart = false;
remainTime = setteiTime;
}
unsigned long StopWatch::GetRemainTime()
{
if (!isStart)
{
return remainTime;
}
unsigned long currentT = millis() / 1000;
if (currentT >= targetTime)
{
remainTime = 0;
}
else
{
remainTime = targetTime - currentT;
}
return remainTime;
}
unsigned long StopWatch::GetSetteiTime()
{
return setteiTime;
}
void StopWatch::SetRemainTime(unsigned long t)
{
remainTime = t;
}
void StopWatch::SetSetteiTime(unsigned long t)
{
setteiTime = t;
}
bool StopWatch::GetIsStart()
{
return isStart;
}
音データが入っているsound.hはこちらから拾ってきました。data.cというファイルをsound.hにリネームして使ってます。この記事で全文載せようとすると上手く表示できなかったので、リンクでご勘弁ください。