/*
* IoT Intruder Detection — Digital Twin Firmware
* ESP32 + HC-SR04 + PIR (HC-SR501) + Reed Switch + Buzzer + LED
*
* Features:
* - Sensor fusion scoring (0–100 risk score)
* - 5-state machine: IDLE → DETECTING → ALARM → COOLDOWN → DISARMED
* - MQTT over WiFi (publishes rich JSON event payloads)
* - MQTT subscribe for remote DISARM command
* - Alarm cooldown to suppress repeated alerts
* - Non-blocking, millis()-based timing throughout
*
* Dependencies (install via Arduino Library Manager):
* - PubSubClient by Nick O'Leary
* - ArduinoJson by Benoit Blanchon
*/
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
// ─── WiFi & MQTT config ───────────────────────────────────────────────────────
const char* WIFI_SSID = "Wokwi-GUEST";
const char* WIFI_PASSWORD = "";
const char* MQTT_BROKER = "broker.hivemq.com"; // or your local Mosquitto IP
const int MQTT_PORT = 1883;
const char* MQTT_USER = ""; // leave blank for public broker
const char* MQTT_PASS = "";
const char* DEVICE_ID = "door-sensor8-01";
// MQTT topics
const char* TOPIC_EVENTS = "home/security/events";
const char* TOPIC_STATUS = "home/security/status";
const char* TOPIC_COMMAND = "home/security/command"; // subscribe for DISARM
// ─── Pin definitions ─────────────────────────────────────────────────────────
const int PIN_TRIG = 4; // HC-SR04 trigger
const int PIN_ECHO = 5; // HC-SR04 echo
const int PIN_PIR = 18; // PIR motion output
const int PIN_REED = 23; // Reed switch (INPUT_PULLUP, LOW = door open)
const int PIN_BUZZER = 22; // Active buzzer (or NPN base via 1kΩ)
const int PIN_LED = 19; // Status LED
// ─── Tuning parameters ───────────────────────────────────────────────────────
const float DOOR_BASELINE_CM = 30.0; // measured distance when door is closed
const float DOOR_THRESHOLD_CM = 8.0; // delta that counts as "door opened"
const int COOLDOWN_MS = 30000; // 30s quiet period after alarm
const int ALARM_BUZZ_INTERVAL = 200; // buzzer beep period ms
const int DETECT_CONFIRM_MS = 500; // sensor must agree for this long before alarming
const int MQTT_RECONNECT_MS = 5000;
const int STATUS_PUBLISH_MS = 10000; // heartbeat publish interval
// ─── State machine ───────────────────────────────────────────────────────────
enum SecurityState { IDLE, DETECTING, ALARM, COOLDOWN, DISARMED };
SecurityState state = IDLE;
// ─── Runtime state ───────────────────────────────────────────────────────────
float lastDistanceCm = DOOR_BASELINE_CM;
bool pirTriggered = false;
bool reedOpen = false;
int riskScore = 0;
uint32_t stateEnteredAt = 0;
uint32_t detectStartedAt = 0;
uint32_t lastBuzzToggle = 0;
uint32_t lastMqttAttempt = 0;
uint32_t lastStatusPublish = 0;
bool buzzState = false;
char lastEventSummary[256] = "";
WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);
// ─── Utility: measure distance ───────────────────────────────────────────────
float measureDistanceCm() {
digitalWrite(PIN_TRIG, LOW);
delayMicroseconds(2);
digitalWrite(PIN_TRIG, HIGH);
delayMicroseconds(10);
digitalWrite(PIN_TRIG, LOW);
long duration = pulseIn(PIN_ECHO, HIGH, 30000); // 30ms timeout
if (duration == 0) return -1.0; // out of range
return (duration * 0.0343) / 2.0;
}
// ─── Risk scoring ─────────────────────────────────────────────────────────────
// Returns 0–100 based on which sensors fired and how much
int calcRiskScore(float distCm, bool pir, bool reed) {
int score = 0;
// Door distance change
if (distCm > 0) {
float delta = abs(distCm - DOOR_BASELINE_CM);
if (delta > DOOR_THRESHOLD_CM) {
score += 30;
if (delta > DOOR_THRESHOLD_CM * 2) score += 20; // door swung wide
}
}
// PIR motion
if (pir) score += 35;
// Reed switch (most reliable — physical contact)
if (reed) score += 35;
return min(score, 100);
}
// ─── Build event JSON payload ─────────────────────────────────────────────────
void publishEvent(const char* eventType) {
StaticJsonDocument<384> doc;
doc["device_id"] = DEVICE_ID;
doc["event"] = eventType;
doc["state"] = stateName(state);
doc["risk_score"] = riskScore;
doc["distance_cm"] = (int)lastDistanceCm;
doc["pir"] = pirTriggered;
doc["reed_open"] = reedOpen;
doc["timestamp_ms"] = millis();
// Human-readable summary for AI Agent in n8n
buildSummary(doc);
char payload[384];
serializeJson(doc, payload);
mqtt.publish(TOPIC_EVENTS, payload, true); // retained = true
Serial.print("[MQTT] Published → "); Serial.println(payload);
}
void buildSummary(JsonDocument& doc) {
char summary[200];
snprintf(summary, sizeof(summary),
"Risk %d/100. Distance %.0fcm (baseline %.0fcm). PIR: %s. Reed: %s.",
riskScore,
lastDistanceCm, DOOR_BASELINE_CM,
pirTriggered ? "MOTION" : "clear",
reedOpen ? "OPEN" : "closed"
);
doc["summary"] = summary;
strncpy(lastEventSummary, summary, sizeof(lastEventSummary));
}
const char* stateName(SecurityState s) {
switch(s) {
case IDLE: return "IDLE";
case DETECTING: return "DETECTING";
case ALARM: return "ALARM";
case COOLDOWN: return "COOLDOWN";
case DISARMED: return "DISARMED";
}
return "UNKNOWN";
}
// ─── Publish heartbeat status ─────────────────────────────────────────────────
void publishStatus() {
StaticJsonDocument<256> doc;
doc["device_id"] = DEVICE_ID;
doc["state"] = stateName(state);
doc["uptime_ms"] = millis();
doc["distance_cm"] = (int)lastDistanceCm;
doc["ip"] = WiFi.localIP().toString();
char payload[256];
serializeJson(doc, payload);
mqtt.publish(TOPIC_STATUS, payload, true);
}
// ─── MQTT command handler ─────────────────────────────────────────────────────
void onMqttMessage(char* topic, byte* payload, unsigned int len) {
char msg[64];
len = min(len, (unsigned int)63);
memcpy(msg, payload, len);
msg[len] = '\0';
Serial.print("[MQTT CMD] "); Serial.println(msg);
StaticJsonDocument<128> doc;
if (deserializeJson(doc, msg) == DeserializationError::Ok) {
const char* cmd = doc["command"] | "";
if (strcmp(cmd, "DISARM") == 0) {
transitionTo(DISARMED);
Serial.println("[STATE] Remote DISARM received");
} else if (strcmp(cmd, "ARM") == 0) {
transitionTo(IDLE);
Serial.println("[STATE] Remote ARM received");
}
}
}
// ─── State transitions ────────────────────────────────────────────────────────
void transitionTo(SecurityState next) {
Serial.print("[STATE] "); Serial.print(stateName(state));
Serial.print(" → "); Serial.println(stateName(next));
state = next;
stateEnteredAt = millis();
if (next != ALARM) {
digitalWrite(PIN_BUZZER, LOW);
digitalWrite(PIN_LED, LOW);
buzzState = false;
}
if (next == ALARM) {
publishEvent("ALARM_TRIGGERED");
} else if (next == DISARMED) {
publishEvent("DISARMED");
} else if (next == COOLDOWN) {
publishEvent("COOLDOWN_START");
} else if (next == IDLE) {
publishEvent("ARMED");
}
}
// ─── WiFi ─────────────────────────────────────────────────────────────────────
void connectWiFi() {
Serial.print("[WiFi] Connecting to "); Serial.println(WIFI_SSID);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500); Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.print("\n[WiFi] Connected. IP: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("\n[WiFi] Failed — running offline");
}
}
void ensureMqtt() {
if (mqtt.connected()) return;
if (millis() - lastMqttAttempt < MQTT_RECONNECT_MS) return;
lastMqttAttempt = millis();
Serial.print("[MQTT] Connecting to "); Serial.print(MQTT_BROKER);
char clientId[32];
snprintf(clientId, sizeof(clientId), "esp32-%s", DEVICE_ID);
bool ok = (strlen(MQTT_USER) > 0)
? mqtt.connect(clientId, MQTT_USER, MQTT_PASS)
: mqtt.connect(clientId);
if (ok) {
Serial.println(" — connected");
mqtt.subscribe(TOPIC_COMMAND);
publishEvent("BOOT");
} else {
Serial.print(" — failed rc="); Serial.println(mqtt.state());
}
}
// ─── Setup ────────────────────────────────────────────────────────────────────
void setup() {
Serial.begin(115200);
Serial.println("\n[BOOT] IoT Intruder Detection v2.0");
pinMode(PIN_TRIG, OUTPUT);
pinMode(PIN_ECHO, INPUT);
pinMode(PIN_PIR, INPUT);
pinMode(PIN_REED, INPUT_PULLUP); // LOW = door open (switch closes to GND)
pinMode(PIN_BUZZER, OUTPUT);
pinMode(PIN_LED, OUTPUT);
digitalWrite(PIN_BUZZER, LOW);
digitalWrite(PIN_LED, LOW);
// Baseline distance calibration
Serial.println("[INIT] Calibrating baseline distance...");
float sum = 0;
for (int i = 0; i < 5; i++) {
float d = measureDistanceCm();
if (d > 0) sum += d;
delay(200);
}
// Uncomment to use auto-calibration:
// DOOR_BASELINE_CM = sum / 5.0;
Serial.printf("[INIT] Baseline: %.1f cm\n", DOOR_BASELINE_CM);
connectWiFi();
mqtt.setServer(MQTT_BROKER, MQTT_PORT);
mqtt.setCallback(onMqttMessage);
mqtt.setBufferSize(512);
ensureMqtt();
stateEnteredAt = millis();
Serial.println("[INIT] System armed. Entering IDLE.");
}
// ─── Main loop ────────────────────────────────────────────────────────────────
void loop() {
// MQTT keep-alive
if (WiFi.status() == WL_CONNECTED) {
ensureMqtt();
mqtt.loop();
}
uint32_t now = millis();
// Heartbeat
if (now - lastStatusPublish >= STATUS_PUBLISH_MS) {
lastStatusPublish = now;
if (mqtt.connected()) publishStatus();
}
// ── Read sensors ──────────────────────────────────────────────────────────
lastDistanceCm = measureDistanceCm();
pirTriggered = digitalRead(PIN_PIR) == HIGH;
reedOpen = digitalRead(PIN_REED) == LOW; // PULLUP: LOW = door open
bool distanceTriggered = (lastDistanceCm > 0) &&
(abs(lastDistanceCm - DOOR_BASELINE_CM) > DOOR_THRESHOLD_CM);
bool anySensorTriggered = distanceTriggered || pirTriggered || reedOpen;
riskScore = calcRiskScore(lastDistanceCm, pirTriggered, reedOpen);
// ── State machine ─────────────────────────────────────────────────────────
switch (state) {
case IDLE:
if (anySensorTriggered) {
detectStartedAt = now;
transitionTo(DETECTING);
}
break;
case DETECTING:
if (!anySensorTriggered) {
// Brief noise — reset
transitionTo(IDLE);
} else if (now - detectStartedAt >= DETECT_CONFIRM_MS) {
// Sustained trigger — alarm
transitionTo(ALARM);
}
break;
case ALARM: {
// Strobe buzzer + LED
if (now - lastBuzzToggle >= ALARM_BUZZ_INTERVAL) {
lastBuzzToggle = now;
buzzState = !buzzState;
digitalWrite(PIN_BUZZER, buzzState ? HIGH : LOW);
digitalWrite(PIN_LED, buzzState ? HIGH : LOW);
}
break;
}
case COOLDOWN:
if (now - stateEnteredAt >= COOLDOWN_MS) {
transitionTo(IDLE);
}
break;
case DISARMED:
// Stays disarmed until remote ARM command or physical reset
break;
}
// Serial debug every 2s
static uint32_t lastDebug = 0;
if (now - lastDebug >= 2000) {
lastDebug = now;
Serial.printf("[%s] dist=%.1fcm pir=%d reed=%d risk=%d\n",
stateName(state), lastDistanceCm, pirTriggered, reedOpen, riskScore);
}
delay(50); // 20Hz sensor poll rate
}