#pragma once // Prevents duplicate inclusion of header files (used here for the main program as a convention)
#include <WiFi.h> // ESP32 WiFi functionality library
#include <PubSubClient.h> // MQTT communication library
#include <Wire.h> // I2C communication library (for LCD)
#include <LiquidCrystal_I2C.h> // I2C interface LCD display library
#include <DHT.h> // Library for DHT series temperature and humidity sensors
#include <time.h> // Time handling library (for NTP synchronization)
#include <esp_system.h> // ESP32 system functions library (e.g., restart)
// Pin definitions (corresponding to ESP32 GPIO pins)
#define BUTTON_PIN 15 // Restart button pin
#define LED1_PIN 19 // Alarm LED pin
#define LED2_PIN 18 // Status LED pin
#define DHTPIN 26 // DHT temperature and humidity sensor pin
#define DHTTYPE DHT22 // Sensor type is DHT22
#define BUZZER_PIN 4 // Buzzer pin
#define GAS_PIN 34 // Gas sensor (analog input) pin
#define PIR_PIN 27 // PIR motion sensor pin
// Threshold parameters (trigger alarm when exceeded)
const float TEMP_THRESHOLD_C = 20.0; // Temperature threshold (Celsius)
const float TEMP_THRESHOLD_F = 68.0; // Temperature threshold (Fahrenheit)
const float HUMIDITY_THRESHOLD = 70.0; // Humidity threshold (percentage)
const int GAS_THRESHOLD = 900; // Gas concentration threshold (analog value)
const unsigned long MOTION_DELAY = 5000; // Motion detection duration (milliseconds)
// WiFi and MQTT configuration
const char* ssid = "Wokwi-GUEST"; // WiFi name
const char* password = ""; // WiFi password (empty here)
const char* mqtt_server = "broker.hivemq.com"; // MQTT server address
// MQTT topics (categories of messages to publish/subscribe)
const char* mqtt_topic_pub = "IOT/Warehouse/sensor"; // Sensor data publication topic
const char* mqtt_topic_time = "IOT/Warehouse/time"; // Time information publication topic
const char* mqtt_topic_sub = "IOT/Warehouse/control"; // Control command subscription topic
const char* mqtt_topic_alarm = "IOT/Warehouse/alarm"; // Alarm status publication topic
const char* mqtt_topic_lwt = "IOT/Warehouse/status"; // Device online status topic (LWT)
const char* mqtt_topic_motion = "IOT/Warehouse/motion"; // Motion detection publication topic
// Functional object instantiation
WiFiClient espClient; // WiFi client instance
PubSubClient client(espClient); // MQTT client instance (depends on WiFi client)
DHT dht(DHTPIN, DHTTYPE); // Temperature and humidity sensor instance
LiquidCrystal_I2C lcd(0x27, 20, 4); // LCD instance (I2C address 0x27, 20 columns 4 rows)
// State variables
bool led1State = false, led2State = false; // States of LED1 and LED2 (on/off)
// Timestamp variables (record last operation time for timed tasks)
unsigned long lastMsg = 0, lastDisplayChange = 0,
alarmStartTime = 0, lastBuzzerToggle = 0,
lastSensorRead = 0;
const long sensorReadInterval = 2000; // Sensor reading interval (2000ms = 2s)
const long publishInterval = 2000, displayInterval = 2000; // Data publication and display switching interval (2s)
int displayState = 0; // LCD display state (0: welcome page, 1: time, 2: sensor data)
char msg[100]; // MQTT message buffer (max 100 characters)
// Flag bits (control program flow)
bool welcomeDisplayed = false, // Whether the welcome page has been displayed
isAlarm = false, // Whether in alarm state
motionDetected = false, // Whether motion is detected
motionStateChanged = false; // Whether motion state has changed (to trigger display update)
unsigned long lastMotionTime = 0; // Timestamp of last detected motion
// Button state variables (for debounce handling)
int buttonState, lastButtonState = HIGH;
unsigned long lastDebounceTime = 0, debounceDelay = 50; // Debounce delay (50ms)
bool restartTriggered = false; // Whether restart is triggered
uint8_t dot[] = {0x0E, 0x0A, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00}; // LCD custom character (for temperature symbol "°")
// Function declarations (inform compiler of function existence, implementations follow)
void blinkAndRestart(); // Blink LED then restart device
void checkSensors(); // Check sensor data and determine if alarm is needed
void setup_wifi(); // Initialize WiFi connection
int readGasLevel(); // Read gas sensor value
void handlePIR(); // Process PIR motion sensor data
void displayWelcome(); // Display welcome message on LCD
void displayDateTime(); // Display date and time on LCD
void displaySensorData(); // Display sensor data on LCD
void callback(char* topic, byte* payload, unsigned int length); // MQTT message callback function
void controlBuzzer(bool enable); // Control buzzer (on/off)
void handleButton(); // Process button input (for restart)
void reconnect(); // Reconnect to MQTT server
void setup_wifi() {
Serial.println("Connecting to WiFi..."); // Print connection prompt to serial port
WiFi.begin(ssid, password); // Start WiFi connection (using previously defined ssid and password)
int wifiRetry = 0; // Retry counter
// Wait for WiFi connection, max 10 retries
while (WiFi.status() != WL_CONNECTED && wifiRetry < 10) {
Serial.print("."); // Print progress dots
delay(500); // Wait 500ms
wifiRetry++;
}
if (WiFi.status() == WL_CONNECTED) Serial.println("\nWiFi connected!"); // Connection successful
else { // Connection failed
Serial.println("\nWiFi failed! Restarting...");
esp_restart(); // Restart device to retry
}
}
int readGasLevel() { return analogRead(GAS_PIN); } // Read analog value from gas sensor and return
void handlePIR() {
bool currentMotion = digitalRead(PIR_PIN) == HIGH; // Read PIR pin state (high = motion detected)
if (currentMotion) { // Motion detected
lastMotionTime = millis(); // Update last motion timestamp
if (!motionDetected) { // If motion was not detected before (state change)
motionDetected = true; // Update state to "motion detected"
motionStateChanged = true; // Mark state change
Serial.println("Motion detected!"); // Print to serial port
client.publish(mqtt_topic_motion, "detected"); // Publish to MQTT motion topic
}
} else if (motionDetected && millis() - lastMotionTime > MOTION_DELAY) { // No motion, and beyond duration
motionDetected = false; // Update state to "no motion"
motionStateChanged = true; // Mark state change
Serial.println("Motion stopped"); // Print to serial port
client.publish(mqtt_topic_motion, "stopped"); // Publish to MQTT motion topic
}
}
void displayWelcome() {
if (!welcomeDisplayed) { // If welcome page not displayed
lcd.clear(); // Clear screen
lcd.print("Hi,Welcome!"); // First line shows welcome message
lcd.setCursor(0, 1); // Move cursor to second line
lcd.print("Warehouse1,"); // Second line shows warehouse name
lcd.setCursor(0, 2); // Move cursor to third line
lcd.print("By Group3!"); // Third line shows group name
welcomeDisplayed = true; // Mark as displayed
delay(2000); // Stay for 2 seconds
}
}
void displayDateTime() {
struct tm timeinfo; // Time structure (stores year, month, day, hour, minute, second)
if (getLocalTime(&timeinfo)) { // Get local time (execute if successful)
char dtbuf[21]; // Time string buffer
// Format time as "YYYY-MM-DD HH:MM:SS"
strftime(dtbuf, sizeof(dtbuf), "%Y-%m-%d %H:%M:%S", &timeinfo);
lcd.clear(); // Clear screen
lcd.print("DateTime:"); // First line shows "DateTime:"
lcd.setCursor(0, 1); // Move cursor to second line
lcd.print(dtbuf); // Second line shows formatted time
// Publish time to MQTT every publishInterval (2s)
if (millis() - lastMsg >= publishInterval) {
lastMsg = millis(); // Update timestamp
client.publish(mqtt_topic_time, dtbuf); // Publish time message
}
} else { // Time synchronization failed
lcd.clear();
lcd.print("Time sync lost!"); // Show synchronization failure prompt
}
}
void checkSensors() {
unsigned long start = millis(); // Record start time (for timeout judgment)
// Read temperature and humidity data (dht.readHumidity() returns humidity, readTemperature() returns Celsius, with true returns Fahrenheit)
float h = dht.readHumidity();
float c = dht.readTemperature();
float f = dht.readTemperature(true);
int gas = readGasLevel(); // Read gas concentration
// If reading takes more than 1s, consider timeout
if (millis() - start > 1000) {
Serial.println("Sensor read timeout");
return;
}
// Check if temperature and humidity data are valid (isnan judges if non-numeric)
if (isnan(h) || isnan(c) || isnan(f)) {
Serial.println(F("DHT read failed!")); // Print failure prompt
return;
}
// Print sensor data to serial port (for debugging)
Serial.printf("Hum: %.1f%% Temp: %.1f°C / %.1f°F Gas: %d Motion: %s\n",
h, c, f, gas, motionDetected ? "Yes" : "No");
// Determine if alarm is triggered (whether temperature, humidity, gas exceed thresholds)
bool tempAlarm = c > TEMP_THRESHOLD_C;
bool humAlarm = h > HUMIDITY_THRESHOLD;
bool gasAlarm = gas > GAS_THRESHOLD;
bool newAlarm = tempAlarm || humAlarm || gasAlarm; // Alarm triggers if any exceeds
if (newAlarm) { // Alarm needed
if (!isAlarm) { // If not alarming before (state change)
isAlarm = true; // Update to alarm state
alarmStartTime = millis(); // Record alarm start time
client.publish(mqtt_topic_alarm, "Alarm"); // Publish alarm message
// Determine alarm type (gas, temperature, humidity)
const char* alarmType = "";
float alarmValue = 0;
const char* alarmUnit = "";
if (gasAlarm) {
alarmType = "Gas";
alarmValue = gas;
alarmUnit = "";
}
else if (tempAlarm) {
alarmType = "Temp";
alarmValue = c;
alarmUnit = "°C";
}
else if (humAlarm) {
alarmType = "Humidity";
alarmValue = h;
alarmUnit = "%";
}
// Print alarm details to serial port
Serial.printf("Alarm triggered: %s %.1f%s\n", alarmType, alarmValue, alarmUnit);
}
} else { // No alarm needed
if (isAlarm) { // If alarming before (state change)
isAlarm = false; // Update to normal state
client.publish(mqtt_topic_alarm, "Normal"); // Publish normal message
Serial.println("Alarm cleared"); // Print to serial port
}
}
}
void displaySensorData() {
// Read sensor data (for display)
float h = dht.readHumidity();
float c = dht.readTemperature();
float f = dht.readTemperature(true);
int gas = readGasLevel();
if (isAlarm) { // Display in alarm state
lcd.clear();
lcd.print("ALARM!"); // First line shows "ALARM!"
lcd.setCursor(0, 1); // Move cursor to second line
// Determine alarm type
bool tempAlarm = c > TEMP_THRESHOLD_C;
bool humAlarm = h > HUMIDITY_THRESHOLD;
bool gasAlarm = gas > GAS_THRESHOLD;
if (gasAlarm) { // Gas exceeds limit
lcd.print("Gas Over!");
lcd.setCursor(0, 2); // Third line
lcd.print(String(gas) + " > " + String(GAS_THRESHOLD)); // Show current value and threshold
}
else if (tempAlarm) { // Temperature exceeds limit
lcd.print("Temp Over!");
lcd.setCursor(0, 2);
lcd.print(String(c,1));lcd.write(0);lcd.print("C>"); // 0 is custom "°" symbol
lcd.print(String(TEMP_THRESHOLD_C, 1));lcd.write(0);lcd.print("C");
}
else if (humAlarm) { // Humidity exceeds limit
lcd.print("Humidity Over!");
lcd.setCursor(0, 2);
lcd.print(String(h) + "% > " + String(HUMIDITY_THRESHOLD) + "%");
}
lcd.setCursor(0, 3); // Fourth line
lcd.print("Motion: ");
lcd.print(motionDetected ? "Detected" : "None"); // Show motion state
} else { // Display in normal state
lcd.clear();
lcd.setCursor(0, 0); // First line: humidity
lcd.print("Humidity: "); lcd.print(h, 1); lcd.print("%");
lcd.setCursor(0, 1); // Second line: temperature (Celsius/Fahrenheit)
lcd.print("Temp: "); lcd.print(c, 1); lcd.write(0); lcd.print("C/");
lcd.print(f, 1); lcd.write(0); lcd.print("F");
lcd.setCursor(0, 2); // Third line: gas concentration
lcd.print("Gas Level: "); lcd.print(gas);
lcd.setCursor(0, 3); // Fourth line: motion state
lcd.print("Motion: "); lcd.print(motionDetected ? "Detected" : "None");
// Publish sensor data to MQTT every publishInterval (2s)
if (millis() - lastMsg >= publishInterval) {
lastMsg = millis();
// Format message as JSON (easy to parse)
snprintf(msg, sizeof(msg),
"{\"temp_c\": %.1f, \"temp_f\": %.1f, \"hum\": %.1f, \"gas\": %d, \"motion\": %s}",
c, f, h, gas, motionDetected ? "true" : "false");
client.publish(mqtt_topic_pub, msg); // Publish message
}
}
}
void callback(char* topic, byte* payload, unsigned int length) {
payload[length] = 0; // Add terminator to payload (convert to string)
String command = String((char*)payload); // Convert to string
command.toUpperCase(); // Convert to uppercase (case-insensitive)
Serial.print("MQTT command: "); Serial.println(command); // Print received command
if (command == "RESTART") { // If command is "RESTART"
Serial.println("Received restart command");
blinkAndRestart(); // Execute restart
}
}
void controlBuzzer(bool enable) {
static int buzzerPattern = 0; // Static variable (records buzzer pattern)
unsigned long now = millis(); // Current time
if (enable && now - lastBuzzerToggle >= 500) { // When alarming, switch frequency every 500ms
lastBuzzerToggle = now; // Update timestamp
buzzerPattern = (buzzerPattern + 1) % 4; // Cycle through patterns (0-3)
switch (buzzerPattern) { // Different patterns correspond to different frequencies
case 0: tone(BUZZER_PIN, 1000); break; // 1000Hz
case 1: tone(BUZZER_PIN, 1500); break; // 1500Hz
case 2: tone(BUZZER_PIN, 2000); break; // 2000Hz
case 3: noTone(BUZZER_PIN); break; // Mute
}
} else if (!enable) { // Turn off buzzer when not alarming
noTone(BUZZER_PIN);
}
}
void blinkAndRestart() {
lcd.clear();
lcd.print("Restarting..."); // LCD shows restart prompt
// Blink LED2 3 times (prompt restart)
for (int i = 0; i < 3; i++) {
digitalWrite(LED2_PIN, LOW); // Off
delay(300);
digitalWrite(LED2_PIN, HIGH); // On
delay(300);
}
// Publish LWT message (device state is "restarting")
client.publish(mqtt_topic_lwt, "restarting", true);
delay(100); // Wait for message to be published
esp_restart(); // Restart ESP32
}
void handleButton() {
int reading = digitalRead(BUTTON_PIN); // Read button pin state
// If current state differs from last (possible jitter), update debounce timestamp
if (reading != lastButtonState) {
lastDebounceTime = millis();
}
// Exceed debounce delay, and state is stable (confirm button press)
if (millis() - lastDebounceTime > debounceDelay && reading != buttonState) {
buttonState = reading; // Update button state
// Button pressed (low level, due to INPUT_PULLUP, high when not pressed) and restart not triggered
if (buttonState == LOW && !restartTriggered) {
restartTriggered = true; // Mark as triggered
Serial.println("Button pressed: Restart");
blinkAndRestart(); // Execute restart
}
}
lastButtonState = reading; // Update last state
}
void reconnect() {
if (client.connected()) return; // Return directly if connected
Serial.println("Connecting to MQTT...");
// Connect to MQTT server (client ID "ESP32ClientDevice", set LWT message)
if (client.connect("ESP32ClientDevice", mqtt_topic_lwt, 0, true, "offline")) {
Serial.println("MQTT connected!");
client.publish(mqtt_topic_lwt, "online", true); // Publish online state
client.subscribe(mqtt_topic_sub); // Subscribe to control topic
delay(500);
} else { // Connection failed
Serial.print("MQTT failed (");
Serial.print(client.state()); // Print failure status code
Serial.println("), retry in 5s...");
delay(5000); // Retry after 5s
}
}
void setup() {
Serial.begin(115200); // Initialize serial port (baud rate 115200, for debugging)
Serial.println("Starting...");
// Configure pin modes
pinMode(BUTTON_PIN, INPUT_PULLUP); // Button (pull-up input, high when not pressed)
pinMode(LED1_PIN, OUTPUT); // LED1 (output)
pinMode(LED2_PIN, OUTPUT); // LED2 (output)
pinMode(BUZZER_PIN, OUTPUT); // Buzzer (output)
pinMode(GAS_PIN, INPUT); // Gas sensor (input)
pinMode(PIR_PIN, INPUT); // PIR sensor (input)
// Initialize pin states
digitalWrite(LED1_PIN, LOW); // LED1 off
digitalWrite(LED2_PIN, HIGH); // LED2 on (indicates running)
digitalWrite(BUZZER_PIN, LOW); // Buzzer off
lcd.init(); // Initialize LCD
lcd.backlight(); // Turn on backlight
lcd.createChar(0, dot); // Create custom character (index 0, for temperature "°")
lcd.clear();
lcd.print("Initializing..."); // Show initialization prompt
delay(1000);
dht.begin(); // Initialize DHT sensor
setup_wifi(); // Connect to WiFi
// Configure MQTT client
client.setServer(mqtt_server, 1883); // Set MQTT server and port (1883 is default)
client.setCallback(callback); // Set message callback function
client.setKeepAlive(120); // Keep-alive interval 120s
client.setSocketTimeout(5000); // Connection timeout 5s
// Configure NTP time synchronization (time zone UTC+8, server pool.ntp.org)
configTime(8 * 3600, 0, "pool.ntp.org");
struct tm timeinfo;
int timeRetry = 0;
// Wait for time synchronization, max 5 retries
while (!getLocalTime(&timeinfo) && timeRetry < 5) {
Serial.println("Waiting for time...");
delay(1000);
timeRetry++;
}
if (timeRetry < 5) { // Synchronization successful
Serial.println("Time synced");
displayWelcome(); // Show welcome page
lastDisplayChange = millis(); // Update display timestamp
lastMsg = 0; // Initialize message publication timestamp
} else { // Synchronization failed
Serial.println("Time sync failed!");
}
Serial.println("Entering loop"); // Enter main loop
}
void loop() {
client.loop(); // Process MQTT client events (e.g., receive messages)
if (!client.connected()) reconnect(); // Reconnect MQTT if disconnected
handleButton(); // Process button input
handlePIR(); // Process PIR motion sensor
unsigned long now = millis(); // Current time
// Read sensor data every sensorReadInterval (2s)
if (now - lastSensorRead >= sensorReadInterval) {
lastSensorRead = now;
checkSensors(); // Check sensors and judge alarm
}
if (isAlarm) { // Alarm state
controlBuzzer(true); // Turn on buzzer
// Toggle LED1 state every 250ms (blink)
if (now - lastBuzzerToggle >= 250) {
lastBuzzerToggle = now;
digitalWrite(LED1_PIN, !digitalRead(LED1_PIN)); // Invert LED1 state
}
} else { // Normal state
controlBuzzer(false); // Turn off buzzer
digitalWrite(LED1_PIN, LOW); // LED1 off
}
if (isAlarm) { // Update LCD display every second when alarming
if (now - lastDisplayChange >= 1000) {
lastDisplayChange = now;
displaySensorData(); // Show alarm information
}
} else { // Normal state
// Update current display page when motion state changes
if (motionStateChanged && !restartTriggered) {
motionStateChanged = false;
if (displayState == 1) displayDateTime(); // Refresh time if currently showing time
else if (displayState == 2) displaySensorData(); // Refresh sensor data if currently showing
}
// Switch LCD display page every displayInterval (2s) (welcome → time → sensor data cycle)
if (now - lastDisplayChange >= displayInterval && !restartTriggered) {
lastDisplayChange = now;
displayState = (displayState + 1) % 3; // Cycle through states (0→1→2→0)
switch (displayState) {
case 0: displayWelcome(); break;
case 1: displayDateTime(); break;
case 2: displaySensorData(); break;
}
}
}
}