Walter Heger
Published © GPL3+

Alexa, Tell My Steak to Be Medium Rare

A smart home sous-vide cooker with Sony Spresense.

BeginnerShowcase (no instructions)20 hours798
Alexa, Tell My Steak to Be Medium Rare

Things used in this project

Hardware components

Spresense boards (main & extension)
Sony Spresense boards (main & extension)
×1
RGB Backlight LCD - 16x2
Adafruit RGB Backlight LCD - 16x2
×1
WS401 Temperature Chip
×1
433MHz Sender & Receiver
×1
Immersion Coil
×1
Remote Power Socket
×1
Button
×313
Jumper wires (generic)
Jumper wires (generic)
×1
ESP8266 ESP-12E
Espressif ESP8266 ESP-12E
×1
Speaker Chinch Audio
×1
Micro SD Card
×1

Hand tools and fabrication machines

Wood

Story

Read more

Schematics

Circuit

Code

Configuration.h

C/C++
#ifndef CONFIGURATION_H
#define CONFIGURATION_H

// --- general ---
#define SERIAL_BAUD 115200

// --- 433MHz ---
#define RC_SND_PIN  5
#define RC_POWER_ON    "010001000001110100000011"
#define RC_POWER_OFF   "010001000001110100001100"

// --- Thermistor ---
#define THERM_VCC_PIN  6
#define THERM_ARD_PIN  A0
#define THERM_CNT_RD   3
#define THERM_SHE_A    3.49004e-03
#define THERM_SHE_B    1.38558e-04
#define THERM_SHE_C   -1.94967e-06

#endif

// Free Text to mp3
// https://www.texttomp3.online/

RcHandler.cpp

C/C++
#include <RCSwitch.h>
#include "Configuration.h"
#include "RcHandler.h"

RCSwitch *rcSwitch = NULL;

void RcHandler::setup(byte sndPin, byte rcvPin, int protocol, int pulseLength) {
  Serial.print(F("[433Mhz] Initialize switch... "));
  rcSwitch = new RCSwitch();
  rcSwitch->enableTransmit(sndPin);
  rcSwitch->enableReceive(rcvPin);
  rcSwitch->setProtocol(protocol);
  rcSwitch->setPulseLength(pulseLength);
  Serial.println(F("done"));
}

void RcHandler::powerOn() {
  noInterrupts();
  Serial.println(F("[433Mhz] Power On"));
  rcSwitch->send(RC_POWER_ON);
  interrupts();
}

void RcHandler::powerOff() {
  noInterrupts();
  Serial.println(F("[433Mhz] Power Off"));
  rcSwitch->send(RC_POWER_OFF);
  interrupts();
}

void RcHandler::loop() {
  if (rcSwitch->available()) {
    noInterrupts();
    Serial.println(F("[433MHz] >> Signal received:"));
    output(
      rcSwitch->getReceivedValue(),
      rcSwitch->getReceivedBitlength(),
      rcSwitch->getReceivedDelay(),
      rcSwitch->getReceivedRawdata(),
      rcSwitch->getReceivedProtocol());
    rcSwitch->resetAvailable();
    Serial.println(F("[433MHz] << Signal received"));
    interrupts();
  }
}

void RcHandler::output(unsigned long decimal, unsigned int length, unsigned int delay, unsigned int* raw, unsigned int protocol) {

  const char* b = dec2binWzerofill(decimal, length);
  Serial.print(F("Decimal: "));
  Serial.print(decimal);
  Serial.print(F(" ("));
  Serial.print(length);
  Serial.print(F("Bit) Binary: "));
  Serial.print(b);
  Serial.print(F(" Tri-State: "));
  Serial.print(bin2tristate( b));
  Serial.print(F(" PulseLength: "));
  Serial.print(delay);
  Serial.print(F(" microseconds"));
  Serial.print(F(" Protocol: "));
  Serial.println(protocol);

  Serial.print(F("Raw data: "));
  for (unsigned int i = 0; i <= length * 2; i++) {
    Serial.print(raw[i]);
    Serial.print(F(","));
  }
  Serial.println();
}

char* RcHandler::bin2tristate(const char* bin) {
  static char returnValue[50];
  int pos = 0;
  int pos2 = 0;
  while (bin[pos] != '\0' && bin[pos + 1] != '\0') {
    if (bin[pos] == '0' && bin[pos + 1] == '0') {
      returnValue[pos2] = '0';
    } else if (bin[pos] == '1' && bin[pos + 1] == '1') {
      returnValue[pos2] = '1';
    } else if (bin[pos] == '0' && bin[pos + 1] == '1') {
      returnValue[pos2] = 'F';
    } else {
      return "not applicable";
    }
    pos = pos + 2;
    pos2++;
  }
  returnValue[pos2] = '\0';
  return returnValue;
}

