#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <time.h>
// ================= CẤU HÌNH KẾT NỐI (sửa 3 dòng) =================
const char* WIFI_SSID = "Wokwi-GUEST";
const char* WIFI_PASSWORD = "";
const char* TB_HOST = "eu.thingsboard.cloud";
const uint16_t TB_PORT = 1883;
const char* TB_TOKEN = "BfdGMqrY1VFe9PdOnOqD";
const char* SHEET_URL = "https://script.google.com/macros/s/XXXX/exec";
// ================= NGƯỠNG MẶC ĐỊNH (đổi được từ ThingsBoard) =================
float currentThreshold = 0.5; // A: dòng >= ngưỡng => bếp đang hoạt động
uint16_t maxAbsentSec = 30; // s: vắng người tối đa trước khi hú còi
uint16_t alarmSec = 60; // s: còi kêu tối đa, hết mà vẫn vắng => ngắt điện
uint16_t maxRunMin = 120; // phút: chạy liên tục tối đa
int alarmStartHour = 0; // khung giờ cảnh báo (0..24). 0-24 = cả ngày
int alarmEndHour = 24;
const float PRESENCE_RANGE_CM = 200.0; // 2 m
const bool RELAY_ACTIVE_LOW = false; // đổi true nếu relay của bạn đảo mức
// ================= CHÂN (ESP32-C3 Super Mini) =================
// ESP32-C3 Super Mini: tránh GPIO2, GPIO8 (đèn LED trên board), GPIO20/21 (UART).
// GPIO9 là nút BOOT trên board, dùng làm nút xác nhận được.
const uint8_t PIN_TRIG = 0; // Wokwi: HC-SR04 thay radar
const uint8_t PIN_ECHO = 1;
const uint8_t PIN_CURRENT = 4; // ADC1 (GPIO0-4 là ADC1, dùng được cùng Wi-Fi). Wokwi: chiết áp
const uint8_t PIN_RELAY = 10;
const uint8_t PIN_LED_G = 3; // xanh: có người
const uint8_t PIN_LED_Y = 5; // vàng: đang đếm vắng người
const uint8_t PIN_LED_R = 6; // đỏ: còi / đã ngắt điện
const uint8_t PIN_BUZZER = 7;
const uint8_t PIN_BUTTON = 9; // nút xác nhận cảnh báo / cấp điện lại (nút BOOT)
// ================= TRẠNG THÁI =================
enum State { IDLE, PRESENT, COUNTDOWN, ALARM, CUTOFF };
const char* STATE_NAMES[] = {"IDLE", "PRESENT", "COUNTDOWN", "ALARM", "CUTOFF"};
enum Reason { R_NONE, R_ABSENT, R_MAXRUN };
const char* REASON_NAMES[] = {"NONE", "ABSENT", "MAX_RUN"};
State state = IDLE;
Reason alarmReason = R_NONE;
unsigned long stateSince = 0;
float current = 0, lastDistance = -1;
bool present = true, cooking = false, relayOn = false;
uint8_t missCount = 0;
// Phiên sử dụng
bool sessionActive = false;
unsigned long sessionStart = 0, runBase = 0;
int sessionNo = 0;
bool tbPending = true;
unsigned long lastTelemetry = 0, lastSheet = 0, lastMqttAttempt = 0, lastWifiAttempt = 0;
bool mqttAttempted = false;
// Hàng đợi sự kiện gửi lên Google Sheet
struct EventItem { char type[24]; char detail[40]; int session; long value; };
EventItem eventQueue[8];
uint8_t qHead = 0, qCount = 0;
WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);
// ================= SỰ KIỆN =================
void logEvent(const char* type, const char* detail, long value) {
Serial.printf("EVENT %s | %s | %ld\n", type, detail, value);
if (qCount < 8) {
EventItem& e = eventQueue[(qHead + qCount) % 8];
strlcpy(e.type, type, sizeof(e.type));
strlcpy(e.detail, detail, sizeof(e.detail));
e.session = sessionNo;
e.value = value;
qCount++;
}
if (mqtt.connected()) {
JsonDocument d;
d["event"] = type;
d["detail"] = detail;
d["sessionNo"] = sessionNo;
d["eventValue"] = value;
char p[200];
serializeJson(d, p, sizeof(p));
mqtt.publish("v1/devices/me/telemetry", p);
}
}
// ================= CẢM BIẾN =================
// Radar thật (LD2410): thay hàm này bằng đọc khoảng cách từ thư viện LD2410
float readDistanceCm() {
digitalWrite(PIN_TRIG, LOW); delayMicroseconds(2);
digitalWrite(PIN_TRIG, HIGH); delayMicroseconds(10);
digitalWrite(PIN_TRIG, LOW);
unsigned long d = pulseIn(PIN_ECHO, HIGH, 30000UL);
if (d == 0) return -1;
return d * 0.0343f / 2.0f;
}
// Cảm biến dòng thật (ACS712 / SCT-013 / PZEM): thay công thức quy đổi ở đây
float readCurrentA() {
int raw = analogRead(PIN_CURRENT);
return raw * 20.0f / 4095.0f; // chiết áp 0..20A
}
bool readPresence() {
lastDistance = readDistanceCm();
bool nearby = (lastDistance > 0 && lastDistance <= PRESENCE_RANGE_CM);
if (nearby) missCount = 0;
else if (missCount < 255) missCount++;
return missCount < 3; // 3 lần liên tiếp không thấy mới coi là vắng
}
bool inAlarmWindow() {
if (alarmStartHour <= 0 && alarmEndHour >= 24) return true;
struct tm t;
if (!getLocalTime(&t, 0)) return true; // chưa có giờ NTP => cho phép cảnh báo
int h = t.tm_hour;
if (alarmStartHour < alarmEndHour) return h >= alarmStartHour && h < alarmEndHour;
return h >= alarmStartHour || h < alarmEndHour;
}
// ================= ĐIỀU KHIỂN =================
void setRelay(bool on) {
relayOn = on;
bool level = RELAY_ACTIVE_LOW ? !on : on;
digitalWrite(PIN_RELAY, level ? HIGH : LOW); // dùng chân COM-NO: relay đóng = bếp có điện
}
void setState(State s) {
if (s == state) return;
state = s;
stateSince = millis();
tbPending = true;
Serial.printf("State -> %s\n", STATE_NAMES[state]);
}
int sessionSec() {
return sessionActive ? (millis() - sessionStart) / 1000 : 0;
}
int countdownSec() {
unsigned long e = millis() - stateSince;
unsigned long limit = (state == COUNTDOWN) ? maxAbsentSec * 1000UL : alarmSec * 1000UL;
if (state != COUNTDOWN && state != ALARM) return 0;
return (limit > e ? limit - e : 0) / 1000;
}
void startSession() {
sessionNo++;
sessionActive = true;
sessionStart = millis();
runBase = sessionStart;
logEvent("SESSION_START", "cooker_on", 0);
}
void endSession(const char* reason) {
if (!sessionActive) return;
logEvent("SESSION_END", reason, sessionSec());
sessionActive = false;
}
void updateLogic() {
if (state == CUTOFF) return; // chờ người dùng xác nhận
unsigned long now = millis();
cooking = relayOn && current >= currentThreshold;
if (!cooking) {
endSession("COOKER_OFF");
alarmReason = R_NONE;
setState(IDLE);
return;
}
if (!sessionActive) startSession();
bool absentNow = !present && inAlarmWindow();
if (state == ALARM) {
if (alarmReason == R_ABSENT && !absentNow) {
logEvent("ALARM_CLEARED", "person_returned", 0);
alarmReason = R_NONE;
setState(PRESENT);
return;
}
if (now - stateSince >= alarmSec * 1000UL) {
setRelay(false); // còi hết giờ mà chưa xử lý => ngắt điện
logEvent("POWER_CUT", REASON_NAMES[alarmReason], 0);
endSession("POWER_CUT");
setState(CUTOFF);
}
return;
}
if (now - runBase >= maxRunMin * 60000UL) {
alarmReason = R_MAXRUN;
logEvent("ALARM_MAX_RUN", "run_too_long", sessionSec());
setState(ALARM);
return;
}
if (!absentNow) { setState(PRESENT); return; }
if (state == IDLE || state == PRESENT) {
setState(COUNTDOWN);
} else if (state == COUNTDOWN && now - stateSince >= maxAbsentSec * 1000UL) {
alarmReason = R_ABSENT;
logEvent("ALARM_ABSENT", "no_person", maxAbsentSec);
setState(ALARM);
}
}
void updateOutputs() {
digitalWrite(PIN_LED_G, state == PRESENT);
digitalWrite(PIN_LED_Y, state == COUNTDOWN);
digitalWrite(PIN_LED_R, state == ALARM || state == CUTOFF);
static bool buzzOn = false;
bool want = (state == ALARM) && ((millis() / 500) % 2 == 0); // kêu ngắt quãng
if (want != buzzOn) {
buzzOn = want;
if (want) tone(PIN_BUZZER, 2000); else noTone(PIN_BUZZER);
}
}
// Nút bấm / RPC: xác nhận cảnh báo hoặc cấp điện lại
void confirmAction(const char* source) {
if (state == ALARM) {
logEvent("ALARM_ACK", source, 0);
if (alarmReason == R_MAXRUN) runBase = millis();
alarmReason = R_NONE;
missCount = 0;
setState(PRESENT);
} else if (state == CUTOFF) {
logEvent("POWER_RESTORED", source, 0);
setRelay(true);
missCount = 0;
alarmReason = R_NONE;
setState(IDLE);
}
}
void readButton() {
static int last = HIGH, stable = HIGH;
static unsigned long t = 0;
int r = digitalRead(PIN_BUTTON);
if (r != last) { t = millis(); last = r; }
if (millis() - t >= 50 && r != stable) {
stable = r;
if (stable == LOW) confirmAction("button");
}
}
// ================= THINGSBOARD =================
void applyConfig(JsonObject o) {
if (!o["currentThreshold"].isNull()) currentThreshold = o["currentThreshold"].as<float>();
if (!o["maxAbsentSec"].isNull()) maxAbsentSec = o["maxAbsentSec"].as<int>();
if (!o["alarmSec"].isNull()) alarmSec = o["alarmSec"].as<int>();
if (!o["maxRunMin"].isNull()) maxRunMin = o["maxRunMin"].as<int>();
if (!o["alarmStartHour"].isNull()) alarmStartHour = o["alarmStartHour"].as<int>();
if (!o["alarmEndHour"].isNull()) alarmEndHour = o["alarmEndHour"].as<int>();
Serial.printf("Config: thr=%.2fA absent=%us alarm=%us run=%umin window=%d-%d\n",
currentThreshold, maxAbsentSec, alarmSec, maxRunMin, alarmStartHour, alarmEndHour);
}
void publishTelemetry() {
if (!mqtt.connected()) return;
JsonDocument doc;
doc["current"] = roundf(current * 100) / 100;
doc["distance"] = (int)lastDistance;
doc["present"] = present;
doc["state"] = STATE_NAMES[state];
doc["relay"] = relayOn;
doc["cooking"] = cooking;
doc["countdown"] = countdownSec();
doc["sessionNo"] = sessionNo;
doc["sessionSec"] = sessionSec();
doc["alarmReason"] = REASON_NAMES[alarmReason];
char payload[400];
serializeJson(doc, payload, sizeof(payload));
if (mqtt.publish("v1/devices/me/telemetry", payload)) {
tbPending = false;
lastTelemetry = millis();
}
}
void onMqttMessage(char* topic, byte* payload, unsigned int length) {
const char* rpcPrefix = "v1/devices/me/rpc/request/";
const char* attrPrefix = "v1/devices/me/attributes";
// Cấu hình (shared attributes) do dashboard đẩy xuống hoặc trả lời khi xin
if (strncmp(topic, attrPrefix, strlen(attrPrefix)) == 0) {
JsonDocument doc;
if (deserializeJson(doc, payload, length)) return;
JsonObject o = doc["shared"].is<JsonObject>() ? doc["shared"].as<JsonObject>()
: doc.as<JsonObject>();
applyConfig(o);
return;
}
if (strncmp(topic, rpcPrefix, strlen(rpcPrefix)) != 0) return;
String responseTopic = "v1/devices/me/rpc/response/";
responseTopic += topic + strlen(rpcPrefix);
JsonDocument request, response;
if (deserializeJson(request, payload, length)) {
response["ok"] = false;
} else {
const char* method = request["method"] | "";
response["ok"] = true;
if (strcmp(method, "resetSystem") == 0) {
confirmAction("ThingsBoard");
} else if (strcmp(method, "confirmChecked") == 0) {
logEvent("CHECKED", "user_checked", 0);
confirmAction("ThingsBoard");
} else if (strcmp(method, "markFalseAlarm") == 0) {
logEvent("FALSE_ALARM", "marked_by_user", 0);
confirmAction("ThingsBoard");
} else {
response["ok"] = false;
response["error"] = "Unknown method";
}
}
char out[128];
serializeJson(response, out, sizeof(out));
mqtt.publish(responseTopic.c_str(), out);
}
void startWifi() {
if (strcmp(WIFI_SSID, "Wokwi-GUEST") == 0) {
WiFi.begin(WIFI_SSID, WIFI_PASSWORD, 6); // Wokwi
} else {
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
WiFi.setTxPower(WIFI_POWER_8_5dBm); // Super Mini thật hay mất Wi-Fi nếu để công suất mặc định
}
}
void maintainConnection() {
unsigned long now = millis();
if (WiFi.status() != WL_CONNECTED) {
if (now - lastWifiAttempt >= 10000UL) {
lastWifiAttempt = now;
startWifi();
}
return;
}
if (mqtt.connected()) return;
if (mqttAttempted && now - lastMqttAttempt < 5000UL) return;
mqttAttempted = true;
lastMqttAttempt = now;
String clientId = "ESP32-BEPTU-" + WiFi.macAddress();
Serial.println("Connecting to ThingsBoard...");
if (mqtt.connect(clientId.c_str(), TB_TOKEN, nullptr)) {
mqtt.subscribe("v1/devices/me/rpc/request/+");
mqtt.subscribe("v1/devices/me/attributes");
mqtt.subscribe("v1/devices/me/attributes/response/+");
mqtt.publish("v1/devices/me/attributes/request/1",
"{\"sharedKeys\":\"currentThreshold,maxAbsentSec,alarmSec,maxRunMin,alarmStartHour,alarmEndHour\"}");
Serial.println("ThingsBoard connected; RPC ready.");
tbPending = true;
} else {
Serial.printf("MQTT failed, rc=%d\n", mqtt.state());
}
}
// ================= GOOGLE SHEETS =================
void httpGetSheet(const String& query) {
lastSheet = millis();
WiFiClientSecure client;
client.setInsecure();
HTTPClient http;
http.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);
http.setTimeout(5000);
if (http.begin(client, String(SHEET_URL) + "?" + query)) {
int code = http.GET();
Serial.printf("Sheet HTTP %d\n", code);
http.end();
}
}
void sendSheetData() {
httpGetSheet(String("type=data&state=") + STATE_NAMES[state] +
"¤t=" + String(current, 2) +
"&distance=" + String((int)lastDistance) +
"&present=" + String(present ? 1 : 0) +
"&relay=" + String(relayOn ? 1 : 0) +
"&session=" + String(sessionNo) +
"&sessionSec=" + String(sessionSec()));
}
void sendSheetEvent() {
EventItem e = eventQueue[qHead];
qHead = (qHead + 1) % 8;
qCount--;
httpGetSheet(String("type=event&event=") + e.type + "&detail=" + e.detail +
"&session=" + String(e.session) + "&value=" + String(e.value));
}
// ================= SETUP / LOOP =================
void setup() {
Serial.begin(115200);
Serial.println("Bep tu monitor start");
pinMode(PIN_TRIG, OUTPUT);
pinMode(PIN_ECHO, INPUT);
pinMode(PIN_RELAY, OUTPUT);
pinMode(PIN_LED_G, OUTPUT);
pinMode(PIN_LED_Y, OUTPUT);
pinMode(PIN_LED_R, OUTPUT);
pinMode(PIN_BUZZER, OUTPUT);
pinMode(PIN_BUTTON, INPUT_PULLUP);
setRelay(true);
WiFi.mode(WIFI_STA);
startWifi();
lastWifiAttempt = millis();
configTime(7 * 3600, 0, "pool.ntp.org", "time.google.com"); // giờ Việt Nam (UTC+7)
mqtt.setServer(TB_HOST, TB_PORT);
mqtt.setCallback(onMqttMessage);
mqtt.setBufferSize(512);
mqtt.setSocketTimeout(1);
mqtt.setKeepAlive(30);
}
void loop() {
readButton();
static unsigned long lastSample = 0;
if (millis() - lastSample >= 200) {
lastSample = millis();
current = readCurrentA();
present = readPresence();
updateLogic();
}
updateOutputs();
maintainConnection();
if (mqtt.connected()) mqtt.loop();
if (mqtt.connected() && (tbPending || millis() - lastTelemetry >= 5000UL)) publishTelemetry();
if (WiFi.status() == WL_CONNECTED) {
if (qCount > 0 && millis() - lastSheet >= 1000UL) sendSheetEvent();
else if (millis() - lastSheet >= 30000UL) sendSheetData();
}
delay(1);
}Loading
esp32-c3-devkitm-1
esp32-c3-devkitm-1