#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <ESP32Servo.h>
#include <OneWire.h>
#include <DallasTemperature.h>
// [Wokwi 演示操作指引]:請在模擬執行時,
// 用滑鼠依序點擊鍵盤最左側那一行的
// 『 1 』、『 4 』、『 7 』、『 * 』鍵,
// 即可分別啟動控制 1 到 4 號機組並行翻炒!
// 初始化 1602 LCD (SDA=21, SCL=22)
LiquidCrystal_I2C lcd(0x27, 16, 2);
// --- 實體硬體腳位定義 ---
const int KEY_ROWS[4] = {16, 17, 5, 18}; // R1, R2, R3, R4
const int KEY_COLS[4] = {34, 35, 32, 33}; // C1, C2, C3, C4
//18
const int TEMP_PINS[4] = {19, 0, 4, 23}; // TEMP1, TEMP3, TEMP4, TEMP5
// --- One-Wire 溫度傳感器設定 ---
OneWire oneWire1(TEMP_PINS[0]); DallasTemperature sensor1(&oneWire1);
OneWire oneWire2(TEMP_PINS[1]); DallasTemperature sensor2(&oneWire2);
OneWire oneWire3(TEMP_PINS[2]); DallasTemperature sensor3(&oneWire3);
OneWire oneWire4(TEMP_PINS[3]); DallasTemperature sensor4(&oneWire4);
// 狀態機列舉
enum CookState { CHAMBER_IDLE, HEATING, OVERHEAT, DONE };
struct Chamber {
int id;
int motorPin;
int ledPin;
Servo motor;
CookState state;
unsigned long startTime;
unsigned long duration;
float currentTemp;
unsigned long lastFlipTime; // 記錄上一次翻炒變更方向的時間
int motorDirection; // 目前馬達的方向狀態 (0 或 1)
};
Chamber chambers[4] = {
{1, 13, 25, Servo(), CHAMBER_IDLE, 0, 15000, 22.0, 0, 0}, // 1號鍵 -> [LED1(25), SERVO1(13), TEMP1(19)]
{2, 12, 26, Servo(), CHAMBER_IDLE, 0, 15000, 22.0, 0, 0}, // 2號鍵 -> [LED2(26), SERVO4(12), TEMP3(0)]
{3, 14, 27, Servo(), CHAMBER_IDLE, 0, 15000, 22.0, 0, 0}, // 3號鍵 -> [LED3(27), SERVO5(14), TEMP4(4)]
{4, 2, 15, Servo(), CHAMBER_IDLE, 0, 15000, 22.0, 0, 0} // 4號鍵 -> [LED4(15), SERVO3(2), TEMP5(23)]
};
const float MAX_SAFE_TEMP = 105.0;
void setup() {
Serial.begin(115200);
delay(500);
lcd.init();
lcd.backlight();
lcd.print("Chamber System");
lcd.setCursor(0, 1);
lcd.print("Pure SoftFix OK");
sensor1.begin();
sensor2.begin();
sensor3.begin();
sensor4.begin();
sensor1.setWaitForConversion(false);
sensor2.setWaitForConversion(false);
sensor3.setWaitForConversion(false);
sensor4.setWaitForConversion(false);
// 初始化馬達與 LED
for(int i = 0; i < 4; i++) {
pinMode(chambers[i].ledPin, OUTPUT);
digitalWrite(chambers[i].ledPin, LOW);
chambers[i].motor.attach(chambers[i].motorPin);
chambers[i].motor.write(0);
chambers[i].currentTemp = 22.0;
}
// --- 特化鍵盤反向驅動(由 ROWS 輸出 LOW,COLS 負責 INPUT 偵測) ---
for(int i = 0; i < 4; i++) {
pinMode(KEY_ROWS[i], OUTPUT); // 現在由 ROWS 負責發射訊號
digitalWrite(KEY_ROWS[i], LOW); // 常態吐出低電位,做為主動偵測點
pinMode(KEY_COLS[i], INPUT); // 現在由 COLS 負責接收訊號
}
// 針對 32 和 33 號(此時已變更為 COLS[2] 和 COLS[3])啟用內部上拉
pinMode(32, INPUT_PULLUP);
pinMode(33, INPUT_PULLUP);
sensor1.requestTemperatures();
sensor2.requestTemperatures();
sensor3.requestTemperatures();
sensor4.requestTemperatures();
delay(500);
updateDisplay();
}
void loop() {
// 底層手動按鍵狀態機掃描
static bool lastBtnStates[4] = {true, true, true, true};
for (int i = 0; i < 4; i++) {
// 💡 關鍵修改:改成讀取 KEY_COLS[i],因為現在是 ROWS 吐 LOW,COLS 負責收!
bool currentRawState = digitalRead(KEY_COLS[i]);
// 偵測按下瞬間 (由高電位變低電位的負邊緣觸發)
if (currentRawState == LOW && lastBtnStates[i] == true) {
delay(10); // 微幅防抖
if (digitalRead(KEY_COLS[i]) == LOW) { // 💡 這裡同步改成 COLS
// 觸發對應艙位的炒菜控制(後續邏輯完全不用變)
if (chambers[i].state == CHAMBER_IDLE) {
chambers[i].state = HEATING;
chambers[i].startTime = millis();
chambers[i].lastFlipTime = millis();
chambers[i].currentTemp = 22.0;
digitalWrite(chambers[i].ledPin, HIGH);
chambers[i].motor.write(180);
chambers[i].motorDirection = 1;
} else if (chambers[i].state == DONE || chambers[i].state == OVERHEAT) {
chambers[i].state = CHAMBER_IDLE;
digitalWrite(chambers[i].ledPin, LOW);
chambers[i].motor.write(0);
chambers[i].currentTemp = 22.0;
}
updateDisplay();
}
}
lastBtnStates[i] = currentRawState; // 記錄上一次狀態
}
// 【多路獨立翻炒】每個 Heating 中的馬達,各自每隔 1 秒左右反轉翻炒
for(int i = 0; i < 4; i++) {
if (chambers[i].state == HEATING) {
if (millis() - chambers[i].lastFlipTime >= 1000) {
if (chambers[i].motorDirection == 1) {
chambers[i].motor.write(0);
chambers[i].motorDirection = 0;
} else {
chambers[i].motor.write(180);
chambers[i].motorDirection = 1;
}
chambers[i].lastFlipTime = millis();
}
}
}
// --- 每一秒的邏輯更新(各機組獨立模擬溫升) ---
static unsigned long lastTempRead = 0;
if (millis() - lastTempRead >= 1000) {
sensor1.requestTemperatures();
sensor2.requestTemperatures();
sensor3.requestTemperatures();
sensor4.requestTemperatures();
for(int i = 0; i < 4; i++) {
if (chambers[i].state == HEATING) {
// 15秒內從 22度 爬升到 100度 (每秒精準上升 5.2度)
if (chambers[i].currentTemp < 100.0) {
chambers[i].currentTemp += 5.2;
}
if (chambers[i].currentTemp >= 100.0) {
chambers[i].currentTemp = 100.0; // 穩穩鎖定在 H100C
}
} else if (chambers[i].state == CHAMBER_IDLE) {
if (i == 0) chambers[i].currentTemp = sensor1.getTempCByIndex(0);
if (i == 1) chambers[i].currentTemp = sensor2.getTempCByIndex(0);
if (i == 2) chambers[i].currentTemp = sensor3.getTempCByIndex(0);
if (i == 3) chambers[i].currentTemp = sensor4.getTempCByIndex(0);
if (chambers[i].currentTemp < -50) chambers[i].currentTemp = 22.0;
}
}
lastTempRead = millis();
updateDisplay();
}
// 15 秒烹飪定時自動關機檢查
bool needUpdate = false;
for(int i = 0; i < 4; i++) {
if (chambers[i].state == HEATING) {
if (millis() - chambers[i].startTime >= chambers[i].duration) {
chambers[i].state = DONE;
chambers[i].currentTemp = 22.0;
digitalWrite(chambers[i].ledPin, LOW);
chambers[i].motor.write(0);
needUpdate = true;
}
}
}
if (needUpdate) updateDisplay();
}
void updateDisplay() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("C1:"); lcd.print(getStateStr(chambers[0].state)); lcd.print((int)chambers[0].currentTemp); lcd.print("C");
lcd.setCursor(9, 0);
lcd.print("C2:"); lcd.print(getStateStr(chambers[1].state)); lcd.print((int)chambers[1].currentTemp); lcd.print("C");
lcd.setCursor(0, 1);
lcd.print("C3:"); lcd.print(getStateStr(chambers[2].state)); lcd.print((int)chambers[2].currentTemp); lcd.print("C");
lcd.setCursor(9, 1);
lcd.print("C4:"); lcd.print(getStateStr(chambers[3].state)); lcd.print((int)chambers[3].currentTemp); lcd.print("C");
}
const char* getStateStr(CookState state) {
switch(state) {
case HEATING: return "H";
case OVERHEAT: return "X";
case DONE: return "D";
case CHAMBER_IDLE:
default: return "R";
}
}