char* RcHandler::dec2binWzerofill(unsigned long Dec, unsigned int bitLength) {
  static char bin[64];
  unsigned int i = 0;

  while (Dec > 0) {
    bin[32 + i++] = ((Dec & 1) > 0) ? '1' : '0';
    Dec = Dec >> 1;
  }

  for (unsigned int j = 0; j < bitLength; j++) {
    if (j >= bitLength - i) {
      bin[j] = bin[ 31 + i - (j - (bitLength - i)) ];
    } else {
      bin[j] = '0';
    }
  }
  bin[bitLength] = '\0';

  return bin;
}

RcHandler.h

C/C++
#ifndef RCHANDLER_H
#define RCHANDLER_H

#include <Arduino.h>

// 433MHz device
class RcHandler {
public:
  static void setup(byte sndPin, byte rcvPin, int protocol = 1, int pulseLength = 185); // pin 16 (D0)
  static void loop();
  static void output(unsigned long decimal, unsigned int length, unsigned int delay, unsigned int* raw, unsigned int protocol);
  static char* bin2tristate(const char* bin);
  static char* dec2binWzerofill(unsigned long Dec, unsigned int bitLength);

  static void powerOn();
  static void powerOff();
};
#endif

src.ino

C/C++
#include <Audio.h>
#include <LiquidCrystal.h>
#include <OneButton.h>
#include <SDHCI.h>
#include "Configuration.h"
#include "RcHandler.h"
#include "Thermistor.h"

float targetTemperature = 27.0; // in degree celsius
int targetDuration = 120; // in seconds

float currentTemperature;
unsigned long totalDuration, currentMillis, startMillis, lastDecision;
int decisionInterval = 5000;
bool finished;
volatile int currentState = 0;
char tMin[3], tSec[2], cMin[3], cSec[2];

LiquidCrystal lcd(7, 8, 9, 10, 11, 12);
OneButton button1(2, true);
OneButton button2(3, true);
OneButton button3(4, true);

void isrButton1();
void isrButton2();
void isrButton3();
void click1();
void click2();
void click3();
void longPress1();
void longPress2();
void tickButtons();
void delayWithTicks(int delayMilli);

static void audioErrorCallback(const ErrorAttentionParam *atprm);
bool initializeSound();

SDClass theSD;
AudioClass *theAudio;
File soundFileFinished;
File soundFileStart;
bool useSound, soundFinished;

void setup() {
  Serial.begin(SERIAL_BAUD);
  while (!Serial);
  RcHandler::setup(RC_SND_PIN, 6);

  lcd.begin(16, 2);
  lcd.print("Welcome");
  lcd.setCursor(0, 1);
  lcd.print("I'm booting...");

  button1.attachClick(click1);
  button2.attachClick(click2);
  button3.attachClick(click3);
  button1.attachDuringLongPress(longpress1);
  button2.attachDuringLongPress(longpress2);

  pinMode(2, OUTPUT);
  pinMode(3, OUTPUT);
  pinMode(4, OUTPUT);
  digitalWrite(2, LOW);
  digitalWrite(3, LOW);
  digitalWrite(4, LOW);
  attachInterrupt(2, tickButtons, LOW);
  attachInterrupt(3, tickButtons, LOW);
  attachInterrupt(4, tickButtons, LOW);

  currentState = 0;
  useSound = initializeSound();
  Serial.print("[SOUND] Use sound: ");
  Serial.println(useSound);

  Serial.println("[SETUP] Done...");
}

void loop() {
  tickButtons();
  switch (currentState) {
    case 0: selectDuration(); break;
    case 1: selectTemperature(); break;
    case 2: executeCooking(); break;
    case 3: finishedCooking(); break;
  }
  delayWithTicks(500);
  readSerial();
}

void showLcdState1() {
  String firstLine, secondLine;

  lcd.clear();
  firstLine.concat("Set Temperature");
  secondLine.concat(targetTemperature);
  secondLine.concat(" C");

  lcd.setCursor(0, 0);
  lcd.print(firstLine);
  lcd.setCursor(0, 1);
  lcd.print(secondLine);
}

void showLcdState0() {
  String firstLine, secondLine;
  sprintf(tMin, "%02d", targetDuration / 60);
  sprintf(tSec, "%02d", targetDuration % 60);

  lcd.clear();
  firstLine.concat("Set Duration");
  secondLine.concat(tMin);
  secondLine.concat("min ");
  secondLine.concat(tSec);
  secondLine.concat("sec");

  lcd.setCursor(0, 0);
  lcd.print(firstLine);
  lcd.setCursor(0, 1);
  lcd.print(secondLine);
}

