#include <SPI.h> // For SD card and camera SPI
#include <WiFi.h> // For ESP-NOW and HTTPClient
#include <esp_now.h> // ESP-NOW library
#include <SD.h> // SD card library
#include <HTTPClient.h> // For sending data to Django
#include <HardwareSerial.h> // For GSM/GPS modules, if using hardware serial
#include <TinyGPSPlus.h> // For GPS data parsing (often used with Neo6M)
#include <WiFiClientSecure.h> // For HTTPS if using Cloudflare Tunnel
// Define pins for SD Card (from your secondary_board.json)
#define SD_CS 13 // Connected to sd1:CS
#define SD_SCK 14 // Connected to sd1:SCK
#define SD_MISO 12 // Connected to sd1:DO (Data Out from SD)
#define SD_MOSI 15 // Connected to sd1:DI (Data In to SD)
const char* LOG_FILE_NAME = "/incident_log.txt"; // A specific log file for this board
// === ESP-NOW Communication ===
// MAC address of the Main ESP32 board
uint8_t main_board_mac_address[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; // <-- **IMPORTANT: REPLACE WITH ACTUAL MAC ADDRESS OF MAIN ESP32**
// Structure to receive data from the main board (must match `struct_message_to_secondary` in main sketch)
typedef struct struct_message_from_main {
char command[32]; // e.g., "FIRE_ALERT", "GAS_ALERT", "RESET_SYSTEM", "CAPTURE_IMAGE", "GET_GPS"
float temperature;
int gas_analog;
bool fire_triggered;
bool gas_triggered;
} struct_message_from_main;
// Structure to send data to the main board (must match `struct_message_from_secondary` in main sketch)
typedef struct struct_message_to_main {
char status[32]; // e.g., "IMAGE_CAPTURED", "GPS_DATA", "SMS_SENT", "HTTP_OK"
char image_url[128]; // URL for the captured image (if applicable)
float latitude;
float longitude;
char detail[64]; // Any additional details
} struct_message_to_main;
// Create objects for receiving and sending
struct_message_from_main receivedCommand;
struct_message_to_main responseData;
// === Thresholds (Duplicate from Main Board, needed for payload logic) ===
#define GAS_ANALOG_THRESHOLD 600 // Analog threshold for gas detection (adjust as needed, must match main)
// Callback when data is sent
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
Serial.print("\r\nLast Packet Send Status to Main Board:\t");
Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail");
}
// Callback when data is received from the main board
void OnDataRecv(const esp_now_recv_info * info, const uint8_t *incomingData, int len) {
// The MAC address of the sender can be accessed via info->src_addr if needed
// const uint8_t * mac = info->src_addr;
memcpy(&receivedCommand, incomingData, sizeof(receivedCommand));
Serial.print("Command from Main Board received: ");
Serial.println(receivedCommand.command);
Serial.printf(" Temp: %.1fC, Gas: %d, Fire: %d, Gas: %d\n",
receivedCommand.temperature, receivedCommand.gas_analog,
receivedCommand.fire_triggered, receivedCommand.gas_triggered);
// --- Process received command ---
if (strcmp(receivedCommand.command, "FIRE_ALERT") == 0 || strcmp(receivedCommand.command, "GAS_ALERT") == 0) {
// Acknowledge the alert, then proceed with actions
logEvent(receivedCommand.command, receivedCommand.temperature, receivedCommand.gas_analog, "Incoming alert from Main Board.");
// Trigger camera capture (placeholder for actual camera code)
Serial.println("Initiating Camera Capture...");
String imageUrl = "http://wokwi.com/simulated_image.jpg"; // Simulate image URL
// Call your actual camera capture function here
captureImageAndUpload(&imageUrl); // Call actual function
// Get GPS data (placeholder for actual GPS code)
Serial.println("Getting GPS Data...");
float currentLat = 0.0; // Initialize to 0.0 before trying to get fix
float currentLon = 0.0; // Initialize to 0.0 before trying to get fix
getGPSCoordinates(¤tLat, ¤tLon); // Call actual function
// Send HTTP POST to Django
sendIncidentReportToDjango(receivedCommand.command, receivedCommand.temperature, receivedCommand.gas_analog, imageUrl, currentLat, currentLon);
// Send SMS (placeholder for actual SMS code)
Serial.println("Sending SMS Alert...");
// CONVERSION FIX: Convert String to const char* using .c_str()
String smsMessage = "Fire/Gas detected! Check dashboard. Image: " + imageUrl + " Lat: " + String(currentLat, 6) + " Lon: " + String(currentLon, 6);
sendSMSAlert(smsMessage.c_str(), "+1234567890");
// Respond to Main Board after actions
strncpy(responseData.status, "ACTIONS_COMPLETE", sizeof(responseData.status) - 1);
strncpy(responseData.image_url, imageUrl.c_str(), sizeof(responseData.image_url) - 1);
responseData.latitude = currentLat;
responseData.longitude = currentLon;
strncpy(responseData.detail, "Image & GPS acquired, HTTP/SMS sent", sizeof(responseData.detail) - 1);
esp_now_send(main_board_mac_address, (uint8_t *) &responseData, sizeof(responseData));
} else if (strcmp(receivedCommand.command, "RESET_SYSTEM") == 0) {
logEvent(receivedCommand.command, receivedCommand.temperature, receivedCommand.gas_analog, "System reset command received from Main Board.");
// Clear any active alarms or states on this board if needed
Serial.println("Secondary Board reset/all clear confirmed.");
strncpy(responseData.status, "RESET_CONFIRMED", sizeof(responseData.status) - 1);
responseData.image_url[0] = '\0'; // Clear previous image URL
responseData.latitude = 0;
responseData.longitude = 0;
strncpy(responseData.detail, "System is now clear", sizeof(responseData.detail) - 1);
esp_now_send(main_board_mac_address, (uint8_t *) &responseData, sizeof(responseData));
}
}
// === Log Function (for Secondary Board's SD card) ===
void logEvent(const char* event_type, float temp, int gas, const char* message) {
unsigned long timestamp_ms = millis();
char logEntry[300];
snprintf(logEntry, sizeof(logEntry), "[%lu ms] Event: %s | Temp: %.1fC | Gas: %d | Detail: %s",
timestamp_ms, event_type, temp, gas, message);
File file = SD.open(LOG_FILE_NAME, FILE_APPEND);
if (file) {
file.println(logEntry);
file.close();
Serial.print("✅ Secondary Board Logged: ");
Serial.println(logEntry);
} else {
Serial.println("⚠️ SD ERROR on Secondary Board: Failed to write to log.txt");
}
}
// === Wi-Fi + Django API (for Secondary Board to send data) ===
const char* ssid = "Wokwi-GUEST";
const char* password = "";
// IMPORTANT: REPLACE THIS URL WITH YOUR CURRENT, ACTIVE CLOUDFLARE TUNNEL URL!
// This URL CHANGES every time you restart your Cloudflare Tunnel.
const char* django_api_host = "REPLACE_WITH_YOUR_CURRENT_CLOUDFLARE_TUNNEL_URL"; // <--- UPDATE THIS LINE!
const char* django_api_path = "/api/incidents/post/";
const bool USE_HTTPS = true; // This should be true for Cloudflare Tunnel as well
// === Device Info (for this secondary board) ===
const char* DEVICE_LOCATION = "Main System Location (from secondary)"; // You might want to get this from Main Board or keep consistent
const uint64_t chipId = ESP.getEfuseMac(); // Unique ID for this board
unsigned int globalIncidentCounter = 0; // For unique incident IDs
void sendIncidentReportToDjango(const char* type, float temp, int gas, const String& imageUrl, float lat, float lon) {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("❌ WiFi not connected on Secondary Board, cannot send report.");
logEvent(type, temp, gas, "WiFi DISCONNECTED, HTTP report not sent.");
return;
}
HTTPClient http;
String serverPath = (USE_HTTPS ? "https://" : "http://") + String(django_api_host) + String(django_api_path);
Serial.print("Secondary Board Sending POST to: "); Serial.println(serverPath);
if (USE_HTTPS) {
WiFiClientSecure client;
client.setInsecure(); // WARNING: This disables certificate validation. Use only for testing.
http.begin(client, serverPath);
} else {
http.begin(serverPath);
}
http.addHeader("Content-Type", "application/json");
http.addHeader("Authorization", "Token firekey123device"); // Assuming this token applies to both
globalIncidentCounter++;
char incident_id[40];
snprintf(incident_id, sizeof(incident_id), "INC%llu_%lu_%u", chipId, millis(), globalIncidentCounter);
String payload = "{";
payload += "\"incident_id\": \"" + String(incident_id) + "\", ";
payload += "\"location\": \"" + String(DEVICE_LOCATION) + "\", ";
payload += "\"latitude\": " + String(lat, 6) + ", "; // Use actual GPS lat
payload += "\"longitude\": " + String(lon, 6) + ", "; // Use actual GPS lon
payload += "\"temperature\": " + String(temp, 1) + ", ";
// FIXED: GAS_ANALOG_THRESHOLD is now defined
payload += "\"gas_detected\": " + String(gas > GAS_ANALOG_THRESHOLD ? "true" : "false") + ", ";
payload += "\"smoke_detected\": " + String(receivedCommand.fire_triggered ? "true" : "false") + ", "; // Use value from main board
payload += "\"image_url\": \"" + imageUrl + "\", "; // Include image URL
if (String(type) == "GAS_ALERT") {
payload += "\"type\": \"Gas Leak\"}";
} else if (String(type) == "FIRE_ALERT") {
payload += "\"type\": \"Fire\"}";
} else if (String(type) == "RESET_SYSTEM") {
payload += "\"type\": \"Other\"}";
} else {
payload += "\"type\": \"" + String(type) + "\"}";
}
Serial.print("Secondary Board Payload: "); Serial.println(payload);
int httpResponseCode = http.POST(payload);
if (httpResponseCode > 0) {
Serial.printf("Secondary Board HTTP Response code: %d\n", httpResponseCode);
String response = http.getString();
Serial.println("Secondary Board API Response: " + response);
logEvent(type, temp, gas, (String("API Report SUCCESS (Code: ") + httpResponseCode + ")").c_str());
// Send success back to main board
strncpy(responseData.status, "HTTP_OK", sizeof(responseData.status) - 1);
strncpy(responseData.detail, response.c_str(), sizeof(responseData.detail) - 1); // Send back API response as detail
esp_now_send(main_board_mac_address, (uint8_t *) &responseData, sizeof(responseData));
} else {
Serial.printf("❌ Secondary Board HTTP Error: %s\n", http.errorToString(httpResponseCode).c_str());
logEvent(type, temp, gas, (String("API Report FAILED (Code: ") + httpResponseCode + ")").c_str());
// Send failure back to main board
strncpy(responseData.status, "HTTP_FAIL", sizeof(responseData.status) - 1);
strncpy(responseData.detail, http.errorToString(httpResponseCode).c_str(), sizeof(responseData.detail) - 1);
esp_now_send(main_board_mac_address, (uint8_t *) &responseData, sizeof(responseData));
}
http.end();
}
// === GPS setup ===
HardwareSerial GPS_Serial(1); // Use UART1 for GPS, pins esp:16 (TX) and esp:17 (RX)
TinyGPSPlus gps;
void setupGPS() {
GPS_Serial.begin(9600, SERIAL_8N1, 17, 16); // RX, TX (from secondary_board.json, gps1:TX to esp:16, gps1:RX to esp:17)
Serial.println("GPS Serial initialized.");
}
void getGPSCoordinates(float* lat, float* lon) {
unsigned long start_time = millis();
while (millis() - start_time < 5000) { // Try to get GPS fix for 5 seconds
while (GPS_Serial.available() > 0) {
if (gps.encode(GPS_Serial.read())) {
if (gps.location.isValid()) {
*lat = gps.location.lat();
*lon = gps.location.lng();
Serial.printf("GPS Fixed: Lat=%.6f, Lon=%.6f\n", *lat, *lon);
return;
}
}
}
// If no data or no fix, allow other tasks to run briefly
delay(10);
}
Serial.println("GPS Fix not obtained within timeout.");
*lat = 0.0; // Indicate no fix
*lon = 0.0; // Indicate no fix
}
// Placeholder for Camera Capture function (depends on OV5642 library/implementation)
void captureImageAndUpload(String* imageUrl) {
// This is where your custom OV5642 camera code would go.
// It should take a picture, save it to SD, and ideally simulate uploading it
// to a server to get a URL.
Serial.println("Simulating camera capture and upload...");
// For now, just a dummy URL
*imageUrl = "http://wokwi.com/simulated_image_upload_path/img_" + String(millis()) + ".jpg";
// If you want to integrate the actual camera, you'll need the libraries and
// functions to control the OV5642.
}
// Placeholder for SMS Sending function (depends on GSM module AT commands or library)
// Using HardwareSerial2 for GSM, as chip1:RXD goes to esp:4 and chip1:TXD goes to esp:2
HardwareSerial GSM_Serial(2); // Use UART2 for GSM
void sendSMSAlert(const char* message, const char* phoneNumber) {
Serial.println("Simulating SMS sending...");
// Implement GSM AT command sequence here
// Example:
// GSM_Serial.println("AT"); delay(100);
// GSM_Serial.println("AT+CMGF=1"); delay(100); // Set SMS text mode
// GSM_Serial.printf("AT+CMGS=\"%s\"\r\n", phoneNumber); delay(100);
// GSM_Serial.print(message);
// GSM_Serial.write(0x1A); // Ctrl+Z to send SMS
// delay(5000); // Wait for SMS to be sent
Serial.printf("SMS sent to %s: \"%s\"\n", phoneNumber, message);
logEvent("SMS_SENT", 0, 0, (String("SMS to ") + phoneNumber + ": " + message).c_str());
// You might want to get the phone number dynamically or from config
}
// === Setup ===
void setup() {
Serial.begin(115200);
Serial.println("--- Starting EmberQuenched Setup (Secondary Board) ---");
// Initialize SPI for SD Card using specific pins
SPI.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS);
Serial.println("\nInitializing SD card on Secondary Board...");
if (!SD.begin(SD_CS)) {
Serial.println("⚠️ Secondary Board SD Card initialization failed! Check wiring & card.");
Serial.println("This board will not be able to log incidents to SD.");
// Consider a retry or halt if SD logging is critical
} else {
Serial.println("Secondary Board SD Card Initialized OK!");
if (!SD.exists(LOG_FILE_NAME)) {
File file = SD.open(LOG_FILE_NAME, FILE_WRITE);
if (file) {
file.println("--- EmberQuenched Secondary Log Start ---");
file.close();
Serial.printf("Created new log file on Secondary Board: %s\n", LOG_FILE_NAME);
} else {
Serial.printf("Error: Could not create log file on Secondary Board: %s!\n", LOG_FILE_NAME);
}
}
logEvent("System", 0, 0, "Secondary Board Initialized & Ready");
}
// Set up ESP-NOW
WiFi.mode(WIFI_STA);
Serial.print("Secondary Board MAC Address: ");
Serial.println(WiFi.macAddress());
if (esp_now_init() != ESP_OK) {
Serial.println("Error initializing ESP-NOW on Secondary Board");
while(true) { delay(100); } // Halt if ESP-NOW fails
}
esp_now_register_send_cb(OnDataSent);
esp_now_register_recv_cb(OnDataRecv);
// Register peer (main board)
esp_now_peer_info_t peerInfo;
memcpy(peerInfo.peer_addr, main_board_mac_address, 6);
peerInfo.channel = 0;
peerInfo.encrypt = false;
if (esp_now_add_peer(&peerInfo) != ESP_OK){
Serial.println("Failed to add peer (Main Board)");
} else {
Serial.println("Main Board peer added successfully.");
}
// Initialize WiFi for HTTP client
Serial.print("\nSecondary Board Connecting to WiFi ");
Serial.println(ssid);
WiFi.begin(ssid, password);
for (int i = 0; i < 20 && WiFi.status() != WL_CONNECTED; i++) {
delay(500); Serial.print(".");
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nSecondary Board WiFi Connected!");
Serial.print("Secondary Board IP Address: ");
Serial.println(WiFi.localIP());
logEvent("Network", 0, 0, (String("WiFi Connected: ") + WiFi.localIP().toString()).c_str());
} else {
Serial.println("\n❌ Secondary Board WiFi Connection Failed!");
logEvent("Network", 0, 0, "WiFi Connection Failed!");
}
// Initialize GPS (UART1: RX=17, TX=16)
setupGPS();
// Initialize GSM (UART2: RX=4, TX=2) - Note: TX/RX for GSM can be tricky.
// Ensure you're connecting chip1:RXD to ESP:TX and chip1:TXD to ESP:RX
// Your diagram shows chip1:RXD to esp:4 (RX), chip1:TXD to esp:2 (TX)
// So for the ESP32, you'd define the pins for HardwareSerial2 as:
// GSM_Serial.begin(9600, SERIAL_8N1, 4, 2); // RX pin, TX pin
// If chip1:RXD connects to ESP:4 (RX pin of ESP), then ESP:4 is the RX for the ESP's UART.
// If chip1:TXD connects to ESP:2 (TX pin of ESP), then ESP:2 is the TX for the ESP's UART.
// This looks like typical serial communication, but double-check your physical wiring.
GSM_Serial.begin(9600, SERIAL_8N1, 4, 2); // RX pin, TX pin (esp:4 is RX for GSM module, esp:2 is TX for GSM module)
Serial.println("GSM Serial Initialized.");
Serial.println("Secondary Board Setup done. Awaiting commands from Main Board...");
}
// === Loop ===
void loop() {
// The main loop for the secondary board will mostly be idle,
// waiting for ESP-NOW commands in the `OnDataRecv` callback.
// It should, however, continuously read GPS data in case a request comes in.
while (GPS_Serial.available() > 0) {
gps.encode(GPS_Serial.read());
}
// Any other background tasks for the secondary board can go here,
// but avoid long blocking operations as they will delay ESP-NOW response.
delay(10); // Small delay to yield to other tasks
}