LoginSignup
7
7

More than 1 year has passed since last update.

M5Stack Core2で低温調理器を作った

Last updated at Posted at 2021-11-01

概要

M5Stack Core2を使って低温調理器を作ってみました。

本記事は以前投稿した「M5Stackで低温調理器を作ってみた」で作成したモノの改良版です。

完成物

ヒーター、撹拌用のポンプ、水温センサー、M5Stack Core2、リレーで構成されてます。
DSC_1945.JPG
DSC_1141_1.jpg
機器構成ですが、以前作ったやつのM5StackをM5Stack Core2に交換しただけです。以前の機器構成はこちらを参照ください。

調理してみたもの

低温調理器といえばローストビーフ。温度や時間はBONIQのサイトを参考にさせて頂きました。
DSC_1144.JPG
DSC_1154_1.jpg

機能

・水温を設定温度まで加熱し、一定に保つ
・タイマー機能。設定時間が経過したら音でお知らせすると共にLINEで通知する ← NEW
・温度設定、時間設定はM5Stack Core2のタッチパネルで可能 ← NEW
・温度変化をAmbientでグラフ表示可能 ← NEW

LINE通知

調理完了時に音を出すようにしてますが、音だけだと聞き逃すこともあるかもしれないのでLINEに通知するようにしてみました。
Screenshot_20211028-165031.png

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.png
ローストビーフを作ったときの温度は諸事情により取ってなかったので、このグラフは後日温泉卵を作ったときのAmbientグラフです。
このときは設定温度を65℃にしたのですが、グラフを見ると+1℃ほど加熱し過ぎてますね。
これは設定温度に達したときにヒーターを止めてもヒーターの余熱により熱し過ぎてしまうためですね。
(オーバーシュートと言うそうです。テレビでやってました。)
これを防ぐには設定温度に達する前に早めにヒーターをOFFしたり、こまめにヒーターのON/OFFを切り替えるなどして温度の上がり方を調整する必要があります。
今回はそこまでやってませんが、市販の低温調理器なんかはそのあたり実装しているんでしょうね。

サービスの登録方法、M5Stackへの実装方法は先ほどと同じ記事を参考にさせて頂きました。

M5Stackから位置情報をambientに記録し、LINEに結果を通知してみた。

とくに補足することもないので説明は割愛させて頂きます。

ソースコード

開発環境:Arduino IDE 1.8.13
ボードはesp32 1.0.6を使用。この時点の最新版2.0.0ではコンパイルエラーが取れず断念。
画面のチラつき対策として画面描画を約1秒に1回に制限しています。

main.ino
#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");
  }
}
StopWatch.h
#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
StopWatch.cpp
#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にリネームして使ってます。この記事で全文載せようとすると上手く表示できなかったので、リンクでご勘弁ください。

7
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7