void showLcdState2() {
  sprintf(tMin, "%02d", targetDuration / 60);
  sprintf(tSec, "%02d", targetDuration % 60);
  sprintf(cMin, "%02d", (currentMillis - startMillis) / 1000 / 60);
  sprintf(cSec, "%02d", (currentMillis - startMillis) / 1000 % 60);

  String firstLine, secondLine;

  lcd.clear();
  firstLine.concat(cMin);
  firstLine.concat(":");
  firstLine.concat(cSec);
  firstLine.concat(" / ");
  firstLine.concat(tMin);
  firstLine.concat(":");
  firstLine.concat(tSec);
  firstLine.concat(" m");
  secondLine.concat(currentTemperature);
  secondLine.concat(" / ");
  secondLine.concat(targetTemperature);
  secondLine.concat(" C");

  lcd.setCursor(0, 0);
  lcd.print(firstLine);
  lcd.setCursor(0, 1);
  lcd.print(secondLine);
}

void showLcdState3() {
  String firstLine, secondLine;

  lcd.clear();
  firstLine.concat(" -- Finished -- ");
  secondLine.concat(currentTemperature);
  secondLine.concat(" C");
  secondLine.concat("  -  [OK]");

  lcd.setCursor(0, 0);
  lcd.print(firstLine);
  lcd.setCursor(0, 1);
  lcd.print(secondLine);
}

void selectDuration() {
  showLcdState0();
}

void selectTemperature() {
  showLcdState1();
}

void executeCooking() {
  currentTemperature = Thermistor::readTemperature();

  if (!finished) {
    currentMillis = millis();

    if ((currentMillis - lastDecision) >= decisionInterval) {
      lastDecision = currentMillis;
      if (currentTemperature < targetTemperature) {
        RcHandler::powerOn();
      } else {
        RcHandler::powerOff();
      }
    }

    if ((currentMillis - startMillis) >= totalDuration) {
      finished = true;
      RcHandler::powerOff();
    }

  } else {
    RcHandler::powerOff();
    currentState = 3;
  }

  showLcdState2();
}

void finishedCooking() {
  currentTemperature = Thermistor::readTemperature();
  showLcdState3();

  if (!soundFinished) {
    int err = theAudio->writeFrames(AudioClass::Player0, soundFileFinished);
    if (err == AUDIOLIB_ECODE_FILEEND)
    {
      Serial.println("[SOUND] Finished sound!\n");
      soundFinished = true;
      theAudio->stopPlayer(AudioClass::Player0);
      soundFileFinished.close();
    }
  }

}

void tickButtons() {
  noInterrupts();
  button1.tick();
  button2.tick();
  button3.tick();
  interrupts();
}

void delayWithTicks(int delayMillis) {
  int split = delayMillis / 10;
  for (int i = 0; i < split; i++) {
    tickButtons();
    delay(10);
  }
}

void click1() {
  noInterrupts();
  switch (currentState) {
    case 0: {
        targetDuration += 5;
        showLcdState0();
        break;
      }
    case 1: {
        targetTemperature += 0.2;
        showLcdState1();
        break;
      }
    case 2: {
        targetTemperature += 0.2;
        showLcdState2();
        break;
      }
    case 3: break;
  }
  interrupts();
}

void longpress1() {
  noInterrupts();
  switch (currentState) {
    case 0: {
        targetDuration += 20;
        showLcdState0();
        break;
      }
    case 1: {
        targetTemperature += 1;
        showLcdState1();
        break;
      }
    case 2: {
        targetTemperature += 1;
        showLcdState2();
        break;
      }
    case 3: break;
  }
  interrupts();
}

void click2() {
  noInterrupts();
  switch (currentState) {
    case 0: {
        targetDuration -= 5;
        showLcdState0();
        break;
      }
    case 1: {
        targetTemperature -= 0.2;
        showLcdState1();
        break;
      }
    case 2: {
        targetTemperature -= 0.2;
        showLcdState2();
        break;
      }
    case 3: break;
  }
  interrupts();
}

void longpress2() {
  noInterrupts();
  switch (currentState) {
    case 0: {
        targetDuration -= 20;
        showLcdState0();
        break;
      }
    case 1: {
        targetTemperature -= 1;
        showLcdState1();
        break;
      }
    case 2: {
        targetTemperature -= 1;
        showLcdState2();
        break;
      }
    case 3: break;
  }
  interrupts();
}

void click3() {
  noInterrupts();
  switch (currentState) {
    case 0: {
        currentState = 1;
        showLcdState1();
        break;
      }
    case 1: {
        currentState = 2;
        initializeCooking();
        showLcdState2();
        break;
      }
    case 2: {
        currentState = 3;
        finished = true;
        soundFinished = false;
        theAudio->startPlayer(AudioClass::Player0);
        RcHandler::powerOff();
        showLcdState3();
        break;
      }
    case 3: {
        currentState = 0;
        initializeCooking();
        showLcdState0();
        break;
      }
  }
  interrupts();
}

void initializeCooking() {
  totalDuration = targetDuration * 1000;
  startMillis = millis();
  currentMillis = startMillis;
  lastDecision = startMillis - decisionInterval;
  finished = false;
}

void readSerial() {
  if (Serial2.available()) {
    if (Serial2.read() == '#') {
      float rcvdTemperature = Serial2.readStringUntil('#').toFloat();
      float rcvdDurationSeconds = Serial2.readStringUntil('#').toFloat();

      targetTemperature = rcvdTemperature;
      targetDuration = rcvdDurationSeconds / 60;

      initializeCooking();
      currentState = 2;
    }
  }
}

static void audioErrorCallback(const ErrorAttentionParam *atprm) {
  if (atprm->error_code >= AS_ATTENTION_CODE_WARNING)
  {
    // ErrEnd = true;
  }
}

bool initializeSound() {
  theAudio = AudioClass::getInstance();
  theAudio->begin(audioErrorCallback);
  theAudio->setRenderingClockMode(AS_CLKMODE_NORMAL);
  theAudio->setPlayerMode(AS_SETPLAYER_OUTPUTDEVICE_SPHP, AS_SP_DRV_MODE_LINEOUT);
  err_t err = theAudio->initPlayer(AudioClass::Player0, AS_CODECTYPE_MP3, "/mnt/sd0/BIN", AS_SAMPLINGRATE_AUTO, AS_CHANNEL_STEREO);
  if (err != AUDIOLIB_ECODE_OK) {
    Serial.println("Player0 initialize error\n");
    return false;
  }
  soundFileFinished = theSD.open("itIsSteakTime.mp3");
  if (!soundFileFinished) {
    Serial.println("Error opening finish sound");
    return false;
  }
  soundFileStart = theSD.open("startingCooking.mp3");
  if (!soundFileStart) {
    Serial.println("Error opening start sound");
    return false;
  }
  err = theAudio->writeFrames(AudioClass::Player0, soundFileFinished);
  if ((err != AUDIOLIB_ECODE_OK) && (err != AUDIOLIB_ECODE_FILEEND)) {
    printf("File Read Error! =%d\n", err);
    soundFileFinished.close();
    return false;
  }
  theAudio->setVolume(-160);
  // theAudio->startPlayer(AudioClass::Player0);
  return true;
}

Thermistor.cpp

C/C++
#include "Configuration.h"
#include "Thermistor.h"

float Thermistor::readTemperature() {
  return calculateTemperature(getSmoothReading());
}

float Thermistor::getSmoothReading() {
  digitalWrite(THERM_VCC_PIN, HIGH);
  delay(10);

  for (int i = 0; i < 2; i++) {
    analogRead(THERM_ARD_PIN);
  }

  int readings[THERM_CNT_RD];
  for (int i = 0; i < THERM_CNT_RD; i++) {
    readings[i] = analogRead(THERM_ARD_PIN);
  }
  digitalWrite(THERM_VCC_PIN, LOW);

  float average;
  for (int i = 0; i < THERM_CNT_RD; i++) {
    average += readings[i];
  }
  average = average / THERM_CNT_RD;

  // Serial.print("[THERM] Avg Reading: ");
  // Serial.println(average);
  return average;
}

float Thermistor::calculateTemperature(float analogValue) {
  float R = 10000 / (1023 / analogValue - 1);
  float Tc = 1 / (THERM_SHE_A + THERM_SHE_B * log(R) + THERM_SHE_C * pow(log(R), 3)) - 273.15;
  float Tf = (Tc * 9.0) / 5.0 + 32.0;

  Serial.print("[THERM] Temperature: ");
  Serial.print(Tc);
  Serial.print("C (");
  Serial.print(Tf);
  Serial.println("F)");

  return Tc;
}

Thermistor.h

C/C++
#ifndef THERMISTOR
#define THERMISTOR

#include <Arduino.h>

class Thermistor {
public:
  static float readTemperature();
  static float getSmoothReading();
  static float calculateTemperature(float analogValue);
};

#endif

Credits

Walter Heger

Walter Heger

4 projects • 83 followers

Comments