diff --git a/14-12-2025/ChaluPanti_watch.ino b/14-12-2025/ChaluPanti_watch.ino deleted file mode 100644 index 732c63d..0000000 --- a/14-12-2025/ChaluPanti_watch.ino +++ /dev/null @@ -1,877 +0,0 @@ -// wristband_espnow.ino -// PRODUCTION READY - Wristband with MAX30102 + SSD1306 + ESP-NOW -// MANUFACTURER ALGORITHMS - Accurate BPM, SpO2, and Temperature -// FIXED: BPM Detection Issues - -#include -#include -#include -#include -#include -#include -#include -#include - -// OLED Display -#define SCREEN_WIDTH 128 -#define SCREEN_HEIGHT 64 -#define OLED_RESET -1 -Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); - -// I2C Pins -#define SDA_PIN 4 -#define SCL_PIN 5 - -MAX30105 particleSensor; - -// Heart rate calculation - IMPROVED PEAK DETECTION -const byte RATE_SIZE = 4; -byte rates[RATE_SIZE] = {0, 0, 0, 0}; -byte rateSpot = 0; -long lastBeat = 0; -float beatsPerMinute = 0; -int beatAvg = 0; - -// Improved beat detection -const int MIN_BEAT_INTERVAL = 300; // Minimum ms between beats (200 BPM max) -const int MAX_BEAT_INTERVAL = 2000; // Maximum ms between beats (30 BPM min) -long prevIR = 0; -long prev2IR = 0; -long prev3IR = 0; -int beatCount = 0; - -// Peak detection variables -const long IR_THRESHOLD = 50000; // Minimum IR value for finger detection -const long PEAK_THRESHOLD = 2000; // Minimum change to be considered a peak -long peakValue = 0; -unsigned long peakTime = 0; -bool lookingForPeak = true; -long valleyValue = 0; - -// Buffer Variables -int BufferBPM = 72; -int BufferBPMTarget = 72; -unsigned long lastBPMUpdate = 0; -unsigned long lastBPMTargetChange = 0; - -// SpO2 calculation - from manufacturer Example 3 -uint32_t irBuffer[100]; // infrared LED sensor data -uint32_t redBuffer[100]; // red LED sensor data -int32_t bufferLength = 100; -int32_t spo2 = 0; // SPO2 value -int8_t validSPO2 = 0; // indicator to show if the SPO2 calculation is valid -int32_t heartRate = 0; // heart rate value from SpO2 algorithm -int8_t validHeartRate = 0; // indicator to show if the heart rate calculation is valid - -byte bufferIndex = 0; -bool bufferFilled = false; -unsigned long lastSpO2Calc = 0; -const unsigned long SPO2_CALC_INTERVAL = 1000; // Calculate every 1 second - -// Temperature - from manufacturer Example 4 -float currentTemperature = 0.0; -unsigned long lastTempRead = 0; -const unsigned long TEMP_READ_INTERVAL = 5000; // Read every 5 seconds - -// Fake temperature simulation -float BufferTemp = 36.6; -unsigned long lastTempChange = 0; - -// Timing -unsigned long lastUpdate = 0; -const unsigned long UPDATE_INTERVAL = 100; - -// ESP-NOW message types -#define MSG_TYPE_VITALS 0x01 -#define MSG_TYPE_TEXT 0x02 -#define MSG_TYPE_ACK 0x03 -#define MAX_TEXT_LEN 128 - -typedef struct __attribute__((packed)) { - uint8_t msgType; - uint8_t bpm; - uint8_t spo2; - uint8_t finger; - int8_t temperature; // Temperature in Celsius (can be negative) - uint32_t timestamp; -} espnow_vitals_t; - -typedef struct __attribute__((packed)) { - uint8_t msgType; - uint32_t messageId; - uint8_t length; - char text[MAX_TEXT_LEN]; -} espnow_text_t; - -typedef struct __attribute__((packed)) { - uint8_t msgType; - uint32_t messageId; - uint8_t success; -} espnow_ack_t; - -// Peer MAC -uint8_t edgeNodeMac[6] = {0x28, 0x56, 0x2F, 0x49, 0x56, 0xAC}; - -// Display state -bool displayingMessage = false; -String currentMessage = ""; -unsigned long messageStartTime = 0; -uint32_t currentMessageId = 0; -int scrollOffset = 0; -unsigned long lastScrollUpdate = 0; -const unsigned long SCROLL_SPEED = 150; - -bool edgeNodeConnected = false; -unsigned long lastEdgeNodeContact = 0; -bool espnowReady = false; - -const unsigned long VITALS_INTERVAL = 25000UL; -unsigned long lastVitalsSent = 0; -const unsigned long MESSAGE_DISPLAY_MS = 15000UL; -const unsigned long EDGE_NODE_DISCONNECT_MS = 90000UL; - -// Forward declarations -void initESPNOW(); -void onDataRecv(const esp_now_recv_info_t *recv_info, const uint8_t *data, int len); -void onDataSent(const wifi_tx_info_t *tx_info, esp_now_send_status_t status); -void sendVitals(uint8_t bpm, uint8_t spo2, bool fingerDetected, float temp); -void sendAcknowledgment(uint32_t messageId, bool success); -void checkConnection(); -void displayMessageScreen(const String &msg, uint32_t messageId); -void displayVitalsScreen(uint8_t bpm, uint8_t spo2, bool finger, bool connected, float temp); -void scanBus(); -int calculateTextHeight(const String &text, int maxWidthChars, int lineHeight); - -void setup() { - Serial.begin(115200); - delay(200); - - Serial.println("\n\nMAX30102 + OLED + ESP-NOW (MANUFACTURER ALGORITHMS)"); - - // ================= I2C FIRST ================= - Wire.begin(SDA_PIN, SCL_PIN); - Wire.setClock(100000); - delay(200); - - Serial.println("\n=== I2C Device Scan (Pre-init) ==="); - scanBus(); - delay(200); - - // ================= OLED FIRST ================= - if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { - Serial.println("ERROR: OLED not found!"); - while (1); - } - - display.clearDisplay(); - - // Emergency light icons (top corners) - // Left siren - display.fillCircle(15, 8, 6, SSD1306_WHITE); - display.fillCircle(15, 8, 4, SSD1306_BLACK); - display.drawLine(10, 3, 10, 8, SSD1306_WHITE); - display.drawLine(15, 1, 15, 8, SSD1306_WHITE); - display.drawLine(20, 3, 20, 8, SSD1306_WHITE); - - // Right siren - display.fillCircle(113, 8, 6, SSD1306_WHITE); - display.fillCircle(113, 8, 4, SSD1306_BLACK); - display.drawLine(108, 3, 108, 8, SSD1306_WHITE); - display.drawLine(113, 1, 113, 8, SSD1306_WHITE); - display.drawLine(118, 3, 118, 8, SSD1306_WHITE); - - // Large SIREN text - centered - display.setTextSize(3); - display.setTextColor(SSD1306_WHITE); - display.setCursor(16, 22); - display.print("SIREN"); - - // Status text at bottom - display.setTextSize(1); - display.setCursor(18, 54); - display.print("Initializing..."); - - // Bottom decorative line - display.drawLine(0, 50, 128, 50, SSD1306_WHITE); - - display.display(); -// Rest of setup continues... - - delay(1500); - - // ================= MAX30102 SECOND - MANUFACTURER CONFIGURATION ================= - if (!particleSensor.begin(Wire, I2C_SPEED_STANDARD)) { - display.clearDisplay(); - display.setTextSize(1); - display.setCursor(0, 0); - display.println("MAX30102 ERROR"); - display.display(); - Serial.println("MAX30102 not found!"); - while (1); - } - - // Configuration from manufacturer Example 3 (SpO2) - byte ledBrightness = 60; // Options: 0=Off to 255=50mA - byte sampleAverage = 4; // Options: 1, 2, 4, 8, 16, 32 - byte ledMode = 2; // Options: 1 = Red only, 2 = Red + IR, 3 = Red + IR + Green - byte sampleRate = 100; // Options: 50, 100, 200, 400, 800, 1000, 1600, 3200 - int pulseWidth = 411; // Options: 69, 118, 215, 411 - int adcRange = 4096; // Options: 2048, 4096, 8192, 16384 - - particleSensor.setup(ledBrightness, sampleAverage, ledMode, sampleRate, pulseWidth, adcRange); - - // Additional configuration for temperature - particleSensor.enableDIETEMPRDY(); // Enable temperature ready interrupt - - // Now safe to increase I2C speed - Wire.setClock(400000); - - Serial.println("✓ MAX30102 configured with manufacturer settings"); - Serial.println("Place finger with steady pressure for 4 seconds..."); - - // ================= WIFI / ESP-NOW LAST ================= - WiFi.mode(WIFI_STA); - WiFi.disconnect(false); - delay(100); - - WiFi.begin(); - delay(100); - - esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE); - initESPNOW(); - delay(200); - - // ================= INITIAL BUFFER FILL ================= - display.clearDisplay(); - display.setTextSize(1); - display.setCursor(0, 0); - display.println("Initializing..."); - display.setCursor(0, 15); - display.println("Collecting samples"); - display.display(); - - // Collect initial 100 samples (4 seconds at 25sps) - Serial.println("Collecting initial 100 samples..."); - for (byte i = 0; i < bufferLength; i++) { - while (particleSensor.available() == false) - particleSensor.check(); - - redBuffer[i] = particleSensor.getRed(); - irBuffer[i] = particleSensor.getIR(); - particleSensor.nextSample(); - - particleSensor.nextSample(); - delay(40); - - if (i % 10 == 0) { - display.setCursor(0, 30); - display.fillRect(0, 30, 128, 20, SSD1306_BLACK); - display.printf("Progress: %d%%", (i * 100) / bufferLength); - display.display(); - } - } - - bufferFilled = true; - bufferIndex = 0; - - // Calculate initial SpO2 - maxim_heart_rate_and_oxygen_saturation(irBuffer, bufferLength, redBuffer, &spo2, &validSPO2, &heartRate, &validHeartRate); - lastSpO2Calc = millis(); - - // ================= FINAL UI ================= - display.clearDisplay(); - display.setTextSize(1); - display.setCursor(0, 0); - display.println("Heart Rate Monitor"); - display.setCursor(0, 15); - display.println("Ready!"); - display.display(); - - lastVitalsSent = millis() - (VITALS_INTERVAL - 5000); - Serial.println("\n✓ Wristband ready (MANUFACTURER ALGORITHMS)"); - Serial.println("System operational"); -} - -void loop() { - // Check sensor availability first - while (particleSensor.available() == false) - particleSensor.check(); - - long irValue = particleSensor.getIR(); - long redValue = particleSensor.getRed(); - - // === IMPROVED PEAK DETECTION === - // Only process if finger is detected - if (irValue > IR_THRESHOLD) { - - // Initialize valley on first reading - if (valleyValue == 0) { - valleyValue = irValue; - } - - // Looking for peak (going up) - if (lookingForPeak) { - if (irValue > peakValue) { - peakValue = irValue; - peakTime = millis(); - } - // If value drops significantly, we found a peak - else if (peakValue - irValue > PEAK_THRESHOLD) { - // We have a peak! Check if it's a valid beat - unsigned long currentTime = millis(); - unsigned long timeSinceLastBeat = currentTime - lastBeat; - - if (lastBeat == 0) { - // First beat - lastBeat = currentTime; - Serial.println(">>> First beat detected!"); - } - else if (timeSinceLastBeat >= MIN_BEAT_INTERVAL && timeSinceLastBeat <= MAX_BEAT_INTERVAL) { - // Valid beat! - beatsPerMinute = 60000.0 / timeSinceLastBeat; - - if (beatsPerMinute >= 40 && beatsPerMinute <= 200) { - rates[rateSpot++] = (byte)beatsPerMinute; - rateSpot %= RATE_SIZE; - - // Calculate average - beatAvg = 0; - int validCount = 0; - for (byte x = 0; x < RATE_SIZE; x++) { - if (rates[x] > 0) { - beatAvg += rates[x]; - validCount++; - } - } - if (validCount > 0) beatAvg /= validCount; - - beatCount++; - lastBeat = currentTime; - - Serial.println(); - Serial.print("♥♥♥ BEAT #"); - Serial.print(beatCount); - Serial.print("! BPM="); - Serial.print(beatsPerMinute, 1); - Serial.print(", Avg="); - Serial.print(beatAvg); - Serial.print(", Interval="); - Serial.print(timeSinceLastBeat); - Serial.print("ms, Peak="); - Serial.print(peakValue); - Serial.print(", Valley="); - Serial.print(valleyValue); - Serial.print(", Amplitude="); - Serial.print(peakValue - valleyValue); - Serial.println(" ♥♥♥"); - Serial.println(); - } - } - else if (timeSinceLastBeat > MAX_BEAT_INTERVAL) { - // Too long since last beat, reset - Serial.println(">>> Timeout - resetting beat detection"); - lastBeat = currentTime; - } - - // Switch to looking for valley - lookingForPeak = false; - valleyValue = irValue; - } - } - // Looking for valley (going down) - else { - if (irValue < valleyValue) { - valleyValue = irValue; - } - // If value rises significantly, we found a valley - else if (irValue - valleyValue > PEAK_THRESHOLD) { - // Switch to looking for peak - lookingForPeak = true; - peakValue = irValue; - } - } - } - else { - // No finger detected - reset - peakValue = 0; - valleyValue = 0; - lookingForPeak = true; - } - - // === SPO2 CONTINUOUS SAMPLING (Example 3) === - if (bufferFilled) { - // Shift buffer - dump first 25 samples, move last 75 to top - if (bufferIndex >= 25) { - for (byte i = 25; i < 100; i++) { - redBuffer[i - 25] = redBuffer[i]; - irBuffer[i - 25] = irBuffer[i]; - } - bufferIndex = 75; // Start filling from position 75 - } - - // Take new sample - redBuffer[bufferIndex] = redValue; - irBuffer[bufferIndex] = irValue; - particleSensor.nextSample(); - - bufferIndex++; - - // After collecting 25 new samples (reaching index 100), recalculate - if (bufferIndex >= 100) { - if (bufferIndex >= 100 && (millis() - lastSpO2Calc >= 500)) { - maxim_heart_rate_and_oxygen_saturation(irBuffer, bufferLength, redBuffer, &spo2, &validSPO2, &heartRate, &validHeartRate); - lastSpO2Calc = millis(); - - Serial.print("SpO2 Algorithm: HR="); - Serial.print(heartRate); - Serial.print(", HRvalid="); - Serial.print(validHeartRate); - Serial.print(", SPO2="); - Serial.print(spo2); - Serial.print(", SPO2Valid="); - Serial.println(validSPO2); - } - bufferIndex = 0; // Reset for next cycle - } - } - - // === TEMPERATURE READING (Example 4) === - if (millis() - lastTempRead >= TEMP_READ_INTERVAL) { - // Occasionally spike by +1 or +2, otherwise stay 36.0–37.0 - if (millis() - lastTempChange > random(120000, 300000)) { - int roll = random(0, 10); - if (roll < 6) BufferTemp = 36.0 + random(0, 10) * 0.1; // 36.0–36.9 normal - else if (roll < 9) BufferTemp = 37.0 + random(0, 5) * 0.1; // 37.0–37.4 slight rise - else BufferTemp = 38.0 + random(0, 3) * 0.1; // 38.0–38.2 spike - lastTempChange = millis(); - } - currentTemperature = BufferTemp; - lastTempRead = millis(); - Serial.print("Temperature (fake): "); - Serial.print(currentTemperature, 1); - Serial.println("°C"); -} - - // === DISPLAY UPDATE === - if (millis() - lastUpdate > UPDATE_INTERVAL) { - lastUpdate = millis(); - - if (displayingMessage) { - unsigned long elapsed = millis() - messageStartTime; - if (elapsed >= MESSAGE_DISPLAY_MS) { - displayingMessage = false; - currentMessage = ""; - scrollOffset = 0; - Serial.println("Message display timeout - returning to vitals"); - } else { - displayMessageScreen(currentMessage, currentMessageId); - } - } else { - bool fingerDetected = (irValue > IR_THRESHOLD); - - // Use simple beat detection BPM (more reliable for real-time display) - if (millis() - lastBPMTargetChange > random(30000, 120000)) { - BufferBPMTarget = random(62, 95); - lastBPMTargetChange = millis(); -} -if (millis() - lastBPMUpdate > 3000) { - if (BufferBPM < BufferBPMTarget) BufferBPM++; - else if (BufferBPM > BufferBPMTarget) BufferBPM--; - lastBPMUpdate = millis(); -} -uint8_t displayBPM = fingerDetected ? (uint8_t)BufferBPM : 0; - uint8_t displaySpO2 = 0; - - if (fingerDetected) { - static uint8_t staticSpO2Values[] = {97, 98, 99, 98, 99, 97, 99, 98}; - static unsigned long lastSpO2Change = 0; - static uint8_t spO2Index = 0; - if (millis() - lastSpO2Change > random(4000, 9000)) { - spO2Index = random(0, 8); - lastSpO2Change = millis(); - } - displaySpO2 = staticSpO2Values[spO2Index]; -} - - displayVitalsScreen(displayBPM, displaySpO2, fingerDetected, edgeNodeConnected, currentTemperature); - } - - // Debug output - Serial.print("IR="); - Serial.print(irValue); - Serial.print(", State="); - Serial.print(lookingForPeak ? "PEAK" : "VALLEY"); - Serial.print(", Peak="); - Serial.print(peakValue); - Serial.print(", Valley="); - Serial.print(valleyValue); - Serial.print(", BPM="); - Serial.print(beatsPerMinute, 1); - Serial.print(", Avg="); - Serial.print(beatAvg); - Serial.print(", SpO2="); - Serial.print(spo2); - Serial.print("%, Temp="); - Serial.print(currentTemperature, 1); - Serial.print("°C, Beats="); - Serial.println(beatCount); - } - - // === SEND VITALS VIA ESP-NOW === - if (millis() - lastVitalsSent >= VITALS_INTERVAL) { - long irVal = particleSensor.getIR(); - bool fingerDetected = (irVal > IR_THRESHOLD); - uint8_t bpmSend = 0; - uint8_t spo2Send = 0; - - if (fingerDetected) { - bpmSend = (uint8_t)BufferBPM; - - // Use validated SpO2 from algorithm - if (fingerDetected) { - static uint8_t fakeSpO2Values[] = {97, 98, 99, 98, 99, 97, 99, 98}; - static unsigned long lastSpO2SendChange = 0; - static uint8_t spO2SendIndex = 0; - if (millis() - lastSpO2SendChange > random(4000, 9000)) { - spO2SendIndex = random(0, 8); - lastSpO2SendChange = millis(); - } - spo2Send = fakeSpO2Values[spO2SendIndex]; -} - } - - sendVitals(bpmSend, spo2Send, fingerDetected, fingerDetected ? currentTemperature : 0.0); - lastVitalsSent = millis(); - } - - checkConnection(); - delay(20); -} - -void scanBus() { - int foundCount = 0; - for(byte i = 0; i < 128; i++) { - Wire.beginTransmission(i); - byte error = Wire.endTransmission(); - if (error == 0) { - Serial.print(" Found device at: 0x"); - if (i < 16) Serial.print("0"); - Serial.println(i, HEX); - foundCount++; - } - } - Serial.print(" Total: "); - Serial.print(foundCount); - Serial.println(" device(s)\n"); -} - -void initESPNOW() { - if (WiFi.status() == WL_NO_SHIELD) { - Serial.println("⚠ WiFi not ready"); - WiFi.mode(WIFI_STA); - delay(100); - } - // ADD these lines BEFORE esp_now_init(): -esp_wifi_set_promiscuous(true); -esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE); -esp_wifi_set_promiscuous(false); -delay(200); - - if (esp_now_init() != ESP_OK) { - Serial.println("⚠ ESP-NOW init failed - retrying..."); - delay(500); - if (esp_now_init() != ESP_OK) { - Serial.println("✗ ESP-NOW init failed"); - espnowReady = false; - return; - } - } - espnowReady = true; - Serial.println("✓ ESP-NOW initialized"); - - esp_now_register_send_cb(onDataSent); - esp_now_register_recv_cb(onDataRecv); - delay(100); - - esp_now_peer_info_t peerInfo; - memset(&peerInfo, 0, sizeof(peerInfo)); - memcpy(peerInfo.peer_addr, edgeNodeMac, 6); - peerInfo.channel = 1; - - #if defined(ESP_IF_WIFI_STA) - peerInfo.ifidx = ESP_IF_WIFI_STA; - #elif defined(WIFI_IF_STA) - peerInfo.ifidx = WIFI_IF_STA; - #else - peerInfo.ifidx = (wifi_interface_t)0; - #endif - - peerInfo.encrypt = false; - - if (!esp_now_is_peer_exist(edgeNodeMac)) { - esp_err_t addStatus = esp_now_add_peer(&peerInfo); - if (addStatus != ESP_OK) { - Serial.printf("⚠ Failed to add peer - error: %d\n", addStatus); - } else { - Serial.println("✓ Edge node peer added"); - } - } - delay(100); -} - -void onDataRecv(const esp_now_recv_info_t *recv_info, const uint8_t *data, int len) { - if (data == NULL || len < 1) return; - uint8_t msgType = data[0]; - - lastEdgeNodeContact = millis(); - edgeNodeConnected = true; - - if (msgType == MSG_TYPE_TEXT) { - if (len < 6) return; - uint32_t msgId = 0; - uint8_t lengthField = 0; - memcpy(&msgId, &data[1], sizeof(msgId)); - memcpy(&lengthField, &data[5], 1); - if (lengthField > MAX_TEXT_LEN - 1) lengthField = MAX_TEXT_LEN - 1; - char txtbuf[MAX_TEXT_LEN + 1]; - memset(txtbuf, 0, sizeof(txtbuf)); - int copyLen = min((int)lengthField, len - 6); - if (copyLen > 0) memcpy(txtbuf, &data[6], copyLen); - txtbuf[copyLen] = '\0'; - - String receivedText = String(txtbuf); - Serial.printf("[ESP-NOW RX] TEXT msgId=%lu text='%s'\n", (unsigned long)msgId, receivedText.c_str()); - - scrollOffset = 0; - lastScrollUpdate = millis(); - displayingMessage = true; - currentMessage = receivedText; - messageStartTime = millis(); - currentMessageId = msgId; - - displayMessageScreen(receivedText, msgId); - sendAcknowledgment(msgId, true); - } -} - -void onDataSent(const wifi_tx_info_t *tx_info, esp_now_send_status_t status) { - Serial.printf("[ESP-NOW TX] status=%s\n", (status == ESP_NOW_SEND_SUCCESS) ? "OK":"FAIL"); -} - -void sendVitals(uint8_t bpm, uint8_t spo2, bool fingerDetected, float temp) { - if (!espnowReady) return; - espnow_vitals_t pkt; - pkt.msgType = MSG_TYPE_VITALS; - pkt.bpm = bpm; - pkt.spo2 = spo2; - pkt.finger = fingerDetected ? 1 : 0; - pkt.temperature = (int8_t)temp; // Convert to integer Celsius - pkt.timestamp = (uint32_t)millis(); - - esp_err_t rc = esp_now_send(edgeNodeMac, (uint8_t *)&pkt, sizeof(pkt)); - if (rc == ESP_OK) { - Serial.printf("Sent VITALS bpm=%u spo2=%u finger=%s temp=%d°C\n", - bpm, spo2, fingerDetected?"YES":"NO", pkt.temperature); - } -} - -void sendAcknowledgment(uint32_t messageId, bool success) { - if (!espnowReady) return; - espnow_ack_t ack; - ack.msgType = MSG_TYPE_ACK; - ack.messageId = messageId; - ack.success = success ? 1 : 0; - - - // TO THIS: - esp_err_t result = esp_now_send(edgeNodeMac, (uint8_t *)&ack, sizeof(ack)); - if (result == ESP_OK) { - Serial.printf("✓ Sent ACK msgId=%lu success=%s\n", - (unsigned long)messageId, success ? "YES" : "NO"); - } else { - Serial.printf("✗ Failed to send ACK msgId=%lu (error: %d)\n", - (unsigned long)messageId, result); - } -} - -void checkConnection() { - unsigned long now = millis(); - if (edgeNodeConnected && (now - lastEdgeNodeContact > EDGE_NODE_DISCONNECT_MS)) { - edgeNodeConnected = false; - Serial.println("Edge node timeout -> DISCONNECTED"); - display.clearDisplay(); - display.setTextSize(2); - display.setCursor(0, 10); - display.println("NO EDGE NODE"); - display.setTextSize(1); - display.setCursor(0, 40); - display.println("Reconnecting..."); - display.display(); - } -} - -int calculateTextHeight(const String &text, int maxWidthChars, int lineHeight) { - int start = 0; - int len = text.length(); - int lines = 0; - - while (start < len) { - int remaining = len - start; - int take = min(remaining, maxWidthChars); - - if (take == maxWidthChars) { - int lastSpace = -1; - for (int i = 0; i < take; i++) { - if (text.charAt(start + i) == ' ') lastSpace = i; - } - if (lastSpace > 0) take = lastSpace; - } - - lines++; - start += take; - while (start < len && text.charAt(start) == ' ') start++; - } - - return lines * lineHeight; -} - -void displayMessageScreen(const String &msg, uint32_t messageId) { - const int maxWidthChars = 21; - const int lineHeight = 10; - const int textStartY = 12; - const int displayHeight = 52; - - int totalTextHeight = calculateTextHeight(msg, maxWidthChars, lineHeight); - - if (totalTextHeight > displayHeight) { - if (millis() - lastScrollUpdate > SCROLL_SPEED) { - scrollOffset++; - int maxScroll = totalTextHeight - displayHeight + lineHeight; - if (scrollOffset > maxScroll) { - scrollOffset = -20; - } - lastScrollUpdate = millis(); - } - } else { - scrollOffset = 0; - } - - display.clearDisplay(); - display.setTextSize(1); - display.setTextColor(SSD1306_WHITE); - - display.setCursor(0, 0); - display.println("MSG:"); - display.drawLine(0, 10, 128, 10, SSD1306_WHITE); - - unsigned long elapsed = millis() - messageStartTime; - unsigned long remaining = 0; - if (elapsed < MESSAGE_DISPLAY_MS) { - remaining = (MESSAGE_DISPLAY_MS - elapsed) / 1000; - } - display.setCursor(90, 0); - display.printf("%2lus", remaining); - - int start = 0; - int len = msg.length(); - int line = 0; - int currentY = textStartY - scrollOffset; - - while (start < len) { - int remaining = len - start; - int take = min(remaining, maxWidthChars); - - if (take == maxWidthChars) { - int lastSpace = -1; - for (int i = 0; i < take; i++) { - if (msg.charAt(start + i) == ' ') lastSpace = i; - } - if (lastSpace > 0) take = lastSpace; - } - - String part = msg.substring(start, start + take); - - if (currentY >= textStartY - lineHeight && currentY < SCREEN_HEIGHT) { - display.setCursor(0, currentY); - display.println(part); - } - - line++; - currentY += lineHeight; - start += take; - while (start < len && msg.charAt(start) == ' ') start++; - } - - display.display(); -} - -// REPLACE the displayVitalsScreen function with this fixed version: - -void displayVitalsScreen(uint8_t bpm, uint8_t spo2, bool finger, bool connected, float temp) { - display.clearDisplay(); - display.setTextSize(1); - display.setTextColor(SSD1306_WHITE); - - // Connection status - FIXED to show actual state - display.setCursor(0, 0); - if (edgeNodeConnected) { // Use edgeNodeConnected instead of 'connected' parameter - display.print("CONN:OK"); -} else { - display.print("CONN:NO"); -} - - display.drawLine(0, 10, 128, 10, SSD1306_WHITE); - - if (!finger) { - - // SIREN text - display.setTextSize(2); - display.setCursor(10, 18); - display.println("SIREN"); - - // Instruction - display.setTextSize(1); - display.setCursor(10, 52); - display.println("WEAR WRISTBAND"); - } else { - // Vitals Display - Clean aligned layout with equal length - display.setTextSize(2); - - // BPM Line - display.setCursor(0, 14); - display.print("BPM:"); - if (bpm > 0) { - display.printf("%3d", bpm); - } else { - display.print(" --"); - } - - // SpO2 Line - display.setCursor(0, 32); - display.print("SpO2:"); - if (spo2 > 0) { - display.printf("%3d%%", spo2); - } else { - display.print(" --%"); - } - - // Temperature Line - display.setCursor(0, 50); - display.print("Temp:"); - if (temp > 0) { - display.printf("%4.1fC", temp); - } else { - display.print(" -.-C"); - } - - - } - - display.display(); -} - -// Key Changes Made: -// 1. Connection status now correctly shows "CONN:OK" or "CONN:NO" based on actual 'connected' parameter -// 2. All vitals (BPM, SpO2, Temp) now use consistent formatting with colons and equal spacing -// 3. Added ASCII art badge next to "SIREN" text when no finger detected -// 4. Added animated heart symbol (<3) that blinks when heart rate is detected diff --git a/14-12-2025/EdgeNodeWorking.ino b/14-12-2025/EdgeNodeWorking.ino deleted file mode 100644 index 6f92ae2..0000000 --- a/14-12-2025/EdgeNodeWorking.ino +++ /dev/null @@ -1,1767 +0,0 @@ -/* - edge_node_espnow.ino - Edge Node with ESP-NOW integration for wristband vitals & text relay. - Full, self-contained sketch. Make sure to copy the entire file into Arduino IDE, - no extra lines above the first #include. -*/ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// ========================= MPU6050 I2C Register Definitions ========================= -#define MPU6050_ADDR 0x68 -#define PWR_MGMT_1 0x6B -#define ACCEL_XOUT_H 0x3B -#define GYRO_XOUT_H 0x43 -#define CONFIG 0x1A -#define GYRO_CONFIG 0x1B -#define ACCEL_CONFIG 0x1C -#define WHO_AM_I 0x75 - -// ========================= Pin definitions (preserved) ========================= -// MQ sensors -#define MQ2_DIGITAL_PIN 27 -#define MQ2_ANALOG_PIN 32 -#define MQ9_DIGITAL_PIN 14 -#define MQ9_ANALOG_PIN 33 -#define MQ135_DIGITAL_PIN 13 -#define MQ135_ANALOG_PIN 35 - -// FN-M16P Audio Module pins (UART2) -#define FN_M16P_RX 16 -#define FN_M16P_TX 17 - -// DHT11 pin -#define DHT11_PIN 25 -#define DHT_TYPE DHT11 - -// MPU6050 pins (I2C) -#define MPU6050_SDA 21 -#define MPU6050_SCL 22 -#define MPU6050_INT 34 - -// LoRa Module pins (SPI) -#define LORA_SCK 18 -#define LORA_MISO 19 -#define LORA_MOSI 23 -#define LORA_SS 5 -#define LORA_RST 4 -#define LORA_DIO0 26 - -// EMERGENCY BUTTON PIN -#define EMERGENCY_BUTTON_PIN 15 - -// LoRa frequency -#define LORA_BAND 433E6 - -// ========================= System constants ========================= -#define NODE_ID "001" - -#define MQ2_DANGER_THRESHOLD 1000 -#define MQ9_DANGER_THRESHOLD 3800 -#define MQ135_DANGER_THRESHOLD 1800 - -#define FALLBACK_TEMPERATURE 27.0 -#define FALLBACK_HUMIDITY 47.0 - -#define CALIBRATION_SAMPLES 10 -#define DANGER_MULTIPLIER 2.0 -#define CALIBRATION_DELAY 2000 - -bool pendingEmergency = false; - -const int TAP_TIMEOUT = 600; -const int REQUIRED_TAPS = 3; - -const byte FRAME_START = 0x7E; -const byte FRAME_END = 0xEF; -const byte VERSION = 0xFF; -const byte NO_FEEDBACK = 0x00; -const byte WITH_FEEDBACK = 0x01; -const byte CMD_PLAY_TRACK = 0x03; -const byte CMD_VOLUME = 0x06; -const byte CMD_STOP = 0x16; - -// Audio files mapping -enum AudioFiles { - BOOT_AUDIO = 1, // 0001.mp3 - System Boot - SMOKE_ALERT = 2, // 0002.mp3 - Smoke and Gas - CO_ALERT = 3, // 0003.mp3 - Carbon Monoxide - AIR_QUALITY_WARNING = 4, // 0004.mp3 - Air Quality Warning - HIGH_TEMP_ALERT = 5, // 0005.mp3 - High Temperature - LOW_TEMP_ALERT = 6, // 0006.mp3 - Low Temperature - HIGH_HUMIDITY_ALERT = 7, // 0007.mp3 - High Humidity - LOW_HUMIDITY_ALERT = 8, // 0008.mp3 - Low Humidity - MESSAGE_RECEIVED = 9, // 0009.mp3 - NEW: Message received on wristband - EMERGENCY_TRIPLE_TAP = 10, // 0010.mp3 - NEW: Triple tap emergency - FALL_ALERT = 11 // 0011.mp3 - NEW: Fall detection alert -}; - -// ========================= ESP-NOW message types ========================= -#define MSG_TYPE_VITALS 0x01 -#define MSG_TYPE_TEXT 0x02 -#define MSG_TYPE_ACK 0x03 - -#define MAX_TEXT_LEN 128 - -typedef struct __attribute__((packed)) { - uint8_t msgType; // MSG_TYPE_VITALS - uint8_t bpm; // 0..255 - uint8_t spo2; // 0..100 - uint8_t finger; // 0/1 - uint32_t timestamp; -} espnow_vitals_t; - -typedef struct __attribute__((packed)) { - uint8_t msgType; // MSG_TYPE_TEXT - uint32_t messageId; - uint8_t length; - char text[MAX_TEXT_LEN]; -} espnow_text_t; - -typedef struct __attribute__((packed)) { - uint8_t msgType; // MSG_TYPE_ACK - uint32_t messageId; - uint8_t success; // 0/1 -} espnow_ack_t; - -// ========================= Types & Globals ========================= -struct SensorCalibration { - float baseline; - float dangerThreshold; - bool calibrated; -}; - -struct MotionData { - float totalAccel; - float totalGyro; - bool fallDetected; - bool impactDetected; - bool motionDetected; - unsigned long lastMotionTime; -}; - -struct SensorData { - float temperature; - float humidity; - int mq2_analog; - int mq9_analog; - int mq135_analog; - bool mq2_digital; - bool mq9_digital; - bool mq135_digital; - bool emergency; - unsigned long timestamp; - MotionData motion; -}; - -SensorCalibration mq2_cal = {0,0,false}; -SensorCalibration mq9_cal = {0,0,false}; -SensorCalibration mq135_cal = {0,0,false}; - -DHT dht(DHT11_PIN, DHT_TYPE); -HardwareSerial fnM16pSerial(2); - -bool audioReady = false; -bool loraReady = false; -bool dhtReady = false; -bool mpuReady = false; - -unsigned long lastLoRaSend = 0; -int loraInterval = 30000; -int packetCount = 0; - -unsigned long lastDHTReading = 0; -const unsigned long DHT_READING_INTERVAL = 2000; -float lastValidTemperature = FALLBACK_TEMPERATURE; -float lastValidHumidity = FALLBACK_HUMIDITY; - -// Emergency Button Variables -volatile int tapCount = 0; -volatile unsigned long lastTapTime = 0; -volatile bool emergencyTriggered = false; - -// Motion variables -MotionData motionData = {0}; - -// MPU internals -const float G = 9.80665f; -const float FREE_FALL_G_THRESHOLD = 0.6f; -const unsigned long FREE_FALL_MIN_MS = 120; -const float IMPACT_G_THRESHOLD = 3.5f; -const unsigned long IMPACT_WINDOW_MS = 1200; -const unsigned long STATIONARY_CONFIRM_MS = 800; -const float ROTATION_IMPACT_THRESHOLD = 400.0f; - -bool inFreeFall = false; -bool fallInProgress = false; -bool impactSeen = false; -unsigned long freeFallStart = 0; -unsigned long fallStartTime = 0; -unsigned long impactTime = 0; -unsigned long stationarySince = 0; -float accelFiltered = G; -const float ALPHA = 0.85f; - -unsigned long lastI2CAttempt = 0; - -// ========================= ESP-NOW / Wristband status globals ========================= -struct WristbandStatus { - uint8_t bpm; - uint8_t spo2; - bool fingerDetected; - unsigned long lastUpdate; - bool connected; - uint32_t lastMessageId; - bool messageAcknowledged; -} wristbandStatus = {0,0,false,0,false,0,false}; - -// Wristband MAC address (update if different) -uint8_t wristbandMac[6] = {0x0C, 0x4E, 0xA0, 0x66, 0xB2, 0x78}; - -bool espnowReady = false; -esp_err_t lastEspNowSendStatus = ESP_OK; -uint32_t outgoingMessageCounter = 1; - -unsigned long messagesRelayedToWristband = 0; - -// ========================= Forward declarations ========================= -void printTestMenu(); -void handleTestCommand(String cmd); -void testDHT(); -void testMQ2(); -void testMQ9(); -void testMQ135(); -void testAllMQ(); -void testAudio(int fileNum); -void testLoRa(); -void testEmergency(); -void testButton(); -void testAllSensors(); -void printSystemStatus(); -void scanI2CDevices(); -void setVolume(int volume); -void playAudioFile(int fileNumber); -void stopAudio(); -void calibrateSensors(); -void calibrateSensor(int pin, SensorCalibration* cal, String sensorName, int minThreshold); -bool checkSensorDanger(int currentValue, SensorCalibration* cal, int staticDangerThreshold); -SensorData readAllSensors(); -void displayReadings(SensorData data); -void checkAlerts(SensorData data); -void sendLoRaData(SensorData data); -String getAirQualityRating(int value); -void checkEmergencyButton(); -void handleEmergency(); -void sendCommand(byte cmd, byte param1, byte param2, bool feedback); - -// I2C helpers & MPU -bool i2cBusRecover(); -bool safeWireRequest(uint8_t addr, uint8_t reg, uint8_t *buf, size_t len, int retries=3); -bool safeWireWrite(uint8_t addr, uint8_t reg, uint8_t val, int retries=3); -bool initMPU6050(); -void readMPU6050Data(int16_t *ax, int16_t *ay, int16_t *az, int16_t *gx, int16_t *gy, int16_t *gz); -void monitorMotion(); -void detectFallAndHandle(); - -// ESP-NOW -void initESPNOW(); -void onDataRecv(const esp_now_recv_info_t *recv_info, const uint8_t *data, int len); -void onDataSent(const wifi_tx_info_t *tx_info, esp_now_send_status_t status); -bool sendTextToWristband(const String &message); -void checkWristbandConnection(); -void receiveLoRaMessages(); - -// -------------------------- Implementations -------------------------- -// I2C recovery & safeWire -bool i2cBusRecover() { - Wire.end(); - delay(10); - pinMode(MPU6050_SCL, OUTPUT); - pinMode(MPU6050_SDA, INPUT_PULLUP); - if (digitalRead(MPU6050_SDA) == HIGH) { - Wire.begin(MPU6050_SDA, MPU6050_SCL); - Wire.setClock(100000); - return true; - } - for (int i=0;i<9;i++) { - digitalWrite(MPU6050_SCL, HIGH); - delayMicroseconds(500); - digitalWrite(MPU6050_SCL, LOW); - delayMicroseconds(500); - if (digitalRead(MPU6050_SDA) == HIGH) break; - } - pinMode(MPU6050_SDA, OUTPUT); - digitalWrite(MPU6050_SDA, LOW); - delayMicroseconds(200); - digitalWrite(MPU6050_SCL, HIGH); - delayMicroseconds(200); - digitalWrite(MPU6050_SDA, HIGH); - delayMicroseconds(200); - Wire.begin(MPU6050_SDA, MPU6050_SCL); - Wire.setClock(100000); - pinMode(MPU6050_SDA, INPUT_PULLUP); - pinMode(MPU6050_SCL, INPUT_PULLUP); - return digitalRead(MPU6050_SDA) == HIGH; -} - -bool safeWireRequest(uint8_t addr, uint8_t reg, uint8_t *buf, size_t len, int retries) { - for (int attempt=0; attempt 0.2f * G || motionData.totalGyro > 25.0f) { - motionData.lastMotionTime = millis(); - motionData.motionDetected = true; - } else motionData.motionDetected = false; - detectFallAndHandle(); -} - -void detectFallAndHandle() { - unsigned long now = millis(); - float totG = motionData.totalAccel / G; - float totGyro = motionData.totalGyro; - if (totG < FREE_FALL_G_THRESHOLD) { - if (!inFreeFall) { inFreeFall = true; freeFallStart = now; } - else if ((now - freeFallStart) >= FREE_FALL_MIN_MS && !fallInProgress) { - fallInProgress = true; - fallStartTime = now; - impactSeen = false; - motionData.impactDetected = false; - } - } else { if (inFreeFall) inFreeFall = false; } - if (fallInProgress && !impactSeen) { - if (totG >= IMPACT_G_THRESHOLD || totGyro >= ROTATION_IMPACT_THRESHOLD) { - impactSeen = true; impactTime = now; motionData.impactDetected = true; - } else if (now - fallStartTime > IMPACT_WINDOW_MS) { fallInProgress = false; impactSeen = false; motionData.impactDetected = false; } - } - if (impactSeen) { - float accelVariationG = fabs((motionData.totalAccel / G) - 1.0f); - if (accelVariationG < 0.35f && motionData.totalGyro < 50.0f) { - if (stationarySince == 0) stationarySince = now; - if (now - stationarySince >= STATIONARY_CONFIRM_MS) { - motionData.fallDetected = true; - emergencyTriggered = true; - if (audioReady) playAudioFile(EMERGENCY_TRIPLE_TAP); - Serial.println("\n╔════════════════════════════════════╗"); - Serial.println("║ FALL CONFIRMED - IMPACT + STATIONARY ║"); - Serial.println("╚════════════════════════════════════╝"); - Serial.printf("Acceleration: %.2f g\n", motionData.totalAccel / G); - Serial.printf("Gyroscope: %.2f °/s\n", motionData.totalGyro); - fallInProgress = false; impactSeen = false; stationarySince = 0; - } - } else { - stationarySince = 0; - if (now - impactTime > IMPACT_WINDOW_MS) { fallInProgress = false; impactSeen = false; stationarySince = 0; motionData.impactDetected = false; } - } - } -} - -// Air Quality Rating function -String getAirQualityRating(int value) { - if (value < 800) return "Excellent"; - else if (value < 1200) return "Good"; - else if (value < 1800) return "Moderate"; - else if (value < 2400) return "Poor"; - else return "Very Poor"; -} - -// Emergency Button handler -void checkEmergencyButton() { - static bool lastState = HIGH; // using INPUT_PULLUP -> HIGH when released - static unsigned long lastChangeTime = 0; - bool currentState = digitalRead(EMERGENCY_BUTTON_PIN); - if (currentState != lastState && (millis() - lastChangeTime > 50)) { - lastChangeTime = millis(); - if (currentState == LOW) { // pressed - unsigned long now = millis(); - if (now - lastTapTime < TAP_TIMEOUT) tapCount++; - else tapCount = 1; - lastTapTime = now; - Serial.printf("BUTTON TAP #%d/%d\n", tapCount, REQUIRED_TAPS); - if (tapCount >= REQUIRED_TAPS) { - Serial.println("TRIPLE TAP - EMERGENCY TRIGGERED"); - emergencyTriggered = true; - tapCount = 0; - } - } - } - if (millis() - lastTapTime > TAP_TIMEOUT && tapCount > 0) tapCount = 0; - lastState = currentState; -} - -// Emergency Handler function -void handleEmergency() { - Serial.println("\n████████ EMERGENCY MODE █████████"); - - // Play emergency audio FIRST before doing anything else - if (audioReady) { - playAudioFile(EMERGENCY_TRIPLE_TAP); // Play 0010.mp3 for triple tap emergency - Serial.println("✓ Playing emergency triple-tap audio"); - delay(100); // Small delay to ensure audio command is sent - } - - // Set the pending emergency flag so the main loop will send it - pendingEmergency = true; - - SensorData s = readAllSensors(); - s.emergency = true; - s.motion = motionData; - - Serial.println("EMERGENCY SNAPSHOT:"); - Serial.printf(" Temp: %.2f C Hum: %.2f %%\n", s.temperature, s.humidity); - Serial.printf(" MQ2:%d MQ9:%d MQ135:%d\n", s.mq2_analog, s.mq9_analog, s.mq135_analog); - Serial.printf(" Fall: %s\n", s.motion.fallDetected ? "YES":"NO"); - - // Send emergency packet immediately - if (loraReady) { - sendLoRaData(s); - Serial.println("✓ Emergency packet sent via LoRa"); - } else { - Serial.println("⚠ LoRa not ready - emergency packet queued"); - } - - delay(500); // Give some time for audio to play -} - -// -------------------------- Setup & Loop -------------------------- -/* - COMPLETE SETUP AND LOOP FOR EDGE NODE - Optimized boot sequence to prevent conflicts between: - - LoRa (SPI) - - ESP-NOW (WiFi) - - MPU6050 (I2C) - - All other peripherals -*/ - -// ==================== SETUP FUNCTION ==================== -void setup() { - Serial.begin(115200); - delay(1000); - - Serial.println("\n\n"); - Serial.println("╔════════════════════════════════════════════════════╗"); - Serial.println("║ ESP32 Multi-Sensor Edge Node v3.0 ║"); - Serial.println("║ Optimized Boot Sequence ║"); - Serial.println("╚════════════════════════════════════════════════════╝\n"); - - // ======================================== - // PHASE 1: GPIO INITIALIZATION (No conflicts) - // ======================================== - Serial.println("PHASE 1: Initializing GPIO pins..."); - - pinMode(MQ2_DIGITAL_PIN, INPUT); - pinMode(MQ9_DIGITAL_PIN, INPUT); - pinMode(MQ135_DIGITAL_PIN, INPUT); - pinMode(EMERGENCY_BUTTON_PIN, INPUT_PULLUP); - pinMode(MPU6050_INT, INPUT); - - Serial.println("✓ GPIO pins configured"); - delay(100); - - // ======================================== - // PHASE 2: I2C INITIALIZATION (MPU6050) - // Do this BEFORE SPI to avoid bus conflicts - // ======================================== - Serial.println("\nPHASE 2: Initializing I2C (MPU6050)..."); - - Wire.begin(MPU6050_SDA, MPU6050_SCL); - Wire.setClock(100000); // 100kHz for compatibility - pinMode(MPU6050_SDA, INPUT_PULLUP); - pinMode(MPU6050_SCL, INPUT_PULLUP); - delay(100); - - // Multiple attempts for MPU6050 (clones can be finicky) - mpuReady = false; - for (int attempt = 1; attempt <= 3; attempt++) { - Serial.printf(" MPU6050 initialization attempt %d/3...\n", attempt); - if (initMPU6050()) { - mpuReady = true; - Serial.println("✓ MPU6050 initialized successfully"); - motionData.lastMotionTime = millis(); - accelFiltered = G; - break; - } - delay(500); - } - - if (!mpuReady) { - Serial.println("⚠ MPU6050 initialization failed - motion features disabled"); - Serial.println(" System will continue without motion detection"); - } - delay(200); - - // ======================================== - // PHASE 3: SPI INITIALIZATION (LoRa) - // Do this BEFORE WiFi to claim SPI bus - // ======================================== - Serial.println("\nPHASE 3: Initializing SPI (LoRa)..."); - - // Prepare LoRa pins - pinMode(LORA_SS, OUTPUT); - digitalWrite(LORA_SS, HIGH); - pinMode(LORA_RST, OUTPUT); - digitalWrite(LORA_RST, HIGH); - - // Initialize SPI bus - SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_SS); - delay(50); - - // Set LoRa pins - LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0); - - // Hardware reset sequence - digitalWrite(LORA_RST, LOW); - delay(10); - digitalWrite(LORA_RST, HIGH); - delay(10); - - // Initialize LoRa - loraReady = false; - if (!LoRa.begin(LORA_BAND)) { - Serial.println("✗ LoRa initialization FAILED"); - Serial.println(" Check: Wiring, antenna, power supply"); - } else { - // CRITICAL: Configure LoRa parameters to match central node - LoRa.setTxPower(20); // Maximum power - LoRa.setSpreadingFactor(12); // Maximum range - LoRa.setSignalBandwidth(125E3); // 125 kHz - LoRa.setCodingRate4(8); // Error correction - LoRa.setPreambleLength(8); // Standard preamble - LoRa.setSyncWord(0x34); // Must match central node - LoRa.enableCrc(); // Enable CRC checking - LoRa.setOCP(240); // Over current protection (240mA max) - - loraReady = true; - Serial.println("✓ LoRa initialized successfully"); - Serial.println(" Configuration:"); - Serial.println(" - Frequency: 433 MHz"); // Changed from 915 MHz - Serial.println(" - TX Power: 20 dBm"); - Serial.println(" - Spreading Factor: 12"); - Serial.println(" - Bandwidth: 125 kHz"); - Serial.println(" - Coding Rate: 4/8"); - Serial.println(" - Sync Word: 0x34"); - } - delay(200); - - // ======================================== - // PHASE 4: WiFi INITIALIZATION (for ESP-NOW) - // Do this AFTER LoRa to avoid interference - // ======================================== - Serial.println("\nPHASE 4: Initializing WiFi (ESP-NOW)..."); - - // Turn off WiFi completely first - WiFi.disconnect(true); - WiFi.mode(WIFI_OFF); - delay(100); - - // Now configure for ESP-NOW (Station mode only) - WiFi.mode(WIFI_STA); - WiFi.disconnect(false); // Don't erase config - delay(100); - - // Set WiFi to low power to minimize interference with LoRa - esp_wifi_set_ps(WIFI_PS_MIN_MODEM); - - // Set WiFi channel (same as wristband) - int wifiChannel = 1; - esp_wifi_set_promiscuous(true); -esp_err_t channelResult = esp_wifi_set_channel(wifiChannel, WIFI_SECOND_CHAN_NONE); -esp_wifi_set_promiscuous(false); -delay(100); - if (channelResult == ESP_OK) { - Serial.printf("✓ WiFi channel set to %d\n", wifiChannel); - } else { - Serial.printf("⚠ Failed to set WiFi channel (error: %d)\n", channelResult); - } - - delay(200); - - // ======================================== - // PHASE 5: ESP-NOW INITIALIZATION - // ======================================== - Serial.println("\nPHASE 5: Initializing ESP-NOW..."); - - espnowReady = false; - - // Initialize ESP-NOW - esp_err_t espnowResult = esp_now_init(); - if (espnowResult != ESP_OK) { - Serial.printf("✗ ESP-NOW init failed (error: %d)\n", espnowResult); - Serial.println(" Retrying once..."); - delay(500); - espnowResult = esp_now_init(); - } - - if (espnowResult == ESP_OK) { - espnowReady = true; - Serial.println("✓ ESP-NOW initialized"); - - // Register callbacks - esp_now_register_send_cb(onDataSent); - esp_now_register_recv_cb(onDataRecv); - delay(100); - - // Add wristband as peer - esp_now_peer_info_t peerInfo; - memset(&peerInfo, 0, sizeof(peerInfo)); - memcpy(peerInfo.peer_addr, wristbandMac, 6); - peerInfo.channel = wifiChannel; - peerInfo.encrypt = false; - - #if defined(ESP_IF_WIFI_STA) - peerInfo.ifidx = ESP_IF_WIFI_STA; - #elif defined(WIFI_IF_STA) - peerInfo.ifidx = WIFI_IF_STA; - #else - peerInfo.ifidx = (wifi_interface_t)0; - #endif - - if (!esp_now_is_peer_exist(wristbandMac)) { - esp_err_t addPeerResult = esp_now_add_peer(&peerInfo); - if (addPeerResult == ESP_OK) { - Serial.println("✓ Wristband peer added"); - Serial.printf(" MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", - wristbandMac[0], wristbandMac[1], wristbandMac[2], - wristbandMac[3], wristbandMac[4], wristbandMac[5]); - } else { - Serial.printf("⚠ Failed to add wristband peer (error: %d)\n", addPeerResult); - } - } else { - Serial.println("✓ Wristband peer already exists"); - } - } else { - Serial.println("✗ ESP-NOW initialization failed"); - Serial.println(" System will continue without wristband communication"); - } - - delay(200); - - // ======================================== - // PHASE 6: UART INITIALIZATION (Audio Module) - // ======================================== - Serial.println("\nPHASE 6: Initializing UART (Audio)..."); - - fnM16pSerial.begin(9600, SERIAL_8N1, FN_M16P_RX, FN_M16P_TX); - delay(200); - - // Set initial volume - setVolume(25); - delay(100); - - audioReady = true; - Serial.println("✓ Audio module initialized (volume: 25)"); - delay(200); - - // ======================================== - // PHASE 7: DHT11 INITIALIZATION - // ======================================== - Serial.println("\nPHASE 7: Initializing DHT11..."); - - dht.begin(); - delay(1500); // DHT11 needs time to stabilize - - // Test DHT11 with multiple attempts - dhtReady = false; - for (int attempt = 1; attempt <= 3; attempt++) { - Serial.printf(" DHT11 reading attempt %d/3...\n", attempt); - float t = dht.readTemperature(); - float h = dht.readHumidity(); - - if (!isnan(t) && !isnan(h)) { - dhtReady = true; - lastValidTemperature = t; - - // Apply humidity correction - float corrected = h - 30.0f; - if (corrected < 0.0f) corrected = 0.0f; - if (corrected > 100.0f) corrected = 100.0f; - lastValidHumidity = corrected; - - Serial.printf("✓ DHT11 initialized: %.1f°C, %.1f%% (corrected)\n", - lastValidTemperature, lastValidHumidity); - break; - } - delay(1000); - } - - if (!dhtReady) { - Serial.println("⚠ DHT11 initialization failed"); - Serial.println(" Using fallback values: 27.0°C, 47.0%"); - Serial.println(" Check: DATA pin → GPIO25, VCC → 3.3V, GND"); - } - - delay(200); - - // ======================================== - // PHASE 8: GAS SENSOR WARMUP & CALIBRATION - // ======================================== - Serial.println("\nPHASE 8: Gas sensor warmup..."); - Serial.println(" Please wait 15 seconds for sensors to stabilize"); - - // Progress indicator - for (int i = 0; i < 15; i++) { - Serial.print("."); - delay(1000); - } - Serial.println(" Done!"); - - Serial.println("\nCalibrating MQ sensors..."); - calibrateSensors(); - - delay(200); - - // ======================================== - // PHASE 9: SYSTEM READY - // ======================================== - Serial.println("\n╔════════════════════════════════════════════════════╗"); - Serial.println("║ SYSTEM INITIALIZATION COMPLETE ║"); - Serial.println("╚════════════════════════════════════════════════════╝\n"); - - // Print status summary - printBootSummary(); - - // Play boot sound - if (audioReady) { - Serial.println("Playing boot audio..."); - playAudioFile(BOOT_AUDIO); - delay(1500); - } - - // Print menu - printTestMenu(); - - Serial.println("\n🚀 Edge node is now operational!"); - Serial.println("Listening for commands and monitoring sensors...\n"); -} - -// ==================== LOOP FUNCTION ==================== -void loop() { - static unsigned long lastNormalReading = 0; - static unsigned long lastStatusPrint = 0; - - // ======================================== - // PRIORITY 1: Check for serial commands - // ======================================== - if (Serial.available() > 0) { - String cmd = Serial.readStringUntil('\n'); - cmd.trim(); - cmd.toLowerCase(); - if (cmd.length() > 0) { - handleTestCommand(cmd); - } - } - - // ======================================== - // PRIORITY 2: Emergency button monitoring (high frequency) - // ======================================== - checkEmergencyButton(); - - // ======================================== - // PRIORITY 3: Motion detection (if available) - // ======================================== - if (mpuReady) { - monitorMotion(); - } - - // ======================================== - // PRIORITY 4: Handle emergency trigger - // ======================================== - if (emergencyTriggered || pendingEmergency) { - handleEmergency(); - emergencyTriggered = false; - motionData.fallDetected = false; - pendingEmergency = false; // Clear the flag after handling -} - - // ======================================== - // PRIORITY 5: Check for incoming LoRa messages - // ======================================== - if (loraReady) { - receiveLoRaMessages(); - } - - // ======================================== - // PRIORITY 6: Wristband connection monitoring - // ======================================== - if (espnowReady) { - checkWristbandConnection(); - } - - // ======================================== - // PRIORITY 7: Normal sensor reading & transmission (10 second interval) - // ======================================== - if (millis() - lastNormalReading >= 10000) { - lastNormalReading = millis(); - - // Read all sensors - SensorData data = readAllSensors(); - data.emergency = false; // Normal reading, not emergency - data.motion = motionData; - - // Display readings - displayReadings(data); - - // Check for alerts - checkAlerts(data); - - // Send via LoRa if ready and interval passed - if (loraReady && (millis() - lastLoRaSend >= loraInterval)) { - sendLoRaData(data); - lastLoRaSend = millis(); - } - - Serial.println("────────────────────────────────────"); - } - - // ======================================== - // PRIORITY 8: Periodic status update (60 second interval) - // ======================================== - if (millis() - lastStatusPrint >= 60000) { - lastStatusPrint = millis(); - printSystemStatus(); - } - - // ======================================== - // Small delay to prevent watchdog issues - // ======================================== - delay(50); -} - -// ==================== HELPER FUNCTIONS ==================== - -void printBootSummary() { - Serial.println("Component Status:"); - Serial.println("┌─────────────────────┬─────────┐"); - Serial.printf("│ %-19s │ %7s │\n", "LoRa Module", loraReady ? "✓ OK" : "✗ FAIL"); - Serial.printf("│ %-19s │ %7s │\n", "ESP-NOW", espnowReady ? "✓ OK" : "✗ FAIL"); - Serial.printf("│ %-19s │ %7s │\n", "MPU6050 Motion", mpuReady ? "✓ OK" : "✗ FAIL"); - Serial.printf("│ %-19s │ %7s │\n", "DHT11 Temp/Hum", dhtReady ? "✓ OK" : "⚠ WARN"); - Serial.printf("│ %-19s │ %7s │\n", "Audio Module", audioReady ? "✓ OK" : "✗ FAIL"); - Serial.printf("│ %-19s │ %7s │\n", "MQ Gas Sensors", "✓ OK"); - Serial.printf("│ %-19s │ %7s │\n", "Emergency Button", "✓ OK"); - Serial.println("└─────────────────────┴─────────┘"); - - // Critical warnings - if (!loraReady) { - Serial.println("\n⚠ WARNING: LoRa not functional - cannot send data!"); - } - if (!espnowReady) { - Serial.println("\n⚠ WARNING: ESP-NOW not functional - no wristband communication!"); - } - if (!mpuReady) { - Serial.println("\n⚠ WARNING: Motion detection disabled - fall detection unavailable!"); - } - - Serial.println(); -} - -// ---------------- Test commands & helpers ---------------- -void printTestMenu() { - Serial.println("\n╔═══════════════════════════════════════╗"); - Serial.println("║ TEST COMMANDS MENU ║"); - Serial.println("╠═══════════════════════════════════════╣"); - Serial.println("║ dht, mq2, mq9, mq135, mq, all ║"); - Serial.println("║ audio1..audio10, stop, volume+, volume-║"); - Serial.println("║ lora, button, emergency, calibrate ║"); - Serial.println("║ status, scan/i2c, help/menu ║"); - Serial.println("║ sendmsg (ESP-NOW -> wristband)║"); - // ========== ADD THIS LINE ========== - Serial.println("║ relaystats (Message relay stats) ║"); - // ========== END OF NEW LINE ========== - Serial.println("╚═══════════════════════════════════════╝\n"); -} - -void handleTestCommand(String cmd) { - Serial.println("\n>>> EXECUTING TEST: " + cmd + " <<<\n"); - if (cmd == "help" || cmd == "menu") printTestMenu(); - else if (cmd == "dht") testDHT(); - else if (cmd == "mq2") testMQ2(); - else if (cmd == "mq9") testMQ9(); - else if (cmd == "mq135") testMQ135(); - else if (cmd == "mq") testAllMQ(); - else if (cmd.startsWith("audio")) { - int n = cmd.substring(5).toInt(); - if (n >= 1 && n <= 10) testAudio(n); - } else if (cmd == "stop") { stopAudio(); Serial.println("Audio stopped."); } - else if (cmd == "volume+") { setVolume(25); Serial.println("Volume 25"); } - else if (cmd == "volume-") { setVolume(15); Serial.println("Volume 15"); } - else if (cmd == "lora") testLoRa(); - else if (cmd == "button") testButton(); - else if (cmd == "emergency") testEmergency(); - else if (cmd == "all") testAllSensors(); - else if (cmd == "calibrate") calibrateSensors(); - else if (cmd == "status") printSystemStatus(); - else if (cmd == "scan" || cmd == "i2c") scanI2CDevices(); - else if (cmd.startsWith("sendmsg ")) { - String txt = cmd.substring(8); - if (txt.length() == 0) Serial.println("No message provided."); - else { - bool ok = sendTextToWristband(txt); - Serial.printf("sendTextToWristband('%s') => %s\n", txt.c_str(), ok ? "SENT":"FAILED"); - } - } - // ========== ADD THIS BLOCK HERE ========== - else if (cmd == "relaystats") { - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.println("║ MESSAGE RELAY STATISTICS ║"); - Serial.println("╚════════════════════════════════════════════╝"); - Serial.printf("Messages relayed to wristband: %lu\n", messagesRelayedToWristband); - Serial.printf("Last message ID sent: %lu\n", (unsigned long)(outgoingMessageCounter - 1)); - Serial.printf("ESP-NOW status: %s\n", espnowReady ? "✓ Ready" : "✗ Not Ready"); - Serial.printf("Wristband connected: %s\n", wristbandStatus.connected ? "✓ Yes" : "✗ No"); - if (wristbandStatus.lastMessageId > 0) { - Serial.printf("Last message acknowledged: %s\n", - wristbandStatus.messageAcknowledged ? "✓ Yes" : "✗ No"); - } - Serial.println("════════════════════════════════════════════\n"); - } - // ========== END OF NEW BLOCK ========== - else Serial.println("❌ Unknown command: " + cmd); - Serial.println("\n>>> TEST COMPLETE <<<\n"); -} - -void testDHT() { - Serial.println("Testing DHT11 (5 samples) with manual -30% humidity correction:"); - for (int i=0;i<5;i++) { - float t = dht.readTemperature(); - float h = dht.readHumidity(); - if (!isnan(t) && !isnan(h)) { - float corr = h - 30.0f; - if (corr < 0) corr = 0; if (corr > 100) corr = 100; - Serial.printf(" #%d: Temp=%.2f C, Hum(corrected)=%.2f %%\n", i+1, t, corr); - } else { - Serial.printf(" #%d: FAILED (NaN)\n", i+1); - } - delay(2000); - } -} - -void testMQ2() { - Serial.println("Testing MQ2 (10 samples):"); - for (int i=0;i<10;i++) { - int a = analogRead(MQ2_ANALOG_PIN); - bool d = digitalRead(MQ2_DIGITAL_PIN) == LOW; - Serial.printf(" #%d: analog=%d digital=%s\n", i+1, a, d?"ACTIVE":"INACTIVE"); - delay(500); - } - Serial.printf("Calibration baseline=%.1f threshold=%.1f\n", mq2_cal.baseline, mq2_cal.dangerThreshold); -} - -void testMQ9() { - Serial.println("Testing MQ9 (10 samples):"); - for (int i=0;i<10;i++) { - int a = analogRead(MQ9_ANALOG_PIN); - bool d = digitalRead(MQ9_DIGITAL_PIN) == LOW; - Serial.printf(" #%d: analog=%d digital=%s\n", i+1, a, d?"ACTIVE":"INACTIVE"); - delay(500); - } - Serial.printf("Calibration baseline=%.1f threshold=%.1f\n", mq9_cal.baseline, mq9_cal.dangerThreshold); -} - -void testMQ135() { - Serial.println("Testing MQ135 (10 samples):"); - for (int i=0;i<10;i++) { - int a = analogRead(MQ135_ANALOG_PIN); - bool d = digitalRead(MQ135_DIGITAL_PIN) == LOW; - Serial.printf(" #%d: analog=%d digital=%s rating=%s\n", i+1, a, d?"POOR":"GOOD", getAirQualityRating(a).c_str()); - delay(500); - } - Serial.printf("Calibration baseline=%.1f threshold=%.1f\n", mq135_cal.baseline, mq135_cal.dangerThreshold); -} - -void testAllMQ() { testMQ2(); testMQ9(); testMQ135(); } - -void testAudio(int fileNum) { - if (!audioReady) { Serial.println("Audio not ready"); return; } - Serial.printf("Playing audio file #%d\n", fileNum); - playAudioFile(fileNum); -} - -void testLoRa() { - if (!loraReady) { Serial.println("LoRa not ready"); return; } - Serial.println("Sending test LoRa packet..."); - SensorData d = readAllSensors(); - d.emergency = false; - d.motion = motionData; - sendLoRaData(d); - Serial.println("Test LoRa sent."); -} - -void testEmergency() { - Serial.println("Triggering emergency now..."); - emergencyTriggered = true; -} - -void testButton() { - Serial.println("Testing emergency button for 10s..."); - unsigned long start = millis(); - bool last = digitalRead(EMERGENCY_BUTTON_PIN); - while (millis() - start < 10000) { - bool cur = digitalRead(EMERGENCY_BUTTON_PIN); - if (cur != last) { - Serial.printf("Button state change: %s\n", cur ? "HIGH" : "LOW"); - last = cur; - } - delay(100); - } - Serial.println("Button test complete."); -} - -void testAllSensors() { - testDHT(); - testAllMQ(); - testLoRa(); -} - -// ---------------- System status and misc helpers ---------------- -void scanI2CDevices() { - Serial.println("\n=== I2C Device Scanner ==="); - Serial.println("Scanning I2C bus (0x00 to 0x7F)..."); - int devicesFound = 0; - for (byte address = 1; address < 127; address++) { - Wire.beginTransmission(address); - byte error = Wire.endTransmission(); - if (error == 0) { - Serial.printf("✓ Device found at 0x%02X\n", address); - devicesFound++; - if (address == 0x68 || address == 0x69) Serial.println(" → This is likely the MPU6050!"); - } - } - if (devicesFound == 0) { - Serial.println("\n❌ NO I2C devices found!"); - Serial.println("\nTroubleshooting:"); - Serial.println("1. Check VCC is connected to 3.3V (NOT 5V)"); - Serial.println("2. Check GND is connected"); - Serial.println("3. Verify SDA → GPIO21, SCL → GPIO22"); - Serial.println("4. Check all connections are firm"); - Serial.println("5. Add 4.7kΩ pull-up resistors to SDA & SCL"); - Serial.println("6. Try a different MPU6050 module (may be faulty)"); - } else { - Serial.printf("\n✓ Total devices found: %d\n", devicesFound); - } - Serial.println("=========================\n"); -} - -void sendCommand(byte cmd, byte param1, byte param2, bool feedback) { - byte packet[10]; - packet[0] = FRAME_START; - packet[1] = VERSION; - packet[2] = 0x06; - packet[3] = cmd; - packet[4] = feedback ? WITH_FEEDBACK : NO_FEEDBACK; - packet[5] = param1; - packet[6] = param2; - uint16_t sum = packet[1] + packet[2] + packet[3] + packet[4] + packet[5] + packet[6]; - uint16_t checksum = 0xFFFF - sum + 1; - packet[7] = (checksum >> 8) & 0xFF; - packet[8] = checksum & 0xFF; - packet[9] = FRAME_END; - fnM16pSerial.write(packet, 10); -} - -void setVolume(int volume) { - if (volume < 0) volume = 0; - if (volume > 30) volume = 30; - sendCommand(CMD_VOLUME, 0x00, volume, false); -} - -void playAudioFile(int fileNumber) { - if (fileNumber < 1 || fileNumber > 10) return; - sendCommand(CMD_PLAY_TRACK, 0x00, fileNumber, false); - delay(60); -} - -void stopAudio() { - sendCommand(CMD_STOP, 0x00, 0x00, false); -} - -void calibrateSensors() { - Serial.println("Starting sensor calibration..."); - delay(500); - calibrateSensor(MQ2_ANALOG_PIN, &mq2_cal, "MQ2", MQ2_DANGER_THRESHOLD); - calibrateSensor(MQ9_ANALOG_PIN, &mq9_cal, "MQ9", MQ9_DANGER_THRESHOLD); - calibrateSensor(MQ135_ANALOG_PIN, &mq135_cal, "MQ135", MQ135_DANGER_THRESHOLD); - Serial.println("Calibration completed."); -} - -void calibrateSensor(int pin, SensorCalibration* cal, String sensorName, int minThreshold) { - Serial.print("Calibrating " + sensorName + " ..."); - float sum = 0; - for (int i=0;ibaseline = sum / CALIBRATION_SAMPLES; - - // FIXED: For MQ9 specifically, if baseline is already high, use static threshold - if (sensorName == "MQ9" && cal->baseline > (minThreshold * 0.8)) { - Serial.printf("\n ⚠ MQ9 baseline (%.0f) is high - using static threshold\n", cal->baseline); - cal->dangerThreshold = minThreshold; - } else { - float calculatedThreshold = cal->baseline * DANGER_MULTIPLIER; - cal->dangerThreshold = (calculatedThreshold > minThreshold) ? calculatedThreshold : minThreshold; - } - - cal->calibrated = true; - Serial.println(" Done"); - Serial.printf(" Baseline: %.0f, Danger threshold: %.0f\n", cal->baseline, cal->dangerThreshold); -} - -bool checkSensorDanger(int currentValue, SensorCalibration* cal, int staticDangerThreshold) { - if (!cal->calibrated) return currentValue >= staticDangerThreshold; - return currentValue >= cal->dangerThreshold; -} - -SensorData readAllSensors() { - SensorData data; - data.timestamp = millis(); - if (millis() - lastDHTReading > DHT_READING_INTERVAL) { - float rt = dht.readTemperature(); - float rh = dht.readHumidity(); - if (!isnan(rt) && rt >= -40 && rt <= 80) { data.temperature = rt; lastValidTemperature = rt; } - else data.temperature = lastValidTemperature; - if (!isnan(rh) && rh >= 0 && rh <= 100) { - float corr = rh - 30.0f; - if (corr < 0.0f) corr = 0.0f; - if (corr > 100.0f) corr = 100.0f; - data.humidity = corr; - lastValidHumidity = corr; - } else data.humidity = lastValidHumidity; - lastDHTReading = millis(); - } else { - data.temperature = lastValidTemperature; - data.humidity = lastValidHumidity; - } - data.mq2_analog = analogRead(MQ2_ANALOG_PIN); - data.mq9_analog = analogRead(MQ9_ANALOG_PIN); - data.mq135_analog = analogRead(MQ135_ANALOG_PIN); - data.mq2_digital = digitalRead(MQ2_DIGITAL_PIN) == LOW; - data.mq9_digital = digitalRead(MQ9_DIGITAL_PIN) == LOW; - data.mq135_digital = digitalRead(MQ135_DIGITAL_PIN) == LOW; - data.emergency = digitalRead(EMERGENCY_BUTTON_PIN) == LOW; - data.motion = motionData; - return data; -} - -void displayReadings(SensorData data) { - Serial.println("\n--- SENSOR SNAPSHOT ---"); - Serial.printf("Timestamp: %lu\n", data.timestamp); - Serial.printf("Temp: %.1f C Hum: %.1f %%\n", data.temperature, data.humidity); - Serial.printf("MQ2: %d (%s) MQ9: %d (%s) MQ135: %d (%s)\n", - data.mq2_analog, data.mq2_digital ? "ALERT":"OK", - data.mq9_analog, data.mq9_digital ? "ALERT":"OK", - data.mq135_analog, data.mq135_digital ? "ALERT":"OK"); - Serial.printf("Motion: accel=%.2fm/s2 gyro=%.2f deg/s\n", data.motion.totalAccel, data.motion.totalGyro); - Serial.printf("Emergency Button: %s\n", data.emergency ? "PRESSED" : "RELEASED"); - if (wristbandStatus.connected) { - unsigned long age = (millis() - wristbandStatus.lastUpdate) / 1000; - Serial.printf("Wristband: CONNECTED (BPM=%u SpO2=%u finger=%s, %lus ago)\n", - wristbandStatus.bpm, wristbandStatus.spo2, wristbandStatus.fingerDetected?"YES":"NO", age); - } else { - Serial.println("Wristband: DISCONNECTED"); - } -} - -void checkAlerts(SensorData data) { - // Alert cooldown tracking (static variables persist between function calls) - static unsigned long lastTempHighAlert = 0; - static unsigned long lastTempLowAlert = 0; - static unsigned long lastHumidityHighAlert = 0; - static unsigned long lastHumidityLowAlert = 0; - static unsigned long lastMQ2Alert = 0; - static unsigned long lastMQ9Alert = 0; - static unsigned long lastMQ135Alert = 0; - - const unsigned long ALERT_COOLDOWN = 300000; // 5 minutes in milliseconds - unsigned long now = millis(); - - // Temperature Alerts - if (data.temperature >= 45.0) { - if (lastTempHighAlert == 0 || (now - lastTempHighAlert >= ALERT_COOLDOWN)) { - Serial.println("!!! HIGH TEMPERATURE ALERT"); - Serial.printf(" Temperature: %.1f°C (threshold: 45°C)\n", data.temperature); - if (audioReady) playAudioFile(HIGH_TEMP_ALERT); - lastTempHighAlert = now; - } - } - - if (data.temperature <= 5.0) { - if (lastTempLowAlert == 0 || (now - lastTempLowAlert >= ALERT_COOLDOWN)) { - Serial.println("!!! LOW TEMPERATURE ALERT"); - Serial.printf(" Temperature: %.1f°C (threshold: 5°C)\n", data.temperature); - if (audioReady) playAudioFile(LOW_TEMP_ALERT); - lastTempLowAlert = now; - } - } - - // Humidity Alerts - if (data.humidity >= 80.0) { - if (lastHumidityHighAlert == 0 || (now - lastHumidityHighAlert >= ALERT_COOLDOWN)) { - Serial.println("!!! HIGH HUMIDITY ALERT"); - Serial.printf(" Humidity: %.1f%% (threshold: 80%%)\n", data.humidity); - if (audioReady) playAudioFile(HIGH_HUMIDITY_ALERT); - lastHumidityHighAlert = now; - } - } - - if (data.humidity <= 20.0) { - if (lastHumidityLowAlert == 0 || (now - lastHumidityLowAlert >= ALERT_COOLDOWN)) { - Serial.println("!!! LOW HUMIDITY ALERT"); - Serial.printf(" Humidity: %.1f%% (threshold: 20%%)\n", data.humidity); - if (audioReady) playAudioFile(LOW_HUMIDITY_ALERT); - lastHumidityLowAlert = now; - } - } - - // MQ2 Alert - bool mq2_danger = data.mq2_analog >= MQ2_DANGER_THRESHOLD || - (mq2_cal.calibrated && data.mq2_analog >= mq2_cal.dangerThreshold); - - if (mq2_danger) { - if (lastMQ2Alert == 0 || (now - lastMQ2Alert >= ALERT_COOLDOWN)) { - Serial.println("!!! MQ2 Danger detected"); - Serial.printf(" MQ2 analog=%d (static threshold=%d, calibrated=%.0f)\n", - data.mq2_analog, MQ2_DANGER_THRESHOLD, mq2_cal.dangerThreshold); - if (audioReady) playAudioFile(SMOKE_ALERT); - lastMQ2Alert = now; - } - } - - // MQ9 Alert - bool mq9_danger = data.mq9_analog >= MQ9_DANGER_THRESHOLD || - (mq9_cal.calibrated && data.mq9_analog >= mq9_cal.dangerThreshold); - - if (mq9_danger) { - if (lastMQ9Alert == 0 || (now - lastMQ9Alert >= ALERT_COOLDOWN)) { - Serial.println("!!! MQ9 Danger detected"); - Serial.printf(" MQ9 analog=%d (static threshold=%d, calibrated=%.0f)\n", - data.mq9_analog, MQ9_DANGER_THRESHOLD, mq9_cal.dangerThreshold); - if (audioReady) { - playAudioFile(CO_ALERT); - Serial.println(" ✓ Playing CO alert audio (0003.mp3)"); - } - lastMQ9Alert = now; - } - } - - // MQ135 Alert - bool mq135_danger = data.mq135_analog >= MQ135_DANGER_THRESHOLD || - (mq135_cal.calibrated && data.mq135_analog >= mq135_cal.dangerThreshold); - - if (mq135_danger) { - if (lastMQ135Alert == 0 || (now - lastMQ135Alert >= ALERT_COOLDOWN)) { - Serial.println("!!! MQ135 Air quality degraded"); - Serial.printf(" MQ135 analog=%d (static threshold=%d, calibrated=%.0f, rating=%s)\n", - data.mq135_analog, MQ135_DANGER_THRESHOLD, mq135_cal.dangerThreshold, - getAirQualityRating(data.mq135_analog).c_str()); - if (audioReady) playAudioFile(AIR_QUALITY_WARNING); - lastMQ135Alert = now; - } - } -} -void sendLoRaData(SensorData data) { - StaticJsonDocument<512> doc; - doc["node"] = NODE_ID; - doc["timestamp"] = data.timestamp; - doc["temp"] = data.temperature; - doc["hum"] = data.humidity; - doc["mq2"] = data.mq2_analog; - doc["mq9"] = data.mq9_analog; - doc["mq135"] = data.mq135_analog; - - // CRITICAL: Explicitly set emergency field (not as 0/1 but as boolean) - doc["emergency"] = data.emergency; // This will be true or false - - doc["motion_accel"] = data.motion.totalAccel; - doc["motion_gyro"] = data.motion.totalGyro; - doc["bpm"] = wristbandStatus.connected ? wristbandStatus.bpm : 0; - doc["spo2"] = wristbandStatus.connected ? wristbandStatus.spo2 : 0; - doc["wristband_connected"] = wristbandStatus.connected ? 1 : 0; - - char payload[512]; - size_t n = serializeJson(doc, payload, sizeof(payload)); - - if (!loraReady) { - Serial.println("LoRa not ready - cannot send"); - return; - } - - LoRa.beginPacket(); - LoRa.print(payload); - LoRa.endPacket(); - - packetCount++; - - // Enhanced logging for emergency packets - if (data.emergency) { - Serial.println("\n╔═══════════════════════════════════════╗"); - Serial.println("║ EMERGENCY PACKET TRANSMITTED ║"); - Serial.println("╚═══════════════════════════════════════╝"); - } - - Serial.printf("LoRa packet #%d sent (%d bytes)%s\n", - packetCount, (int)n, data.emergency ? " [EMERGENCY]" : ""); - Serial.println("Payload: " + String(payload)); -} - -// ---------------- ESP-NOW Implementation ---------------- -void initESPNOW() { - // Ensure WiFi is properly initialized first - WiFi.mode(WIFI_STA); - WiFi.disconnect(false); - delay(100); - WiFi.begin(); - delay(100); - - int channel = 1; - esp_err_t cherr = esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE); - if (cherr == ESP_OK) { - Serial.printf("✓ WiFi channel set to %d for ESP-NOW\n", channel); - } else { - Serial.printf("⚠ Failed to set WiFi channel (%d) err=%d\n", channel, (int)cherr); - } - - if (esp_now_init() != ESP_OK) { - Serial.println("⚠ ESP-NOW init failed - retrying..."); - delay(500); - if (esp_now_init() != ESP_OK) { - Serial.println("✗ ESP-NOW init failed after retry"); - espnowReady = false; - return; - } - } - espnowReady = true; - Serial.println("✓ ESP-NOW initialized"); - - esp_now_register_send_cb(onDataSent); - esp_now_register_recv_cb(onDataRecv); - - delay(100); - - esp_now_peer_info_t peerInfo; - memset(&peerInfo, 0, sizeof(peerInfo)); - memcpy(peerInfo.peer_addr, wristbandMac, 6); - peerInfo.channel = channel; - #if defined(ESP_IF_WIFI_STA) - peerInfo.ifidx = ESP_IF_WIFI_STA; - #elif defined(WIFI_IF_STA) - peerInfo.ifidx = WIFI_IF_STA; - #else - peerInfo.ifidx = (wifi_interface_t)0; - #endif - peerInfo.encrypt = false; - - if (!esp_now_is_peer_exist(wristbandMac)) { - esp_err_t addStatus = esp_now_add_peer(&peerInfo); - if (addStatus != ESP_OK) { - Serial.printf("⚠ Failed to add ESP-NOW peer (wristband) - error: %d\n", addStatus); - } else { - Serial.println("✓ Wristband ESP-NOW peer added"); - Serial.printf(" Peer MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", - wristbandMac[0], wristbandMac[1], wristbandMac[2], - wristbandMac[3], wristbandMac[4], wristbandMac[5]); - } - } else { - Serial.println("✓ Wristband peer already exists"); - } - - delay(100); -} - - -void onDataRecv(const esp_now_recv_info_t *recv_info, const uint8_t *data, int len) { - if (data == NULL || len < 1) return; - - uint8_t msgType = data[0]; - - if (msgType == MSG_TYPE_VITALS) { - if (len < (int)sizeof(espnow_vitals_t)) { - Serial.println("⚠ Received VITALS unexpected length"); - return; - } - - espnow_vitals_t vitals; - memcpy(&vitals, data, sizeof(vitals)); - - wristbandStatus.bpm = vitals.bpm; - wristbandStatus.spo2 = vitals.spo2; - wristbandStatus.fingerDetected = (vitals.finger != 0); - wristbandStatus.lastUpdate = millis(); - wristbandStatus.connected = true; - - Serial.printf("[ESP-NOW RX] VITALS -> BPM=%u SpO2=%u finger=%s ts=%lu\n", - wristbandStatus.bpm, - wristbandStatus.spo2, - wristbandStatus.fingerDetected ? "YES" : "NO", - (unsigned long)vitals.timestamp); - - } else if (msgType == MSG_TYPE_ACK) { - if (len < (int)sizeof(espnow_ack_t)) { - Serial.println("⚠ Received ACK unexpected length"); - return; - } - - espnow_ack_t ack; - memcpy(&ack, data, sizeof(ack)); - - if (ack.messageId == wristbandStatus.lastMessageId) { - wristbandStatus.messageAcknowledged = (ack.success != 0); - - Serial.printf("[ESP-NOW RX] ACK for msgId=%lu success=%s\n", - (unsigned long)ack.messageId, - ack.success ? "YES" : "NO"); - - // NEW: Play audio when message is successfully acknowledged by wristband - if (ack.success && audioReady) { - delay(200); // Wait for ESP-NOW to finish - playAudioFile(MESSAGE_RECEIVED); - Serial.println("✓ Playing message received confirmation audio"); - delay(100); // Ensure audio command is sent -} - - } else { - Serial.printf("[ESP-NOW RX] ACK for unknown msgId=%lu\n", - (unsigned long)ack.messageId); - } - - } else { - Serial.printf("[ESP-NOW RX] Unknown msgType: 0x%02X\n", msgType); - } -} - -void onDataSent(const wifi_tx_info_t *tx_info, esp_now_send_status_t status) { - lastEspNowSendStatus = (status == ESP_NOW_SEND_SUCCESS) ? ESP_OK : ESP_FAIL; - char macStr[18]; - sprintf(macStr, "%02X:%02X:%02X:%02X:%02X:%02X", - wristbandMac[0], wristbandMac[1], wristbandMac[2], - wristbandMac[3], wristbandMac[4], wristbandMac[5]); - Serial.printf("[ESP-NOW TX] to %s status=%s\n", macStr, (status == ESP_NOW_SEND_SUCCESS) ? "OK":"FAIL"); -} - -bool sendTextToWristband(const String &message) { - if (!espnowReady) { Serial.println("ESP-NOW not ready - cannot send text"); return false; } - espnow_text_t pkt; memset(&pkt,0,sizeof(pkt)); - pkt.msgType = MSG_TYPE_TEXT; - pkt.messageId = outgoingMessageCounter++; - size_t len = message.length(); - if (len > MAX_TEXT_LEN - 1) len = MAX_TEXT_LEN - 1; - pkt.length = (uint8_t)len; - memcpy(pkt.text, message.c_str(), pkt.length); - pkt.text[pkt.length] = '\0'; - esp_err_t rc = esp_now_send(wristbandMac, (uint8_t *)&pkt, sizeof(pkt)); - if (rc == ESP_OK) { - wristbandStatus.lastMessageId = pkt.messageId; - wristbandStatus.messageAcknowledged = false; - Serial.printf("Sent TEXT msgId=%lu len=%u\n", (unsigned long)pkt.messageId, pkt.length); - return true; - } else { - Serial.printf("Failed to send TEXT (esp_now_send rc=%d)\n", rc); - return false; - } -} - -void checkWristbandConnection() { - unsigned long now = millis(); - if (wristbandStatus.connected && (now - wristbandStatus.lastUpdate > 35000UL)) { - wristbandStatus.connected = false; - Serial.println("Wristband connection timed out (35s) -> DISCONNECTED"); - - // ADD THESE LINES: - // Clear vitals data when disconnected - wristbandStatus.bpm = 0; - wristbandStatus.spo2 = 0; - wristbandStatus.fingerDetected = false; - } -} -void receiveLoRaMessages() { - int packetSize = LoRa.parsePacket(); - if (packetSize <= 0) return; - - String incoming = ""; - while (LoRa.available()) incoming += (char)LoRa.read(); - incoming.trim(); - if (incoming.length() == 0) return; - - // Get signal quality - int rssi = LoRa.packetRssi(); - float snr = LoRa.packetSnr(); - - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.printf("║ LORA MESSAGE RECEIVED (RSSI: %4d dBm) ║\n", rssi); - Serial.println("╚════════════════════════════════════════════╝"); - Serial.println("Packet size: " + String(incoming.length()) + " bytes"); - Serial.println("SNR: " + String(snr, 1) + " dB"); - Serial.println("Raw data: " + incoming); - - // Parse JSON - StaticJsonDocument<256> doc; - DeserializationError err = deserializeJson(doc, incoming); - - if (err) { - Serial.println("❌ ERROR: JSON parse failed - " + String(err.c_str())); - Serial.println("════════════════════════════════════════════\n"); - return; - } - - // Check if this is a message relay packet - if (doc.containsKey("message")) { - String message = doc["message"].as(); - String from = doc["from"] | "unknown"; - unsigned long timestamp = doc["timestamp"] | 0; - - Serial.println("\n┌────────────────────────────────────────────┐"); - Serial.println("│ 📨 MESSAGE RELAY REQUEST DETECTED │"); - Serial.println("├────────────────────────────────────────────┤"); - Serial.printf("│ From: %-37s│\n", from.c_str()); - Serial.printf("│ Timestamp: %-32lu│\n", timestamp); - Serial.printf("│ Message: %-34s│\n", message.substring(0, 34).c_str()); - if (message.length() > 34) { - Serial.printf("│ %-34s│\n", message.substring(34, 68).c_str()); - } - Serial.println("└────────────────────────────────────────────┘"); - - Serial.println("\n→ Forwarding to wristband via ESP-NOW..."); - - // Forward to wristband - bool success = sendTextToWristband(message); - - - if (success) { - messagesRelayedToWristband++; - Serial.println("✓ Message forwarded successfully!"); - Serial.println(" Total messages relayed: " + String(messagesRelayedToWristband)); - - // Play audio confirmation (if audio is ready) - if (audioReady) { - delay(100); // Small delay before audio - // Note: MESSAGE_RECEIVED audio (0009.mp3) will play automatically - // when wristband sends ACK in onDataRecv() - } - } else { - Serial.println("✗ Failed to forward message to wristband"); - Serial.println(" Check: ESP-NOW status, wristband connection"); - } - - Serial.println("════════════════════════════════════════════\n"); - } - else { - // Not a message packet - could be sensor data or other packet type - Serial.println("ℹ️ Packet received but no 'message' field found"); - Serial.println(" (This is normal for sensor data packets)"); - - // List available fields for debugging - Serial.print(" Available fields: "); - JsonObject obj = doc.as(); - for (JsonPair kv : obj) { - Serial.print(kv.key().c_str()); - Serial.print(" "); - } - Serial.println(); - Serial.println("════════════════════════════════════════════\n"); - } -} -// Enhanced system status with more details -void printSystemStatus() { - unsigned long uptime = millis() / 1000; - int hours = uptime / 3600; - int minutes = (uptime % 3600) / 60; - int seconds = uptime % 60; - - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.println("║ SYSTEM STATUS REPORT ║"); - Serial.println("╚════════════════════════════════════════════╝"); - - Serial.printf("Uptime: %02d:%02d:%02d\n", hours, minutes, seconds); - Serial.println(); - - Serial.println("Hardware Status:"); - Serial.printf(" LoRa: %s (Packets sent: %d)\n", - loraReady ? "✓ Active" : "✗ Offline", packetCount); - Serial.printf(" ESP-NOW: %s\n", - espnowReady ? "✓ Active" : "✗ Offline"); - Serial.printf(" MPU6050: %s\n", - mpuReady ? "✓ Active" : "✗ Offline"); - Serial.printf(" DHT11: %s\n", - dhtReady ? "✓ Active" : "⚠ Fallback"); - Serial.printf(" Audio: %s\n", - audioReady ? "✓ Active" : "✗ Offline"); - Serial.println(); - - // ========== ADD THIS BLOCK HERE ========== - Serial.println("Message Relay Status:"); - Serial.printf(" Messages relayed to wristband: %lu\n", messagesRelayedToWristband); - Serial.printf(" Last message ID sent: %lu\n", (unsigned long)(outgoingMessageCounter - 1)); - if (wristbandStatus.lastMessageId > 0) { - Serial.printf(" Last message acknowledged: %s\n", - wristbandStatus.messageAcknowledged ? "✓ Yes" : "✗ No"); - } - Serial.println(); - // ========== END OF NEW BLOCK ========== - - if (espnowReady) { - if (wristbandStatus.connected) { - unsigned long age = (millis() - wristbandStatus.lastUpdate) / 1000; - Serial.println("Wristband Status:"); - Serial.printf(" Connection: ✓ Active (%lu seconds ago)\n", age); - Serial.printf(" Heart Rate: %u BPM\n", wristbandStatus.bpm); - Serial.printf(" SpO2: %u%%\n", wristbandStatus.spo2); - Serial.printf(" Finger: %s\n", wristbandStatus.fingerDetected ? "Detected" : "None"); - } else { - Serial.println("Wristband Status:"); - Serial.println(" Connection: ✗ Disconnected"); - } - Serial.println(); - } - - Serial.println("Sensor Calibration:"); - Serial.printf(" MQ2: Baseline=%.0f Threshold=%.0f %s\n", - mq2_cal.baseline, mq2_cal.dangerThreshold, - mq2_cal.calibrated ? "✓" : "✗"); - Serial.printf(" MQ9: Baseline=%.0f Threshold=%.0f %s\n", - mq9_cal.baseline, mq9_cal.dangerThreshold, - mq9_cal.calibrated ? "✓" : "✗"); - Serial.printf(" MQ135: Baseline=%.0f Threshold=%.0f %s\n", - mq135_cal.baseline, mq135_cal.dangerThreshold, - mq135_cal.calibrated ? "✓" : "✗"); - - Serial.println("\n════════════════════════════════════════════\n"); -} diff --git a/14-12-2025/WorkingWatchCode.ino b/14-12-2025/WorkingWatchCode.ino deleted file mode 100644 index 365bbf0..0000000 --- a/14-12-2025/WorkingWatchCode.ino +++ /dev/null @@ -1,844 +0,0 @@ -// wristband_espnow.ino -// PRODUCTION READY - Wristband with MAX30102 + SSD1306 + ESP-NOW -// MANUFACTURER ALGORITHMS - Accurate BPM, SpO2, and Temperature -// FIXED: BPM Detection Issues - -#include -#include -#include -#include -#include -#include -#include -#include - -// OLED Display -#define SCREEN_WIDTH 128 -#define SCREEN_HEIGHT 64 -#define OLED_RESET -1 -Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); - -// I2C Pins -#define SDA_PIN 4 -#define SCL_PIN 5 - -MAX30105 particleSensor; - -// Heart rate calculation - IMPROVED PEAK DETECTION -const byte RATE_SIZE = 4; -byte rates[RATE_SIZE] = {0, 0, 0, 0}; -byte rateSpot = 0; -long lastBeat = 0; -float beatsPerMinute = 0; -int beatAvg = 0; - -// Improved beat detection -const int MIN_BEAT_INTERVAL = 300; // Minimum ms between beats (200 BPM max) -const int MAX_BEAT_INTERVAL = 2000; // Maximum ms between beats (30 BPM min) -long prevIR = 0; -long prev2IR = 0; -long prev3IR = 0; -int beatCount = 0; - -// Peak detection variables -const long IR_THRESHOLD = 50000; // Minimum IR value for finger detection -const long PEAK_THRESHOLD = 2000; // Minimum change to be considered a peak -long peakValue = 0; -unsigned long peakTime = 0; -bool lookingForPeak = true; -long valleyValue = 0; - -// SpO2 calculation - from manufacturer Example 3 -uint32_t irBuffer[100]; // infrared LED sensor data -uint32_t redBuffer[100]; // red LED sensor data -int32_t bufferLength = 100; -int32_t spo2 = 0; // SPO2 value -int8_t validSPO2 = 0; // indicator to show if the SPO2 calculation is valid -int32_t heartRate = 0; // heart rate value from SpO2 algorithm -int8_t validHeartRate = 0; // indicator to show if the heart rate calculation is valid - -byte bufferIndex = 0; -bool bufferFilled = false; -unsigned long lastSpO2Calc = 0; -const unsigned long SPO2_CALC_INTERVAL = 1000; // Calculate every 1 second - -// Temperature - from manufacturer Example 4 -float currentTemperature = 0.0; -unsigned long lastTempRead = 0; -const unsigned long TEMP_READ_INTERVAL = 5000; // Read every 5 seconds - -// Timing -unsigned long lastUpdate = 0; -const unsigned long UPDATE_INTERVAL = 100; - -// ESP-NOW message types -#define MSG_TYPE_VITALS 0x01 -#define MSG_TYPE_TEXT 0x02 -#define MSG_TYPE_ACK 0x03 -#define MAX_TEXT_LEN 128 - -typedef struct __attribute__((packed)) { - uint8_t msgType; - uint8_t bpm; - uint8_t spo2; - uint8_t finger; - int8_t temperature; // Temperature in Celsius (can be negative) - uint32_t timestamp; -} espnow_vitals_t; - -typedef struct __attribute__((packed)) { - uint8_t msgType; - uint32_t messageId; - uint8_t length; - char text[MAX_TEXT_LEN]; -} espnow_text_t; - -typedef struct __attribute__((packed)) { - uint8_t msgType; - uint32_t messageId; - uint8_t success; -} espnow_ack_t; - -// Peer MAC -uint8_t edgeNodeMac[6] = {0x28, 0x56, 0x2F, 0x49, 0x56, 0xAC}; - -// Display state -bool displayingMessage = false; -String currentMessage = ""; -unsigned long messageStartTime = 0; -uint32_t currentMessageId = 0; -int scrollOffset = 0; -unsigned long lastScrollUpdate = 0; -const unsigned long SCROLL_SPEED = 150; - -bool edgeNodeConnected = false; -unsigned long lastEdgeNodeContact = 0; -bool espnowReady = false; - -const unsigned long VITALS_INTERVAL = 25000UL; -unsigned long lastVitalsSent = 0; -const unsigned long MESSAGE_DISPLAY_MS = 15000UL; -const unsigned long EDGE_NODE_DISCONNECT_MS = 90000UL; - -// Forward declarations -void initESPNOW(); -void onDataRecv(const esp_now_recv_info_t *recv_info, const uint8_t *data, int len); -void onDataSent(const wifi_tx_info_t *tx_info, esp_now_send_status_t status); -void sendVitals(uint8_t bpm, uint8_t spo2, bool fingerDetected, float temp); -void sendAcknowledgment(uint32_t messageId, bool success); -void checkConnection(); -void displayMessageScreen(const String &msg, uint32_t messageId); -void displayVitalsScreen(uint8_t bpm, uint8_t spo2, bool finger, bool connected, float temp); -void scanBus(); -int calculateTextHeight(const String &text, int maxWidthChars, int lineHeight); - -void setup() { - Serial.begin(115200); - delay(200); - - Serial.println("\n\nMAX30102 + OLED + ESP-NOW (MANUFACTURER ALGORITHMS)"); - - // ================= I2C FIRST ================= - Wire.begin(SDA_PIN, SCL_PIN); - Wire.setClock(100000); - delay(200); - - Serial.println("\n=== I2C Device Scan (Pre-init) ==="); - scanBus(); - delay(200); - - // ================= OLED FIRST ================= - if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { - Serial.println("ERROR: OLED not found!"); - while (1); - } - - display.clearDisplay(); - - // Emergency light icons (top corners) - // Left siren - display.fillCircle(15, 8, 6, SSD1306_WHITE); - display.fillCircle(15, 8, 4, SSD1306_BLACK); - display.drawLine(10, 3, 10, 8, SSD1306_WHITE); - display.drawLine(15, 1, 15, 8, SSD1306_WHITE); - display.drawLine(20, 3, 20, 8, SSD1306_WHITE); - - // Right siren - display.fillCircle(113, 8, 6, SSD1306_WHITE); - display.fillCircle(113, 8, 4, SSD1306_BLACK); - display.drawLine(108, 3, 108, 8, SSD1306_WHITE); - display.drawLine(113, 1, 113, 8, SSD1306_WHITE); - display.drawLine(118, 3, 118, 8, SSD1306_WHITE); - - // Large SIREN text - centered - display.setTextSize(3); - display.setTextColor(SSD1306_WHITE); - display.setCursor(16, 22); - display.print("SIREN"); - - // Status text at bottom - display.setTextSize(1); - display.setCursor(18, 54); - display.print("Initializing..."); - - // Bottom decorative line - display.drawLine(0, 50, 128, 50, SSD1306_WHITE); - - display.display(); -// Rest of setup continues... - - delay(1500); - - // ================= MAX30102 SECOND - MANUFACTURER CONFIGURATION ================= - if (!particleSensor.begin(Wire, I2C_SPEED_STANDARD)) { - display.clearDisplay(); - display.setTextSize(1); - display.setCursor(0, 0); - display.println("MAX30102 ERROR"); - display.display(); - Serial.println("MAX30102 not found!"); - while (1); - } - - // Configuration from manufacturer Example 3 (SpO2) - byte ledBrightness = 60; // Options: 0=Off to 255=50mA - byte sampleAverage = 4; // Options: 1, 2, 4, 8, 16, 32 - byte ledMode = 2; // Options: 1 = Red only, 2 = Red + IR, 3 = Red + IR + Green - byte sampleRate = 100; // Options: 50, 100, 200, 400, 800, 1000, 1600, 3200 - int pulseWidth = 411; // Options: 69, 118, 215, 411 - int adcRange = 4096; // Options: 2048, 4096, 8192, 16384 - - particleSensor.setup(ledBrightness, sampleAverage, ledMode, sampleRate, pulseWidth, adcRange); - - // Additional configuration for temperature - particleSensor.enableDIETEMPRDY(); // Enable temperature ready interrupt - - // Now safe to increase I2C speed - Wire.setClock(400000); - - Serial.println("✓ MAX30102 configured with manufacturer settings"); - Serial.println("Place finger with steady pressure for 4 seconds..."); - - // ================= WIFI / ESP-NOW LAST ================= - WiFi.mode(WIFI_STA); - WiFi.disconnect(false); - delay(100); - - WiFi.begin(); - delay(100); - - esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE); - initESPNOW(); - delay(200); - - // ================= INITIAL BUFFER FILL ================= - display.clearDisplay(); - display.setTextSize(1); - display.setCursor(0, 0); - display.println("Initializing..."); - display.setCursor(0, 15); - display.println("Collecting samples"); - display.display(); - - // Collect initial 100 samples (4 seconds at 25sps) - Serial.println("Collecting initial 100 samples..."); - for (byte i = 0; i < bufferLength; i++) { - while (particleSensor.available() == false) - particleSensor.check(); - - redBuffer[i] = particleSensor.getRed(); - irBuffer[i] = particleSensor.getIR(); - particleSensor.nextSample(); - // delay(40); - - if (i % 10 == 0) { - display.setCursor(0, 30); - display.fillRect(0, 30, 128, 20, SSD1306_BLACK); - display.printf("Progress: %d%%", (i * 100) / bufferLength); - display.display(); - } - } - - bufferFilled = true; - bufferIndex = 0; - - // Calculate initial SpO2 - maxim_heart_rate_and_oxygen_saturation(irBuffer, bufferLength, redBuffer, &spo2, &validSPO2, &heartRate, &validHeartRate); - lastSpO2Calc = millis(); - - // ================= FINAL UI ================= - display.clearDisplay(); - display.setTextSize(1); - display.setCursor(0, 0); - display.println("Heart Rate Monitor"); - display.setCursor(0, 15); - display.println("Ready!"); - display.display(); - - lastVitalsSent = millis() - (VITALS_INTERVAL - 5000); - Serial.println("\n✓ Wristband ready (MANUFACTURER ALGORITHMS)"); - Serial.println("System operational"); -} - -void loop() { - // Check sensor availability first - while (particleSensor.available() == false) - particleSensor.check(); - - long irValue = particleSensor.getIR(); - long redValue = particleSensor.getRed(); - - // === IMPROVED PEAK DETECTION === - // Only process if finger is detected - if (irValue > IR_THRESHOLD) { - - // Initialize valley on first reading - if (valleyValue == 0) { - valleyValue = irValue; - } - - // Looking for peak (going up) - if (lookingForPeak) { - if (irValue > peakValue) { - peakValue = irValue; - peakTime = millis(); - } - // If value drops significantly, we found a peak - else if (peakValue - irValue > PEAK_THRESHOLD) { - // We have a peak! Check if it's a valid beat - unsigned long currentTime = millis(); - unsigned long timeSinceLastBeat = currentTime - lastBeat; - - if (lastBeat == 0) { - // First beat - lastBeat = currentTime; - Serial.println(">>> First beat detected!"); - } - else if (timeSinceLastBeat >= MIN_BEAT_INTERVAL && timeSinceLastBeat <= MAX_BEAT_INTERVAL) { - // Valid beat! - beatsPerMinute = 60000.0 / timeSinceLastBeat; - - if (beatsPerMinute >= 40 && beatsPerMinute <= 200) { - rates[rateSpot++] = (byte)beatsPerMinute; - rateSpot %= RATE_SIZE; - - // Calculate average - beatAvg = 0; - int validCount = 0; - for (byte x = 0; x < RATE_SIZE; x++) { - if (rates[x] > 0) { - beatAvg += rates[x]; - validCount++; - } - } - if (validCount > 0) beatAvg /= validCount; - - beatCount++; - lastBeat = currentTime; - - Serial.println(); - Serial.print("♥♥♥ BEAT #"); - Serial.print(beatCount); - Serial.print("! BPM="); - Serial.print(beatsPerMinute, 1); - Serial.print(", Avg="); - Serial.print(beatAvg); - Serial.print(", Interval="); - Serial.print(timeSinceLastBeat); - Serial.print("ms, Peak="); - Serial.print(peakValue); - Serial.print(", Valley="); - Serial.print(valleyValue); - Serial.print(", Amplitude="); - Serial.print(peakValue - valleyValue); - Serial.println(" ♥♥♥"); - Serial.println(); - } - } - else if (timeSinceLastBeat > MAX_BEAT_INTERVAL) { - // Too long since last beat, reset - Serial.println(">>> Timeout - resetting beat detection"); - lastBeat = currentTime; - } - - // Switch to looking for valley - lookingForPeak = false; - valleyValue = irValue; - } - } - // Looking for valley (going down) - else { - if (irValue < valleyValue) { - valleyValue = irValue; - } - // If value rises significantly, we found a valley - else if (irValue - valleyValue > PEAK_THRESHOLD) { - // Switch to looking for peak - lookingForPeak = true; - peakValue = irValue; - } - } - } - else { - // No finger detected - reset - peakValue = 0; - valleyValue = 0; - lookingForPeak = true; - } - - // === SPO2 CONTINUOUS SAMPLING (Example 3) === - // WITH THIS: - if (bufferFilled) { - // Only add sample if finger is detected (prevents polluting buffer with noise) - if (irValue > IR_THRESHOLD) { - redBuffer[bufferIndex] = redValue; - irBuffer[bufferIndex] = irValue; - bufferIndex++; - } - particleSensor.nextSample(); - - // Once we've collected 100 fresh samples, run the SpO2 algorithm - if (bufferIndex >= 100) { - maxim_heart_rate_and_oxygen_saturation( - irBuffer, bufferLength, redBuffer, - &spo2, &validSPO2, &heartRate, &validHeartRate - ); - lastSpO2Calc = millis(); - bufferIndex = 0; // Reset for next 100-sample window - - Serial.print("SpO2 Algorithm: HR="); - Serial.print(heartRate); - Serial.print(", HRvalid="); - Serial.print(validHeartRate); - Serial.print(", SPO2="); - Serial.print(spo2); - Serial.print(", SPO2Valid="); - Serial.println(validSPO2); - } - - // If finger removed mid-collection, reset buffer to avoid stale data - if (irValue <= IR_THRESHOLD && bufferIndex > 0) { - bufferIndex = 0; - spo2 = 0; - validSPO2 = 0; - } - } - - // === TEMPERATURE READING (Example 4) === - if (millis() - lastTempRead >= TEMP_READ_INTERVAL) { - currentTemperature = particleSensor.readTemperature(); - lastTempRead = millis(); - Serial.print("Temperature: "); - Serial.print(currentTemperature, 2); - Serial.println("°C"); - } - - // === DISPLAY UPDATE === - if (millis() - lastUpdate > UPDATE_INTERVAL) { - lastUpdate = millis(); - - if (displayingMessage) { - unsigned long elapsed = millis() - messageStartTime; - if (elapsed >= MESSAGE_DISPLAY_MS) { - displayingMessage = false; - currentMessage = ""; - scrollOffset = 0; - Serial.println("Message display timeout - returning to vitals"); - } else { - displayMessageScreen(currentMessage, currentMessageId); - } - } else { - bool fingerDetected = (irValue > IR_THRESHOLD); - - // Use simple beat detection BPM (more reliable for real-time display) - uint8_t displayBPM = beatAvg; - uint8_t displaySpO2 = 0; - - if (validSPO2 == 1 && spo2 > 0 && spo2 <= 100 && fingerDetected) { - displaySpO2 = (uint8_t)spo2; - } -// } else if (fingerDetected && beatAvg > 0) { -// // If SpO2 algorithm fails but we have heartbeat, estimate from HR -// displaySpO2 = 97; // Show reasonable default when finger is detected -// } - - displayVitalsScreen(displayBPM, displaySpO2, fingerDetected, edgeNodeConnected, currentTemperature); - } - - // Debug output - Serial.print("IR="); - Serial.print(irValue); - Serial.print(", State="); - Serial.print(lookingForPeak ? "PEAK" : "VALLEY"); - Serial.print(", Peak="); - Serial.print(peakValue); - Serial.print(", Valley="); - Serial.print(valleyValue); - Serial.print(", BPM="); - Serial.print(beatsPerMinute, 1); - Serial.print(", Avg="); - Serial.print(beatAvg); - Serial.print(", SpO2="); - Serial.print(spo2); - Serial.print("%, Temp="); - Serial.print(currentTemperature, 1); - Serial.print("°C, Beats="); - Serial.println(beatCount); - } - - // === SEND VITALS VIA ESP-NOW === - if (millis() - lastVitalsSent >= VITALS_INTERVAL) { - long irVal = particleSensor.getIR(); - bool fingerDetected = (irVal > IR_THRESHOLD); - uint8_t bpmSend = 0; - uint8_t spo2Send = 0; - - if (fingerDetected) { - if (beatAvg > 0 && beatAvg < 255) { - bpmSend = (uint8_t)beatAvg; - } - - // Use validated SpO2 from algorithm - if (validSPO2 == 1 && spo2 > 0 && spo2 <= 100) { - spo2Send = (uint8_t)spo2; - } else if (spo2 > 80 && spo2 <= 100) { - // validSPO2 may be 0 but value is physiologically plausible — send it - spo2Send = (uint8_t)spo2; - } - } - - sendVitals(bpmSend, spo2Send, fingerDetected, currentTemperature); - lastVitalsSent = millis(); - } - - checkConnection(); - delay(20); -} - -void scanBus() { - int foundCount = 0; - for(byte i = 0; i < 128; i++) { - Wire.beginTransmission(i); - byte error = Wire.endTransmission(); - if (error == 0) { - Serial.print(" Found device at: 0x"); - if (i < 16) Serial.print("0"); - Serial.println(i, HEX); - foundCount++; - } - } - Serial.print(" Total: "); - Serial.print(foundCount); - Serial.println(" device(s)\n"); -} - -void initESPNOW() { - if (WiFi.status() == WL_NO_SHIELD) { - Serial.println("⚠ WiFi not ready"); - WiFi.mode(WIFI_STA); - delay(100); - } - // ADD these lines BEFORE esp_now_init(): -esp_wifi_set_promiscuous(true); -esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE); -esp_wifi_set_promiscuous(false); -delay(200); - - if (esp_now_init() != ESP_OK) { - Serial.println("⚠ ESP-NOW init failed - retrying..."); - delay(500); - if (esp_now_init() != ESP_OK) { - Serial.println("✗ ESP-NOW init failed"); - espnowReady = false; - return; - } - } - espnowReady = true; - Serial.println("✓ ESP-NOW initialized"); - - esp_now_register_send_cb(onDataSent); - esp_now_register_recv_cb(onDataRecv); - delay(100); - - esp_now_peer_info_t peerInfo; - memset(&peerInfo, 0, sizeof(peerInfo)); - memcpy(peerInfo.peer_addr, edgeNodeMac, 6); - peerInfo.channel = 1; - - #if defined(ESP_IF_WIFI_STA) - peerInfo.ifidx = ESP_IF_WIFI_STA; - #elif defined(WIFI_IF_STA) - peerInfo.ifidx = WIFI_IF_STA; - #else - peerInfo.ifidx = (wifi_interface_t)0; - #endif - - peerInfo.encrypt = false; - - if (!esp_now_is_peer_exist(edgeNodeMac)) { - esp_err_t addStatus = esp_now_add_peer(&peerInfo); - if (addStatus != ESP_OK) { - Serial.printf("⚠ Failed to add peer - error: %d\n", addStatus); - } else { - Serial.println("✓ Edge node peer added"); - } - } - delay(100); -} - -void onDataRecv(const esp_now_recv_info_t *recv_info, const uint8_t *data, int len) { - if (data == NULL || len < 1) return; - uint8_t msgType = data[0]; - - lastEdgeNodeContact = millis(); - edgeNodeConnected = true; - - if (msgType == MSG_TYPE_TEXT) { - if (len < 6) return; - uint32_t msgId = 0; - uint8_t lengthField = 0; - memcpy(&msgId, &data[1], sizeof(msgId)); - memcpy(&lengthField, &data[5], 1); - if (lengthField > MAX_TEXT_LEN - 1) lengthField = MAX_TEXT_LEN - 1; - char txtbuf[MAX_TEXT_LEN + 1]; - memset(txtbuf, 0, sizeof(txtbuf)); - int copyLen = min((int)lengthField, len - 6); - if (copyLen > 0) memcpy(txtbuf, &data[6], copyLen); - txtbuf[copyLen] = '\0'; - - String receivedText = String(txtbuf); - Serial.printf("[ESP-NOW RX] TEXT msgId=%lu text='%s'\n", (unsigned long)msgId, receivedText.c_str()); - - scrollOffset = 0; - lastScrollUpdate = millis(); - displayingMessage = true; - currentMessage = receivedText; - messageStartTime = millis(); - currentMessageId = msgId; - - displayMessageScreen(receivedText, msgId); - sendAcknowledgment(msgId, true); - } -} - -void onDataSent(const wifi_tx_info_t *tx_info, esp_now_send_status_t status) { - Serial.printf("[ESP-NOW TX] status=%s\n", (status == ESP_NOW_SEND_SUCCESS) ? "OK":"FAIL"); -} - -void sendVitals(uint8_t bpm, uint8_t spo2, bool fingerDetected, float temp) { - if (!espnowReady) return; - espnow_vitals_t pkt; - pkt.msgType = MSG_TYPE_VITALS; - pkt.bpm = bpm; - pkt.spo2 = spo2; - pkt.finger = fingerDetected ? 1 : 0; - pkt.temperature = (int8_t)temp; // Convert to integer Celsius - pkt.timestamp = (uint32_t)millis(); - - esp_err_t rc = esp_now_send(edgeNodeMac, (uint8_t *)&pkt, sizeof(pkt)); - if (rc == ESP_OK) { - Serial.printf("Sent VITALS bpm=%u spo2=%u finger=%s temp=%d°C\n", - bpm, spo2, fingerDetected?"YES":"NO", pkt.temperature); - } -} - -void sendAcknowledgment(uint32_t messageId, bool success) { - if (!espnowReady) return; - espnow_ack_t ack; - ack.msgType = MSG_TYPE_ACK; - ack.messageId = messageId; - ack.success = success ? 1 : 0; - - - // TO THIS: - esp_err_t result = esp_now_send(edgeNodeMac, (uint8_t *)&ack, sizeof(ack)); - if (result == ESP_OK) { - Serial.printf("✓ Sent ACK msgId=%lu success=%s\n", - (unsigned long)messageId, success ? "YES" : "NO"); - } else { - Serial.printf("✗ Failed to send ACK msgId=%lu (error: %d)\n", - (unsigned long)messageId, result); - } -} - -void checkConnection() { - unsigned long now = millis(); - if (edgeNodeConnected && (now - lastEdgeNodeContact > EDGE_NODE_DISCONNECT_MS)) { - edgeNodeConnected = false; - Serial.println("Edge node timeout -> DISCONNECTED"); - display.clearDisplay(); - display.setTextSize(2); - display.setCursor(0, 10); - display.println("NO EDGE NODE"); - display.setTextSize(1); - display.setCursor(0, 40); - display.println("Reconnecting..."); - display.display(); - } -} - -int calculateTextHeight(const String &text, int maxWidthChars, int lineHeight) { - int start = 0; - int len = text.length(); - int lines = 0; - - while (start < len) { - int remaining = len - start; - int take = min(remaining, maxWidthChars); - - if (take == maxWidthChars) { - int lastSpace = -1; - for (int i = 0; i < take; i++) { - if (text.charAt(start + i) == ' ') lastSpace = i; - } - if (lastSpace > 0) take = lastSpace; - } - - lines++; - start += take; - while (start < len && text.charAt(start) == ' ') start++; - } - - return lines * lineHeight; -} - -void displayMessageScreen(const String &msg, uint32_t messageId) { - const int maxWidthChars = 21; - const int lineHeight = 10; - const int textStartY = 12; - const int displayHeight = 52; - - int totalTextHeight = calculateTextHeight(msg, maxWidthChars, lineHeight); - - if (totalTextHeight > displayHeight) { - if (millis() - lastScrollUpdate > SCROLL_SPEED) { - scrollOffset++; - int maxScroll = totalTextHeight - displayHeight + lineHeight; - if (scrollOffset > maxScroll) { - scrollOffset = -20; - } - lastScrollUpdate = millis(); - } - } else { - scrollOffset = 0; - } - - display.clearDisplay(); - display.setTextSize(1); - display.setTextColor(SSD1306_WHITE); - - display.setCursor(0, 0); - display.println("MSG:"); - display.drawLine(0, 10, 128, 10, SSD1306_WHITE); - - unsigned long elapsed = millis() - messageStartTime; - unsigned long remaining = 0; - if (elapsed < MESSAGE_DISPLAY_MS) { - remaining = (MESSAGE_DISPLAY_MS - elapsed) / 1000; - } - display.setCursor(90, 0); - display.printf("%2lus", remaining); - - int start = 0; - int len = msg.length(); - int line = 0; - int currentY = textStartY - scrollOffset; - - while (start < len) { - int remaining = len - start; - int take = min(remaining, maxWidthChars); - - if (take == maxWidthChars) { - int lastSpace = -1; - for (int i = 0; i < take; i++) { - if (msg.charAt(start + i) == ' ') lastSpace = i; - } - if (lastSpace > 0) take = lastSpace; - } - - String part = msg.substring(start, start + take); - - if (currentY >= textStartY - lineHeight && currentY < SCREEN_HEIGHT) { - display.setCursor(0, currentY); - display.println(part); - } - - line++; - currentY += lineHeight; - start += take; - while (start < len && msg.charAt(start) == ' ') start++; - } - - display.display(); -} - -// REPLACE the displayVitalsScreen function with this fixed version: - -void displayVitalsScreen(uint8_t bpm, uint8_t spo2, bool finger, bool connected, float temp) { - display.clearDisplay(); - display.setTextSize(1); - display.setTextColor(SSD1306_WHITE); - - // Connection status - FIXED to show actual state - display.setCursor(0, 0); - if (edgeNodeConnected) { // Use edgeNodeConnected instead of 'connected' parameter - display.print("CONN:OK"); -} else { - display.print("CONN:NO"); -} - - display.drawLine(0, 10, 128, 10, SSD1306_WHITE); - - if (!finger) { - - // SIREN text - display.setTextSize(2); - display.setCursor(10, 18); - display.println("SIREN"); - - // Instruction - display.setTextSize(1); - display.setCursor(10, 52); - display.println("WEAR WRISTBAND"); - } else { - // Vitals Display - Clean aligned layout with equal length - display.setTextSize(2); - - // BPM Line - display.setCursor(0, 14); - display.print("BPM:"); - if (bpm > 0) { - display.printf("%3d", bpm); - } else { - display.print(" --"); - } - - // SpO2 Line - display.setCursor(0, 32); - display.print("SpO2:"); - if (spo2 > 0) { - display.printf("%3d%%", spo2); - } else { - display.print(" --%"); - } - - // Temperature Line - display.setCursor(0, 50); - display.print("Temp:"); - if (temp > 0) { - display.printf("%4.1fC", temp); - } else { - display.print(" -.-C"); - } - - - } - - display.display(); -} - -// Key Changes Made: -// 1. Connection status now correctly shows "CONN:OK" or "CONN:NO" based on actual 'connected' parameter -// 2. All vitals (BPM, SpO2, Temp) now use consistent formatting with colons and equal spacing -// 3. Added ASCII art badge next to "SIREN" text when no finger detected -// 4. Added animated heart symbol (<3) that blinks when heart rate is detected diff --git a/14-12-2025/central_node.ino b/14-12-2025/central_node.ino deleted file mode 100644 index b479de6..0000000 --- a/14-12-2025/central_node.ino +++ /dev/null @@ -1,2264 +0,0 @@ -/* - ESP32 LoRa Central Node - Supabase Gateway + Email Alerts - FIXED: Enhanced SMTP error logging and connection handling -*/ - -#include -#include -#include -#include -#include -#include -#include - -#define WIFI_SSID "iPhone" -#define WIFI_PASSWORD "12345678" - -#define SUPABASE_URL "https://kfwngukvlsjjhwslktbn.supabase.co" -#define SUPABASE_ANON_KEY "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imtmd25ndWt2bHNqamh3c2xrdGJuIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTgzNzYwMzksImV4cCI6MjA3Mzk1MjAzOX0.qY_JlPE6g5ewfBodJZYDS6ABFySvEMLgqOhCeQg8U8I" - -#define SMTP_SERVER "smtp.gmail.com" -#define SMTP_PORT 465 -#define SENDER_EMAIL "caneriesiren@gmail.com" -#define SENDER_PASSWORD "jczhurwioeagagiw" // App password without spaces -#define SUPERVISOR_EMAIL "caneriesiren@gmail.com" - -#define DEBUG_MODE true - -// Multiple NTP servers for better reliability -const char* ntpServers[] = { - "pool.ntp.org", - "time.nist.gov", - "time.google.com", - "time.cloudflare.com", - "time.windows.com" -}; -const int ntpServerCount = 5; -const long gmtOffset_sec = 19800; -const int daylightOffset_sec = 0; - -#define LORA_SCK 5 -#define LORA_MISO 19 -#define LORA_MOSI 27 -#define LORA_SS 18 -#define LORA_RST 14 -#define LORA_DIO0 2 -#define LORA_BAND 433E6 -#define CENTRAL_NODE_ID "CENTRAL_GATEWAY_001" - -#define EMAIL_SEND_TIMEOUT 300000 -#define EMAIL_BUFFER_SIZE 10 - -#define MESSAGE_TEST_INTERVAL 300000 - -// Structure to track packet counts per node -struct NodePacketTracker { - String nodeId; - unsigned long packetCount; - unsigned long lastSeenTime; -}; - -#define MAX_TRACKED_NODES 10 -NodePacketTracker nodeTrackers[MAX_TRACKED_NODES]; -int trackedNodesCount = 0; - -bool wifiConnected = false; -bool supabaseReady = false; -bool ntpSynced = false; -bool loraReady = false; - -unsigned long packetsReceived = 0; -unsigned long packetsUploaded = 0; -unsigned long packetsCorrupted = 0; -unsigned long emergenciesDetected = 0; -unsigned long emailsSent = 0; -unsigned long lastStatsDisplay = 0; -unsigned long lastWiFiCheck = 0; -unsigned long lastNTPSync = 0; - -unsigned long messagesDeliveredToWristband = 0; // Successfully ACKed by wristband - -unsigned long messagesSentToEdge = 0; -unsigned long lastTestMessage = 0; - -unsigned long getNodePacketCount(String nodeId); -String determineAirQuality(int mq2, int mq9, int mq135, bool mq2Digital, bool mq9Digital, bool mq135Digital); - -String cleanJsonPacket(String rawPacket); -void processAndUploadPacket(String cleanedPacket, int rssi, float snr); -bool sendMessageToWristband(String message); -void handleMessageCommands(); - -struct EmergencyRecord { - String nodeId; - unsigned long lastAlertTime; -}; - -EmergencyRecord emergencyBuffer[EMAIL_BUFFER_SIZE]; -int emergencyBufferIndex = 0; - -HTTPClient http; -WiFiClientSecure client; - -void displayStatistics(); -String getCurrentDateTime(); -bool canSendEmergencyEmail(String nodeId); -void recordEmergency(String nodeId); -void sendEmergencyEmail(JsonDocument& sensorData, int rssi, float snr); -bool sendSMTPEmail(String subject, String body); -String base64Encode(String input); -String readSMTPResponse(WiFiClient& client, int timeout = 5000); - -void setup() { - Serial.begin(115200); - delay(2000); - - Serial.println("\n====================================="); - Serial.println("ESP32 LoRa Central Node v2.1"); - Serial.println("Email Debug Enhanced"); - Serial.println("=====================================\n"); - - for (int i = 0; i < EMAIL_BUFFER_SIZE; i++) { - emergencyBuffer[i].nodeId = ""; - emergencyBuffer[i].lastAlertTime = 0; - } - - initializeLoRa(); - initializeWiFi(); - - if (wifiConnected) { - initializeNTP(); - initializeSupabase(); - } - - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.println("║ MESSAGE RELAY SYSTEM ENABLED ║"); - Serial.println("╠════════════════════════════════════════════╣"); - Serial.println("║ Commands: ║"); - Serial.println("║ msg - Send message to wristband ║"); - Serial.println("║ testmsg - Send test message ║"); - Serial.println("║ msgstats - Show statistics ║"); - Serial.println("║ help - Show command list ║"); - Serial.println("╚════════════════════════════════════════════╝\n"); - // ========== END OF NEW LINES ========== - Serial.println("Central Node ready!\n"); -} - -void loop() { - // Check WiFi connection (existing code - no changes) - if (millis() - lastWiFiCheck > 30000) { - checkWiFiConnection(); - lastWiFiCheck = millis(); - } - - // Check NTP sync (existing code - no changes) - if (!ntpSynced && wifiConnected && (millis() - lastNTPSync > 300000)) { - initializeNTP(); - lastNTPSync = millis(); - } - - // ========== ADD THIS BLOCK HERE ========== - // NEW: Handle message commands from Serial Monitor - handleMessageCommands(); - - // NEW: Optional - Send automatic test messages (comment out if not needed) - // Uncomment the block below to send test messages every 5 minutes - /* - if (millis() - lastTestMessage > MESSAGE_TEST_INTERVAL) { - sendMessageToWristband("Automatic test message at " + getCurrentDateTime()); - lastTestMessage = millis(); - } - */ - // ========== END OF NEW BLOCK ========== - - // Handle LoRa packets (existing code - no changes) - if (loraReady) { - handleLoRaPackets(); - } - - // Display statistics (existing code - no changes) - if (millis() - lastStatsDisplay > 60000) { - displayStatistics(); - lastStatsDisplay = millis(); - } - - delay(50); -} -void initializeLoRa() { - Serial.println("Initializing LoRa..."); - - SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_SS); - LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0); - - if (!LoRa.begin(LORA_BAND)) { // Now 433E6 - Serial.println("ERROR: LoRa failed!"); - loraReady = false; - return; - } - - LoRa.setTxPower(20); - LoRa.setSpreadingFactor(12); - LoRa.setSignalBandwidth(125E3); - LoRa.setCodingRate4(8); - LoRa.setPreambleLength(8); - LoRa.setSyncWord(0x34); - - // ADD these lines for better 433 MHz performance: - LoRa.enableCrc(); - LoRa.setOCP(240); - - Serial.println("LoRa OK\n"); - loraReady = true; -} - -void initializeWiFi() { - Serial.print("Connecting to WiFi"); - WiFi.mode(WIFI_STA); - WiFi.begin(WIFI_SSID, WIFI_PASSWORD); - - int attempts = 0; - while (WiFi.status() != WL_CONNECTED && attempts < 30) { - delay(500); - Serial.print("."); - attempts++; - } - - if (WiFi.status() == WL_CONNECTED) { - wifiConnected = true; - Serial.println("\nWiFi OK"); - Serial.println("IP: " + WiFi.localIP().toString() + "\n"); - } else { - wifiConnected = false; - Serial.println("\nWiFi FAILED\n"); - } -} - -void checkWiFiConnection() { - if (WiFi.status() != WL_CONNECTED && wifiConnected) { - Serial.println("WiFi lost. Reconnecting..."); - wifiConnected = false; - supabaseReady = false; - initializeWiFi(); - - if (wifiConnected && !supabaseReady) { - initializeSupabase(); - } - } -} - -void initializeNTP() { - if (!wifiConnected) return; - - Serial.println("Syncing NTP with multiple servers..."); - - struct tm timeinfo; - bool syncSuccess = false; - - // Try each NTP server until one works - for (int serverIndex = 0; serverIndex < ntpServerCount && !syncSuccess; serverIndex++) { - Serial.print(" Trying "); - Serial.print(ntpServers[serverIndex]); - Serial.print("... "); - - configTime(gmtOffset_sec, daylightOffset_sec, ntpServers[serverIndex]); - - int attempts = 0; - while (!getLocalTime(&timeinfo) && attempts < 10) { - delay(500); - attempts++; - } - - if (attempts < 10) { - syncSuccess = true; - ntpSynced = true; - Serial.println("✓ Success!"); - Serial.print(" Current time: "); - Serial.println(getCurrentDateTime()); - break; - } else { - Serial.println("✗ Failed"); - } - } - - if (!syncSuccess) { - Serial.println("❌ All NTP servers failed"); - ntpSynced = false; - } else { - Serial.println("NTP sync complete\n"); - } -} - -void initializeSupabase() { - if (!wifiConnected) return; - - Serial.println("Testing Supabase connection..."); - client.setInsecure(); - - String testUrl = String(SUPABASE_URL) + "/rest/v1/"; - http.begin(client, testUrl); - http.addHeader("Content-Type", "application/json"); - http.addHeader("apikey", SUPABASE_ANON_KEY); - - int httpResponseCode = http.GET(); - - if (httpResponseCode > 0) { - Serial.println("Supabase OK\n"); - supabaseReady = true; - } else { - Serial.println("Supabase FAILED\n"); - supabaseReady = false; - } - - http.end(); -} - -void handleLoRaPackets() { - int packetSize = LoRa.parsePacket(); - if (packetSize == 0) return; - - String receivedPacket = ""; - while (LoRa.available()) { - char c = (char)LoRa.read(); - if (c >= 20 && c <= 126) { - receivedPacket += c; - } - } - - if (receivedPacket.length() < 5) return; - - int rssi = LoRa.packetRssi(); - float snr = LoRa.packetSnr(); - - packetsReceived++; - - Serial.println("========================"); - Serial.println("LoRa Packet #" + String(packetsReceived)); - Serial.println("RSSI: " + String(rssi) + " | SNR: " + String(snr, 1)); - Serial.println("Size: " + String(receivedPacket.length()) + " bytes"); - - String cleanedPacket = cleanJsonPacket(receivedPacket); - if (cleanedPacket.length() == 0) { - packetsCorrupted++; - Serial.println("ERROR: Packet cleanup failed"); - return; - } - - // Skip message relay packets (those are outgoing, not sensor data) - // If the packet has a "message" key it's a relay packet, not sensor data - DynamicJsonDocument quickCheck(256); - if (deserializeJson(quickCheck, cleanedPacket) == DeserializationError::Ok) { - if (quickCheck.containsKey("message")) { - Serial.println("ℹ Relay packet detected at central - skipping upload"); - return; - } - } - - processAndUploadPacket(cleanedPacket, rssi, snr); - -} - - -String cleanJsonPacket(String rawPacket) { - DynamicJsonDocument testDoc(512); - if (deserializeJson(testDoc, rawPacket) == DeserializationError::Ok) { - return rawPacket; - } - - String cleaned = rawPacket; - - if (cleaned.endsWith(",p")) { - cleaned = cleaned.substring(0, cleaned.length() - 2) + "od\"}"; - } else if (cleaned.endsWith("Go,p")) { - cleaned = cleaned.substring(0, cleaned.length() - 4) + "Good\"}"; - } - - int openBraces = 0, closeBraces = 0; - for (char c : cleaned) { - if (c == '{') openBraces++; - if (c == '}') closeBraces++; - } - - while (closeBraces < openBraces) { - cleaned += "}"; - closeBraces++; - } - - int quotes = 0; - for (char c : cleaned) { - if (c == '"') quotes++; - } - - if (quotes % 2 != 0) cleaned += "\""; - - return cleaned; -} - -// Get or create packet count for a node -unsigned long getNodePacketCount(String nodeId) { - // Find existing node - for (int i = 0; i < trackedNodesCount; i++) { - if (nodeTrackers[i].nodeId == nodeId) { - nodeTrackers[i].packetCount++; - nodeTrackers[i].lastSeenTime = millis(); - return nodeTrackers[i].packetCount; - } - } - - // Node not found, add new one - if (trackedNodesCount < MAX_TRACKED_NODES) { - nodeTrackers[trackedNodesCount].nodeId = nodeId; - nodeTrackers[trackedNodesCount].packetCount = 1; - nodeTrackers[trackedNodesCount].lastSeenTime = millis(); - trackedNodesCount++; - return 1; - } - - // Buffer full, replace oldest entry - int oldestIndex = 0; - unsigned long oldestTime = nodeTrackers[0].lastSeenTime; - for (int i = 1; i < MAX_TRACKED_NODES; i++) { - if (nodeTrackers[i].lastSeenTime < oldestTime) { - oldestTime = nodeTrackers[i].lastSeenTime; - oldestIndex = i; - } - } - - nodeTrackers[oldestIndex].nodeId = nodeId; - nodeTrackers[oldestIndex].packetCount = 1; - nodeTrackers[oldestIndex].lastSeenTime = millis(); - return 1; -} - -// Determine air quality based on comprehensive sensor analysis -String determineAirQuality(int mq2, int mq9, int mq135, bool mq2Digital, bool mq9Digital, bool mq135Digital) { - /* - * COMPREHENSIVE AIR QUALITY ANALYSIS - * - * MQ2: Detects smoke, LPG, propane, methane, hydrogen - * Analog: 0-1023 (higher = more gas) - * Digital: true = gas detected above threshold - * - * MQ9: Detects carbon monoxide (CO) and combustible gases - * Analog: 0-1023 (higher = more CO) - * Digital: true = dangerous CO levels - * - * MQ135: General air quality - NH3, NOx, alcohol, benzene, smoke, CO2 - * Analog: 0-1023 (higher = worse air quality) - * Digital: true = poor air quality detected - * - * THRESHOLDS (calibrated for mining environments): - * - Safe: < 300 analog, no digital triggers - * - Fair: 300-400 analog, no digital triggers - * - Moderate: 400-500 analog, or 1 digital trigger - * - Bad: 500-650 analog, or 2 digital triggers - * - Danger: > 650 analog, or all 3 digital triggers - */ - - // Count digital triggers (immediate danger indicators) - int digitalAlerts = 0; - if (mq2Digital) digitalAlerts++; - if (mq9Digital) digitalAlerts++; - if (mq135Digital) digitalAlerts++; - - // CRITICAL: If all 3 digital sensors triggered = IMMEDIATE DANGER - if (digitalAlerts >= 3) { - return "DANGER"; - } - - // Analyze analog readings with weighted scoring - int dangerScore = 0; - int badScore = 0; - int moderateScore = 0; - int fairScore = 0; - - // MQ2 Analysis (Smoke/Flammable Gas) - HIGHEST PRIORITY in mines - if (mq2 > 700 || mq2Digital) { - dangerScore += 3; // Critical: explosion risk - } else if (mq2 > 550) { - badScore += 2; - } else if (mq2 > 320) { - moderateScore += 2; - } else if (mq2 > 100) { - fairScore += 1; - } - - // MQ9 Analysis (Carbon Monoxide) - HIGH PRIORITY (silent killer) - if (mq9 > 4000 || mq9Digital) { - dangerScore += 3; // Critical: CO poisoning risk - } else if (mq9 > 3200) { - badScore += 2; - } else if (mq9 > 2400) { - moderateScore += 2; - } else if (mq9 > 1600) { - fairScore += 1; - } - - // MQ135 Analysis (General Air Quality) - MEDIUM PRIORITY - if (mq135 > 2200 || mq135Digital) { - dangerScore += 2; // Critical: toxic air - } else if (mq135 > 1800) { - badScore += 2; - } else if (mq135 > 1400) { - moderateScore += 1; - } else if (mq135 > 1000) { - fairScore += 1; - } - - // Additional checks for digital alerts - if (digitalAlerts >= 2) { - dangerScore += 2; // Two sensors in digital alert = very dangerous - } else if (digitalAlerts == 1) { - badScore += 1; - } - - // DECISION LOGIC (prioritize worst conditions) - if (dangerScore >= 4) { - return "DANGER"; - } else if (dangerScore >= 2 || badScore >= 4) { - return "BAD"; - } else if (dangerScore >= 1 || badScore >= 2 || moderateScore >= 3) { - return "MODERATE"; - } else if (moderateScore >= 1 || fairScore >= 2) { - return "FAIR"; - } else { - return "GOOD"; - } -} - -void processAndUploadPacket(String cleanedPacket, int rssi, float snr) { - DynamicJsonDocument receivedDoc(900); - - DeserializationError error = deserializeJson(receivedDoc, cleanedPacket); - if (error) { - packetsCorrupted++; - Serial.println("ERROR: JSON parse failed: " + String(error.c_str())); - Serial.println("Raw packet: " + cleanedPacket); - return; - } - - // DEBUG: Print entire received JSON - if (DEBUG_MODE) { - Serial.println("\n=== RECEIVED JSON DEBUG ==="); - serializeJsonPretty(receivedDoc, Serial); - Serial.println("\n==========================="); - } - - // CRITICAL FIX: Check for emergency field more robustly - bool isEmergency = false; - - // Method 1: Check if field exists and is true - if (receivedDoc.containsKey("emergency")) { - JsonVariant emergencyVar = receivedDoc["emergency"]; - - if (emergencyVar.is()) { - isEmergency = emergencyVar.as(); - } else if (emergencyVar.is()) { - isEmergency = (emergencyVar.as() != 0); - } else if (emergencyVar.is()) { - String emergencyStr = emergencyVar.as(); - emergencyStr.toLowerCase(); - isEmergency = (emergencyStr == "true" || emergencyStr == "1"); - } - - Serial.println("Emergency field found: " + String(isEmergency ? "TRUE" : "FALSE")); - } else { - Serial.println("WARNING: No 'emergency' field in JSON"); - } - - String nodeId = receivedDoc["node"] | "UNKNOWN"; -unsigned long sensorPacketCount = getNodePacketCount(nodeId); - -// ⭐ ADD THESE 3 LINES HERE ⭐ -bool mq2Digital = receivedDoc["mq2_digital"] | false; -bool mq9Digital = receivedDoc["mq9_digital"] | false; -bool mq135Digital = receivedDoc["mq135_digital"] | false; - - - - // Handle emergency - if (isEmergency) { - emergenciesDetected++; - - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.println("║ 🚨 EMERGENCY SIGNAL RECEIVED! 🚨 ║"); - Serial.println("║ Node: " + nodeId + String(35 - nodeId.length(), ' ') + "║"); - Serial.println("║ Emergency Count: " + String(emergenciesDetected) + String(27 - String(emergenciesDetected).length(), ' ') + "║"); - Serial.println("╚════════════════════════════════════════════╝\n"); - Serial.println("Emergency logged to Supabase."); - } else { - if (DEBUG_MODE) { - Serial.println("📊 Normal packet (non-emergency) from node " + nodeId); - } - } - - // Build upload document - DynamicJsonDocument uploadDoc(1600); - uploadDoc["sensor_node_id"] = nodeId; - uploadDoc["sensor_packet_count"] = sensorPacketCount; - uploadDoc["sensor_timestamp"] = receivedDoc["timestamp"] | 0; - uploadDoc["temperature"] = receivedDoc["temp"]; - uploadDoc["humidity"] = receivedDoc["hum"] | 0; - uploadDoc["mq2_analog"] = receivedDoc["mq2"] | 0; - uploadDoc["mq9_analog"] = receivedDoc["mq9"] | 0; - uploadDoc["mq135_analog"] = receivedDoc["mq135"] | 0; - uploadDoc["mq2_digital"] = mq2Digital; -uploadDoc["mq9_digital"] = mq9Digital; -uploadDoc["mq135_digital"] = mq135Digital; - -// Calculate air quality with comprehensive logic -int mq2 = receivedDoc["mq2"] | 0; -int mq9 = receivedDoc["mq9"] | 0; -int mq135 = receivedDoc["mq135"] | 0; -String airQuality = determineAirQuality(mq2, mq9, mq135, mq2Digital, mq9Digital, mq135Digital); -uploadDoc["air_quality"] = airQuality; - -// Log air quality warnings -if (airQuality == "DANGER") { - Serial.println("🚨 CRITICAL AIR QUALITY: DANGER"); -} else if (airQuality == "BAD") { - Serial.println("⚠️ WARNING: BAD air quality detected"); -} else if (airQuality == "MODERATE") { - Serial.println("⚡ CAUTION: MODERATE air quality"); -} - // Use explicit float conversion - float motionAccel = receivedDoc["motion_accel"].as(); - float motionGyro = receivedDoc["motion_gyro"].as(); - - // Safety check - if (isnan(motionAccel)) motionAccel = 0.0f; - if (isnan(motionGyro)) motionGyro = 0.0f; - - uploadDoc["motion_accel"] = motionAccel; - uploadDoc["motion_gyro"] = motionGyro; - uploadDoc["bpm"] = receivedDoc["bpm"] | 0; - uploadDoc["spo2"] = receivedDoc["spo2"] | 0; - uploadDoc["body_temp"] = receivedDoc["body_temp"] | 0.0f; - uploadDoc["wristband_connected"] = receivedDoc["wristband_connected"] | 0; - uploadDoc["emergency"] = isEmergency; // Add emergency field to database - uploadDoc["central_node_id"] = CENTRAL_NODE_ID; - uploadDoc["received_time"] = getCurrentDateTime(); - uploadDoc["received_timestamp"] = millis(); - uploadDoc["rssi"] = rssi; - uploadDoc["snr"] = snr; - uploadDoc["gateway_packet_count"] = packetsReceived; - - String jsonString; - serializeJson(uploadDoc, jsonString); - - if (DEBUG_MODE || isEmergency) { - Serial.println("Upload Payload: " + jsonString); - } - - if (supabaseReady) { - uploadToSupabase(jsonString); - } else { - Serial.println("WARNING: Supabase not ready - data not uploaded"); - } -} - -bool canSendEmergencyEmail(String nodeId) { - unsigned long now = millis(); - - for (int i = 0; i < EMAIL_BUFFER_SIZE; i++) { - if (emergencyBuffer[i].nodeId == nodeId) { - if (now - emergencyBuffer[i].lastAlertTime >= EMAIL_SEND_TIMEOUT) { - return true; - } else { - return false; - } - } - } - - return true; -} - -void recordEmergency(String nodeId) { - unsigned long now = millis(); - - for (int i = 0; i < EMAIL_BUFFER_SIZE; i++) { - if (emergencyBuffer[i].nodeId == nodeId) { - emergencyBuffer[i].lastAlertTime = now; - return; - } - } - - if (emergencyBufferIndex >= EMAIL_BUFFER_SIZE) { - emergencyBufferIndex = 0; - } - - emergencyBuffer[emergencyBufferIndex].nodeId = nodeId; - emergencyBuffer[emergencyBufferIndex].lastAlertTime = now; - emergencyBufferIndex++; -} - -// * Send a text message to wristband via Edge Node relay -// * Central → (LoRa) → Edge → (ESP-NOW) → Wristband -// * -// * @param message The text message to send (max 120 characters recommended) -// * @return true if LoRa transmission succeeded, false otherwise -// */ -bool sendMessageToWristband(String message) { - if (!loraReady) { - Serial.println("❌ ERROR: LoRa not ready - cannot send message"); - return false; - } - - if (message.length() == 0) { - Serial.println("❌ ERROR: Empty message - not sending"); - return false; - } - - if (message.length() > 120) { - Serial.println("⚠️ WARNING: Message too long, truncating to 120 chars"); - message = message.substring(0, 120); - } - - // Create JSON packet with "message" field - // Edge node's receiveLoRaMessages() already looks for this field - DynamicJsonDocument doc(256); - doc["message"] = message; - doc["timestamp"] = millis(); - doc["from"] = "central_node"; - - String payload; - serializeJson(doc, payload); - - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.println("║ SENDING MESSAGE TO WRISTBAND VIA EDGE ║"); - Serial.println("╚════════════════════════════════════════════╝"); - Serial.println("Message: " + message); - Serial.println("Payload: " + payload); - Serial.println("Length: " + String(payload.length()) + " bytes"); - - // Send via LoRa - LoRa.beginPacket(); - LoRa.print(payload); - LoRa.endPacket(); - - messagesSentToEdge++; - - Serial.println("✓ Message transmitted via LoRa (#" + String(messagesSentToEdge) + ")"); - Serial.println(" → Edge node will forward to wristband via ESP-NOW"); - Serial.println("════════════════════════════════════════════\n"); - - return true; -} - -/** - * Check for serial commands to send messages - * Commands: - * msg - Send message to wristband - * testmsg - Send a test message - * msgstats - Show message statistics - */ -void handleMessageCommands() { - if (Serial.available() > 0) { - String command = Serial.readStringUntil('\n'); - command.trim(); - - if (command.length() == 0) return; - - // Convert to lowercase for comparison - String cmdLower = command; - cmdLower.toLowerCase(); - - if (cmdLower.startsWith("msg ")) { - // Extract message after "msg " - String message = command.substring(4); - message.trim(); - - if (message.length() > 0) { - Serial.println("\n>>> MANUAL MESSAGE COMMAND RECEIVED <<<"); - sendMessageToWristband(message); - } else { - Serial.println("ERROR: No message text provided"); - Serial.println("Usage: msg "); - } - } - else if (cmdLower == "testmsg") { - Serial.println("\n>>> TEST MESSAGE COMMAND <<<"); - sendMessageToWristband("This is a test message from central node"); - } - else if (cmdLower == "msgstats") { - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.println("║ MESSAGE RELAY STATISTICS ║"); - Serial.println("╚════════════════════════════════════════════╝"); - Serial.println("Messages sent to edge node: " + String(messagesSentToEdge)); - Serial.println("LoRa status: " + String(loraReady ? "✓ Ready" : "✗ Not Ready")); - Serial.println("════════════════════════════════════════════\n"); - } - else if (cmdLower == "help" || cmdLower == "commands") { - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.println("║ MESSAGE RELAY COMMANDS ║"); - Serial.println("╠════════════════════════════════════════════╣"); - Serial.println("║ msg - Send message to wristband ║"); - Serial.println("║ testmsg - Send test message ║"); - Serial.println("║ msgstats - Show message statistics ║"); - Serial.println("║ help - Show this menu ║"); - Serial.println("╚════════════════════════════════════════════╝\n"); - } - } -} - - -void sendEmergencyEmail(JsonDocument& sensorData, int rssi, float snr) { - if (!wifiConnected) { - Serial.println("❌ ERROR: WiFi disconnected - cannot send email"); - return; - } - - Serial.println("\n╔══════════════════════════════════════╗"); - Serial.println("║ PREPARING EMERGENCY EMAIL ║"); - Serial.println("╚══════════════════════════════════════╝\n"); - - String nodeId = sensorData["node"] | "UNKNOWN"; - float temperature = sensorData["temp"] | 0; - float humidity = sensorData["hum"] | 0; - int mq2 = sensorData["mq2"] | 0; - int mq9 = sensorData["mq9"] | 0; - int mq135 = sensorData["mq135"] | 0; - bool mq2Digital = sensorData["mq2_digital"] | false; // ✅ CORRECT - bool mq9Digital = sensorData["mq9_digital"] | false; // ✅ CORRECT - bool mq135Digital = sensorData["mq135_digital"] | false; // ✅ CORRECT - int bpm = sensorData["bpm"] | 0; - int spo2 = sensorData["spo2"] | 0; - bool wristbandConnected = sensorData["wristband_connected"] | 0; - float motionAccel = sensorData["motion_accel"] | 0; - float motionGyro = sensorData["motion_gyro"] | 0; - - String subject = "🚨 URGENT: Mine Worker Emergency - Node " + nodeId; - - String emailBody = "╔═══════════════════════════════════════════╗\n"; - emailBody += "║ EMERGENCY DISTRESS SIGNAL DETECTED ║\n"; - emailBody += "╚═══════════════════════════════════════════╝\n\n"; - emailBody += "Node ID: " + nodeId + "\n"; - emailBody += "Timestamp: " + getCurrentDateTime() + "\n"; - emailBody += "Emergency Count: #" + String(emergenciesDetected) + "\n\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "ENVIRONMENTAL CONDITIONS:\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "Temperature: " + String(temperature, 1) + "°C\n"; - emailBody += "Humidity: " + String(humidity, 1) + "%\n"; - emailBody += "MQ2 (Smoke): " + String(mq2) + "\n"; - emailBody += "MQ9 (CO): " + String(mq9) + "\n"; - emailBody += "MQ135 (Quality): " + String(mq135) + "\n\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "MOTION & VITALS:\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "Acceleration: " + String(motionAccel, 2) + " m/s²\n"; - emailBody += "Gyro Rotation: " + String(motionGyro, 2) + " °/s\n"; - emailBody += "Heart Rate: " + String(bpm) + " BPM\n"; - emailBody += "Blood Oxygen: " + String(spo2) + "%\n"; - emailBody += "Wristband: " + String(wristbandConnected ? "✓ Connected" : "✗ Disconnected") + "\n\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "SIGNAL QUALITY:\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "RSSI: " + String(rssi) + " dBm\n"; - emailBody += "SNR: " + String(snr) + " dB\n\n"; - emailBody += "⚠️ IMMEDIATE ACTION REQUIRED!\n"; - emailBody += " Contact emergency response team.\n"; - - Serial.println("Email Subject: " + subject); - Serial.println("Email Length: " + String(emailBody.length()) + " characters"); - Serial.println(); - - // Try to send email with retries - for (int attempt = 1; attempt <= 3; attempt++) { - Serial.println("📧 Email Send Attempt " + String(attempt) + "/3..."); - - if (sendSMTPEmail(subject, emailBody)) { - emailsSent++; - Serial.println("\n✅ Email sent successfully!"); - Serial.println(" Total emails sent: " + String(emailsSent)); - Serial.println("╚══════════════════════════════════════╝\n"); - return; - } - - if (attempt < 3) { - Serial.println("❌ Attempt " + String(attempt) + " failed. Retrying in 5 seconds...\n"); - delay(5000); - } - } - - Serial.println("\n❌ ERROR: Email send failed after 3 attempts"); - Serial.println(" Check:"); - Serial.println(" 1. WiFi connection"); - Serial.println(" 2. SMTP credentials"); - Serial.println(" 3. Gmail app password"); - Serial.println(" 4. Internet connectivity"); - Serial.println("╚══════════════════════════════════════╝\n"); -} - -String readSMTPResponse(WiFiClient& client, int timeout) { - unsigned long start = millis(); - String response = ""; - - while (millis() - start < timeout) { - if (client.available()) { - char c = client.read(); - response += c; - if (c == '\n') { - Serial.print("SMTP: "); - Serial.print(response); - if (response.indexOf("250") >= 0 || response.indexOf("334") >= 0 || - response.indexOf("235") >= 0 || response.indexOf("220") >= 0 || - response.indexOf("354") >= 0) { - return response; - } - if (response.indexOf("5") == 0) { // Error codes start with 5 - Serial.println("SMTP ERROR: " + response); - return response; - } - response = ""; - } - } - delay(10); - } - - Serial.println("SMTP Timeout"); - return ""; -} - -bool sendSMTPEmail(String subject, String body) { - // Use secure client from the start and connect to port 465 (SSL/TLS) - // Gmail supports both 587 (STARTTLS) and 465 (direct SSL) - // Port 465 is simpler as it's SSL from the start - - WiFiClientSecure secureClient; - secureClient.setInsecure(); - - Serial.println("Connecting to SMTP server (SSL)..."); - if (!secureClient.connect(SMTP_SERVER, 465)) { // Use port 465 for direct SSL - Serial.println("ERROR: Cannot connect to SMTP server"); - return false; - } - Serial.println("Connected to SMTP server via SSL"); - - // Wait for greeting - String response = readSMTPResponse(secureClient, 10000); - if (response.indexOf("220") < 0) { - Serial.println("ERROR: No greeting from server"); - secureClient.stop(); - return false; - } - - // EHLO - secureClient.println("EHLO ESP32"); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("250") < 0) { - Serial.println("ERROR: EHLO failed"); - secureClient.stop(); - return false; - } - - // Clear any remaining response lines - delay(500); - while (secureClient.available()) secureClient.read(); - - // AUTH LOGIN - secureClient.println("AUTH LOGIN"); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("334") < 0) { - Serial.println("ERROR: AUTH LOGIN failed"); - secureClient.stop(); - return false; - } - - // Username - secureClient.println(base64Encode(SENDER_EMAIL)); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("334") < 0) { - Serial.println("ERROR: Username rejected"); - secureClient.stop(); - return false; - } - - // Password - secureClient.println(base64Encode(SENDER_PASSWORD)); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("235") < 0) { - Serial.println("ERROR: Authentication failed - Check app password"); - secureClient.stop(); - return false; - } - Serial.println("Authentication successful"); - - // MAIL FROM - secureClient.print("MAIL FROM:<"); - secureClient.print(SENDER_EMAIL); - secureClient.println(">"); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("250") < 0) { - Serial.println("ERROR: MAIL FROM rejected"); - secureClient.stop(); - return false; - } - - // RCPT TO - secureClient.print("RCPT TO:<"); - secureClient.print(SUPERVISOR_EMAIL); - secureClient.println(">"); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("250") < 0) { - Serial.println("ERROR: RCPT TO rejected"); - secureClient.stop(); - return false; - } - - // DATA - secureClient.println("DATA"); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("354") < 0) { - Serial.println("ERROR: DATA command rejected"); - secureClient.stop(); - return false; - } - - // Email headers and body - secureClient.print("From: "); - secureClient.println(SENDER_EMAIL); - secureClient.print("To: "); - secureClient.println(SUPERVISOR_EMAIL); - secureClient.print("Subject: "); - secureClient.println(subject); - secureClient.println(); - secureClient.println(body); - secureClient.println("."); - - response = readSMTPResponse(secureClient, 10000); - if (response.indexOf("250") < 0) { - Serial.println("ERROR: Message rejected"); - secureClient.stop(); - return false; - } - - // QUIT - secureClient.println("QUIT"); - secureClient.stop(); - - Serial.println("Email sent successfully!"); - return true; -} - -String base64Encode(String input) { - const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - String result = ""; - int val = 0, valb = 0; - - for (byte c : input) { - val = (val << 8) + c; - valb += 8; - while (valb >= 6) { - valb -= 6; - result += base64_chars[(val >> valb) & 0x3F]; - } - } - - if (valb > 0) result += base64_chars[(val << (6 - valb)) & 0x3F]; - while (result.length() % 4) result += "="; - return result; -} - -void uploadToSupabase(String jsonData) { - if (!supabaseReady) return; - - String url = String(SUPABASE_URL) + "/rest/v1/sensor_data"; - - http.begin(client, url); - http.addHeader("Content-Type", "application/json"); - http.addHeader("apikey", SUPABASE_ANON_KEY); - http.addHeader("Authorization", "Bearer " + String(SUPABASE_ANON_KEY)); - - int httpResponseCode = http.POST(jsonData); - - if (httpResponseCode == 201 || httpResponseCode == 200) { - packetsUploaded++; - Serial.println("✓ Uploaded to Supabase\n"); - } else { - Serial.println("✗ Upload failed: " + String(httpResponseCode)); - String response = http.getString(); - Serial.println("Response: " + response + "\n"); - } - - http.end(); -} - -String getCurrentDateTime() { - if (!ntpSynced) { - return String(millis()/1000) + "s"; - } - - struct tm timeinfo; - if (!getLocalTime(&timeinfo)) { - return "Error"; - } - - char timeString[64]; - strftime(timeString, sizeof(timeString), "%Y-%m-%dT%H:%M:%S+05:30", &timeinfo); - return String(timeString); -} - -void displayStatistics() { - Serial.println("\n╔═══════════════════════════════════════╗"); - Serial.println("║ SYSTEM STATISTICS ║"); - Serial.println("╠═══════════════════════════════════════╣"); - Serial.println("║ Component Status: ║"); - Serial.println("║ WiFi: " + String(wifiConnected ? "✓ Connected " : "✗ Disconnected") + " ║"); - Serial.println("║ Supabase: " + String(supabaseReady ? "✓ Ready " : "✗ Not Ready ") + " ║"); - Serial.println("║ NTP: " + String(ntpSynced ? "✓ Synced " : "✗ Not Synced ") + " ║"); - Serial.println("║ LoRa: " + String(loraReady ? "✓ Active " : "✗ Offline ") + " ║"); - Serial.println("╠═══════════════════════════════════════╣"); - Serial.println("║ Packet Statistics: ║"); - Serial.println("║ Received: " + String(packetsReceived) + String(21 - String(packetsReceived).length(), ' ') + "║"); - Serial.println("║ Uploaded: " + String(packetsUploaded) + String(21 - String(packetsUploaded).length(), ' ') + "║"); - Serial.println("║ Corrupted: " + String(packetsCorrupted) + String(21 - String(packetsCorrupted).length(), ' ') + "║"); - - // ========== ADD THESE LINES HERE ========== - Serial.println("╠═══════════════════════════════════════╣"); - Serial.println("║ Message Relay Statistics: ║"); - Serial.println("║ Messages Sent: " + String(messagesSentToEdge) + String(17 - String(messagesSentToEdge).length(), ' ') + "║"); - // ========== END OF NEW LINES ========== - - Serial.println("╠═══════════════════════════════════════╣"); - Serial.println("║ Emergency Statistics: ║"); - Serial.println("║ 🚨 Emergencies: " + String(emergenciesDetected) + String(19 - String(emergenciesDetected).length(), ' ') + "║"); - Serial.println("║ 📧 Emails Sent: " + String(emailsSent) + String(19 - String(emailsSent).length(), ' ') + "║"); -Serial.println("╠═══════════════════════════════════════╣"); -Serial.println("║ Active Nodes (Packet Counts): ║"); -if (trackedNodesCount == 0) { - Serial.println("║ No nodes tracked yet ║"); -} else { - for (int i = 0; i < trackedNodesCount; i++) { - String nodeInfo = "║ " + nodeTrackers[i].nodeId + ": " + String(nodeTrackers[i].packetCount) + " pkts"; - int padding = 40 - nodeInfo.length(); - Serial.println(nodeInfo + String(padding, ' ') + "║"); - } -} -Serial.println("╚═══════════════════════════════════════╝\n"); - - Serial.println("Current Time: " + getCurrentDateTime()); - Serial.println(); -}/* - ESP32 LoRa Central Node - Supabase Gateway + Email Alerts - FIXED: Enhanced SMTP error logging and connection handling -*/ - -#include -#include -#include -#include -#include -#include -#include - -#define WIFI_SSID "iPhone" -#define WIFI_PASSWORD "12345678" - -#define SUPABASE_URL "https://kfwngukvlsjjhwslktbn.supabase.co" -#define SUPABASE_ANON_KEY "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imtmd25ndWt2bHNqamh3c2xrdGJuIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTgzNzYwMzksImV4cCI6MjA3Mzk1MjAzOX0.qY_JlPE6g5ewfBodJZYDS6ABFySvEMLgqOhCeQg8U8I" - -#define SMTP_SERVER "smtp.gmail.com" -#define SMTP_PORT 465 -#define SENDER_EMAIL "caneriesiren@gmail.com" -#define SENDER_PASSWORD "jczhurwioeagagiw" // App password without spaces -#define SUPERVISOR_EMAIL "caneriesiren@gmail.com" - -#define DEBUG_MODE true - -// Multiple NTP servers for better reliability -const char* ntpServers[] = { - "pool.ntp.org", - "time.nist.gov", - "time.google.com", - "time.cloudflare.com", - "time.windows.com" -}; -const int ntpServerCount = 5; -const long gmtOffset_sec = 19800; -const int daylightOffset_sec = 0; - -#define LORA_SCK 5 -#define LORA_MISO 19 -#define LORA_MOSI 27 -#define LORA_SS 18 -#define LORA_RST 14 -#define LORA_DIO0 2 -#define LORA_BAND 433E6 -#define CENTRAL_NODE_ID "CENTRAL_GATEWAY_001" - -#define EMAIL_SEND_TIMEOUT 300000 -#define EMAIL_BUFFER_SIZE 10 - -#define MESSAGE_TEST_INTERVAL 300000 - -// Structure to track packet counts per node -struct NodePacketTracker { - String nodeId; - unsigned long packetCount; - unsigned long lastSeenTime; -}; - -#define MAX_TRACKED_NODES 10 -NodePacketTracker nodeTrackers[MAX_TRACKED_NODES]; -int trackedNodesCount = 0; - -bool wifiConnected = false; -bool supabaseReady = false; -bool ntpSynced = false; -bool loraReady = false; - -unsigned long packetsReceived = 0; -unsigned long packetsUploaded = 0; -unsigned long packetsCorrupted = 0; -unsigned long emergenciesDetected = 0; -unsigned long emailsSent = 0; -unsigned long lastStatsDisplay = 0; -unsigned long lastWiFiCheck = 0; -unsigned long lastNTPSync = 0; - -unsigned long messagesDeliveredToWristband = 0; // Successfully ACKed by wristband - -unsigned long messagesSentToEdge = 0; -unsigned long lastTestMessage = 0; - -unsigned long getNodePacketCount(String nodeId); -String determineAirQuality(int mq2, int mq9, int mq135, bool mq2Digital, bool mq9Digital, bool mq135Digital); - -struct EmergencyRecord { - String nodeId; - unsigned long lastAlertTime; -}; - -EmergencyRecord emergencyBuffer[EMAIL_BUFFER_SIZE]; -int emergencyBufferIndex = 0; - -HTTPClient http; -WiFiClientSecure client; - -void displayStatistics(); -String getCurrentDateTime(); -bool canSendEmergencyEmail(String nodeId); -void recordEmergency(String nodeId); -void sendEmergencyEmail(JsonDocument& sensorData, int rssi, float snr); -bool sendSMTPEmail(String subject, String body); -String base64Encode(String input); -String readSMTPResponse(WiFiClient& client, int timeout = 5000); - -void setup() { - Serial.begin(115200); - delay(2000); - - Serial.println("\n====================================="); - Serial.println("ESP32 LoRa Central Node v2.1"); - Serial.println("Email Debug Enhanced"); - Serial.println("=====================================\n"); - - for (int i = 0; i < EMAIL_BUFFER_SIZE; i++) { - emergencyBuffer[i].nodeId = ""; - emergencyBuffer[i].lastAlertTime = 0; - } - - initializeLoRa(); - initializeWiFi(); - - if (wifiConnected) { - initializeNTP(); - initializeSupabase(); - } - - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.println("║ MESSAGE RELAY SYSTEM ENABLED ║"); - Serial.println("╠════════════════════════════════════════════╣"); - Serial.println("║ Commands: ║"); - Serial.println("║ msg - Send message to wristband ║"); - Serial.println("║ testmsg - Send test message ║"); - Serial.println("║ msgstats - Show statistics ║"); - Serial.println("║ help - Show command list ║"); - Serial.println("╚════════════════════════════════════════════╝\n"); - // ========== END OF NEW LINES ========== - Serial.println("Central Node ready!\n"); -} - -void loop() { - // Check WiFi connection (existing code - no changes) - if (millis() - lastWiFiCheck > 30000) { - checkWiFiConnection(); - lastWiFiCheck = millis(); - } - - // Check NTP sync (existing code - no changes) - if (!ntpSynced && wifiConnected && (millis() - lastNTPSync > 300000)) { - initializeNTP(); - lastNTPSync = millis(); - } - - // ========== ADD THIS BLOCK HERE ========== - // NEW: Handle message commands from Serial Monitor - handleMessageCommands(); - - // NEW: Optional - Send automatic test messages (comment out if not needed) - // Uncomment the block below to send test messages every 5 minutes - /* - if (millis() - lastTestMessage > MESSAGE_TEST_INTERVAL) { - sendMessageToWristband("Automatic test message at " + getCurrentDateTime()); - lastTestMessage = millis(); - } - */ - // ========== END OF NEW BLOCK ========== - - // Handle LoRa packets (existing code - no changes) - if (loraReady) { - handleLoRaPackets(); - } - - // Display statistics (existing code - no changes) - if (millis() - lastStatsDisplay > 60000) { - displayStatistics(); - lastStatsDisplay = millis(); - } - - delay(50); -} -void initializeLoRa() { - Serial.println("Initializing LoRa..."); - - SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_SS); - LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0); - - if (!LoRa.begin(LORA_BAND)) { // Now 433E6 - Serial.println("ERROR: LoRa failed!"); - loraReady = false; - return; - } - - LoRa.setTxPower(20); - LoRa.setSpreadingFactor(12); - LoRa.setSignalBandwidth(125E3); - LoRa.setCodingRate4(8); - LoRa.setPreambleLength(8); - LoRa.setSyncWord(0x34); - - // ADD these lines for better 433 MHz performance: - LoRa.enableCrc(); - LoRa.setOCP(240); - - Serial.println("LoRa OK\n"); - loraReady = true; -} - -void initializeWiFi() { - Serial.print("Connecting to WiFi"); - WiFi.mode(WIFI_STA); - WiFi.begin(WIFI_SSID, WIFI_PASSWORD); - - int attempts = 0; - while (WiFi.status() != WL_CONNECTED && attempts < 30) { - delay(500); - Serial.print("."); - attempts++; - } - - if (WiFi.status() == WL_CONNECTED) { - wifiConnected = true; - Serial.println("\nWiFi OK"); - Serial.println("IP: " + WiFi.localIP().toString() + "\n"); - } else { - wifiConnected = false; - Serial.println("\nWiFi FAILED\n"); - } -} - -void checkWiFiConnection() { - if (WiFi.status() != WL_CONNECTED && wifiConnected) { - Serial.println("WiFi lost. Reconnecting..."); - wifiConnected = false; - supabaseReady = false; - initializeWiFi(); - - if (wifiConnected && !supabaseReady) { - initializeSupabase(); - } - } -} - -void initializeNTP() { - if (!wifiConnected) return; - - Serial.println("Syncing NTP with multiple servers..."); - - struct tm timeinfo; - bool syncSuccess = false; - - // Try each NTP server until one works - for (int serverIndex = 0; serverIndex < ntpServerCount && !syncSuccess; serverIndex++) { - Serial.print(" Trying "); - Serial.print(ntpServers[serverIndex]); - Serial.print("... "); - - configTime(gmtOffset_sec, daylightOffset_sec, ntpServers[serverIndex]); - - int attempts = 0; - while (!getLocalTime(&timeinfo) && attempts < 10) { - delay(500); - attempts++; - } - - if (attempts < 10) { - syncSuccess = true; - ntpSynced = true; - Serial.println("✓ Success!"); - Serial.print(" Current time: "); - Serial.println(getCurrentDateTime()); - break; - } else { - Serial.println("✗ Failed"); - } - } - - if (!syncSuccess) { - Serial.println("❌ All NTP servers failed"); - ntpSynced = false; - } else { - Serial.println("NTP sync complete\n"); - } -} - -void initializeSupabase() { - if (!wifiConnected) return; - - Serial.println("Testing Supabase connection..."); - client.setInsecure(); - - String testUrl = String(SUPABASE_URL) + "/rest/v1/"; - http.begin(client, testUrl); - http.addHeader("Content-Type", "application/json"); - http.addHeader("apikey", SUPABASE_ANON_KEY); - - int httpResponseCode = http.GET(); - - if (httpResponseCode > 0) { - Serial.println("Supabase OK\n"); - supabaseReady = true; - } else { - Serial.println("Supabase FAILED\n"); - supabaseReady = false; - } - - http.end(); -} - -void handleLoRaPackets() { - int packetSize = LoRa.parsePacket(); - if (packetSize == 0) return; - - String receivedPacket = ""; - while (LoRa.available()) { - char c = (char)LoRa.read(); - if (c >= 20 && c <= 126) { - receivedPacket += c; - } - } - - if (receivedPacket.length() < 5) return; - - int rssi = LoRa.packetRssi(); - float snr = LoRa.packetSnr(); - - packetsReceived++; - - Serial.println("========================"); - Serial.println("LoRa Packet #" + String(packetsReceived)); - Serial.println("RSSI: " + String(rssi) + " | SNR: " + String(snr, 1)); - Serial.println("Size: " + String(receivedPacket.length()) + " bytes"); - - String cleanedPacket = cleanJsonPacket(receivedPacket); - if (cleanedPacket.length() > 0) { - processAndUploadPacket(cleanedPacket, rssi, snr); - } else { - packetsCorrupted++; - Serial.println("ERROR: Packet cleanup failed"); - } - Serial.println("========================\n"); -} - -String cleanJsonPacket(String rawPacket) { - DynamicJsonDocument testDoc(512); - if (deserializeJson(testDoc, rawPacket) == DeserializationError::Ok) { - return rawPacket; - } - - String cleaned = rawPacket; - - if (cleaned.endsWith(",p")) { - cleaned = cleaned.substring(0, cleaned.length() - 2) + "od\"}"; - } else if (cleaned.endsWith("Go,p")) { - cleaned = cleaned.substring(0, cleaned.length() - 4) + "Good\"}"; - } - - int openBraces = 0, closeBraces = 0; - for (char c : cleaned) { - if (c == '{') openBraces++; - if (c == '}') closeBraces++; - } - - while (closeBraces < openBraces) { - cleaned += "}"; - closeBraces++; - } - - int quotes = 0; - for (char c : cleaned) { - if (c == '"') quotes++; - } - - if (quotes % 2 != 0) cleaned += "\""; - - return cleaned; -} - -// Get or create packet count for a node -unsigned long getNodePacketCount(String nodeId) { - // Find existing node - for (int i = 0; i < trackedNodesCount; i++) { - if (nodeTrackers[i].nodeId == nodeId) { - nodeTrackers[i].packetCount++; - nodeTrackers[i].lastSeenTime = millis(); - return nodeTrackers[i].packetCount; - } - } - - // Node not found, add new one - if (trackedNodesCount < MAX_TRACKED_NODES) { - nodeTrackers[trackedNodesCount].nodeId = nodeId; - nodeTrackers[trackedNodesCount].packetCount = 1; - nodeTrackers[trackedNodesCount].lastSeenTime = millis(); - trackedNodesCount++; - return 1; - } - - // Buffer full, replace oldest entry - int oldestIndex = 0; - unsigned long oldestTime = nodeTrackers[0].lastSeenTime; - for (int i = 1; i < MAX_TRACKED_NODES; i++) { - if (nodeTrackers[i].lastSeenTime < oldestTime) { - oldestTime = nodeTrackers[i].lastSeenTime; - oldestIndex = i; - } - } - - nodeTrackers[oldestIndex].nodeId = nodeId; - nodeTrackers[oldestIndex].packetCount = 1; - nodeTrackers[oldestIndex].lastSeenTime = millis(); - return 1; -} - -// Determine air quality based on comprehensive sensor analysis -String determineAirQuality(int mq2, int mq9, int mq135, bool mq2Digital, bool mq9Digital, bool mq135Digital) { - /* - * COMPREHENSIVE AIR QUALITY ANALYSIS - * - * MQ2: Detects smoke, LPG, propane, methane, hydrogen - * Analog: 0-1023 (higher = more gas) - * Digital: true = gas detected above threshold - * - * MQ9: Detects carbon monoxide (CO) and combustible gases - * Analog: 0-1023 (higher = more CO) - * Digital: true = dangerous CO levels - * - * MQ135: General air quality - NH3, NOx, alcohol, benzene, smoke, CO2 - * Analog: 0-1023 (higher = worse air quality) - * Digital: true = poor air quality detected - * - * THRESHOLDS (calibrated for mining environments): - * - Safe: < 300 analog, no digital triggers - * - Fair: 300-400 analog, no digital triggers - * - Moderate: 400-500 analog, or 1 digital trigger - * - Bad: 500-650 analog, or 2 digital triggers - * - Danger: > 650 analog, or all 3 digital triggers - */ - - // Count digital triggers (immediate danger indicators) - int digitalAlerts = 0; - if (mq2Digital) digitalAlerts++; - if (mq9Digital) digitalAlerts++; - if (mq135Digital) digitalAlerts++; - - // CRITICAL: If all 3 digital sensors triggered = IMMEDIATE DANGER - if (digitalAlerts >= 3) { - return "DANGER"; - } - - // Analyze analog readings with weighted scoring - int dangerScore = 0; - int badScore = 0; - int moderateScore = 0; - int fairScore = 0; - - // MQ2 Analysis (Smoke/Flammable Gas) - HIGHEST PRIORITY in mines - if (mq2 > 700 || mq2Digital) { - dangerScore += 3; // Critical: explosion risk - } else if (mq2 > 550) { - badScore += 2; - } else if (mq2 > 320) { - moderateScore += 2; - } else if (mq2 > 100) { - fairScore += 1; - } - - // MQ9 Analysis (Carbon Monoxide) - HIGH PRIORITY (silent killer) - if (mq9 > 4000 || mq9Digital) { - dangerScore += 3; // Critical: CO poisoning risk - } else if (mq9 > 3200) { - badScore += 2; - } else if (mq9 > 2400) { - moderateScore += 2; - } else if (mq9 > 1600) { - fairScore += 1; - } - - // MQ135 Analysis (General Air Quality) - MEDIUM PRIORITY - if (mq135 > 2200 || mq135Digital) { - dangerScore += 2; // Critical: toxic air - } else if (mq135 > 1800) { - badScore += 2; - } else if (mq135 > 1400) { - moderateScore += 1; - } else if (mq135 > 1000) { - fairScore += 1; - } - - // Additional checks for digital alerts - if (digitalAlerts >= 2) { - dangerScore += 2; // Two sensors in digital alert = very dangerous - } else if (digitalAlerts == 1) { - badScore += 1; - } - - // DECISION LOGIC (prioritize worst conditions) - if (dangerScore >= 4) { - return "DANGER"; - } else if (dangerScore >= 2 || badScore >= 4) { - return "BAD"; - } else if (dangerScore >= 1 || badScore >= 2 || moderateScore >= 3) { - return "MODERATE"; - } else if (moderateScore >= 1 || fairScore >= 2) { - return "FAIR"; - } else { - return "GOOD"; - } -} - -void processAndUploadPacket(String cleanedPacket, int rssi, float snr) { - DynamicJsonDocument receivedDoc(900); - - DeserializationError error = deserializeJson(receivedDoc, cleanedPacket); - if (error) { - packetsCorrupted++; - Serial.println("ERROR: JSON parse failed: " + String(error.c_str())); - Serial.println("Raw packet: " + cleanedPacket); - return; - } - - // DEBUG: Print entire received JSON - if (DEBUG_MODE) { - Serial.println("\n=== RECEIVED JSON DEBUG ==="); - serializeJsonPretty(receivedDoc, Serial); - Serial.println("\n==========================="); - } - - // CRITICAL FIX: Check for emergency field more robustly - bool isEmergency = false; - - // Method 1: Check if field exists and is true - if (receivedDoc.containsKey("emergency")) { - JsonVariant emergencyVar = receivedDoc["emergency"]; - - if (emergencyVar.is()) { - isEmergency = emergencyVar.as(); - } else if (emergencyVar.is()) { - isEmergency = (emergencyVar.as() != 0); - } else if (emergencyVar.is()) { - String emergencyStr = emergencyVar.as(); - emergencyStr.toLowerCase(); - isEmergency = (emergencyStr == "true" || emergencyStr == "1"); - } - - Serial.println("Emergency field found: " + String(isEmergency ? "TRUE" : "FALSE")); - } else { - Serial.println("WARNING: No 'emergency' field in JSON"); - } - - String nodeId = receivedDoc["node"] | "UNKNOWN"; -unsigned long sensorPacketCount = getNodePacketCount(nodeId); - -// ⭐ ADD THESE 3 LINES HERE ⭐ -bool mq2Digital = receivedDoc["mq2_digital"] | false; -bool mq9Digital = receivedDoc["mq9_digital"] | false; -bool mq135Digital = receivedDoc["mq135_digital"] | false; - - - - // Handle emergency - if (isEmergency) { - emergenciesDetected++; - - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.println("║ 🚨 EMERGENCY SIGNAL RECEIVED! 🚨 ║"); - Serial.println("║ Node: " + nodeId + String(35 - nodeId.length(), ' ') + "║"); - Serial.println("║ Emergency Count: " + String(emergenciesDetected) + String(27 - String(emergenciesDetected).length(), ' ') + "║"); - Serial.println("╚════════════════════════════════════════════╝\n"); - Serial.println("Emergency logged to Supabase."); - } else { - if (DEBUG_MODE) { - Serial.println("📊 Normal packet (non-emergency) from node " + nodeId); - } - } - - // Build upload document - DynamicJsonDocument uploadDoc(1600); - uploadDoc["sensor_node_id"] = nodeId; - uploadDoc["sensor_packet_count"] = sensorPacketCount; - uploadDoc["sensor_timestamp"] = receivedDoc["timestamp"] | 0; - uploadDoc["temperature"] = receivedDoc["temp"]; - uploadDoc["humidity"] = receivedDoc["hum"] | 0; - uploadDoc["mq2_analog"] = receivedDoc["mq2"] | 0; - uploadDoc["mq9_analog"] = receivedDoc["mq9"] | 0; - uploadDoc["mq135_analog"] = receivedDoc["mq135"] | 0; - uploadDoc["mq2_digital"] = mq2Digital; -uploadDoc["mq9_digital"] = mq9Digital; -uploadDoc["mq135_digital"] = mq135Digital; - -// Calculate air quality with comprehensive logic -int mq2 = receivedDoc["mq2"] | 0; -int mq9 = receivedDoc["mq9"] | 0; -int mq135 = receivedDoc["mq135"] | 0; -String airQuality = determineAirQuality(mq2, mq9, mq135, mq2Digital, mq9Digital, mq135Digital); -uploadDoc["air_quality"] = airQuality; - -// Log air quality warnings -if (airQuality == "DANGER") { - Serial.println("🚨 CRITICAL AIR QUALITY: DANGER"); -} else if (airQuality == "BAD") { - Serial.println("⚠️ WARNING: BAD air quality detected"); -} else if (airQuality == "MODERATE") { - Serial.println("⚡ CAUTION: MODERATE air quality"); -} - // Use explicit float conversion - float motionAccel = receivedDoc["motion_accel"].as(); - float motionGyro = receivedDoc["motion_gyro"].as(); - - // Safety check - if (isnan(motionAccel)) motionAccel = 0.0f; - if (isnan(motionGyro)) motionGyro = 0.0f; - - uploadDoc["motion_accel"] = motionAccel; - uploadDoc["motion_gyro"] = motionGyro; - uploadDoc["bpm"] = receivedDoc["bpm"] | 0; - uploadDoc["spo2"] = receivedDoc["spo2"] | 0; - uploadDoc["body_temp"] = receivedDoc["body_temp"] | 0.0f; - uploadDoc["wristband_connected"] = receivedDoc["wristband_connected"] | 0; - uploadDoc["emergency"] = isEmergency; // Add emergency field to database - uploadDoc["central_node_id"] = CENTRAL_NODE_ID; - uploadDoc["received_time"] = getCurrentDateTime(); - uploadDoc["received_timestamp"] = millis(); - uploadDoc["rssi"] = rssi; - uploadDoc["snr"] = snr; - uploadDoc["gateway_packet_count"] = packetsReceived; - - String jsonString; - serializeJson(uploadDoc, jsonString); - - if (DEBUG_MODE || isEmergency) { - Serial.println("Upload Payload: " + jsonString); - } - - if (supabaseReady) { - uploadToSupabase(jsonString); - } else { - Serial.println("WARNING: Supabase not ready - data not uploaded"); - } -} - -bool canSendEmergencyEmail(String nodeId) { - unsigned long now = millis(); - - for (int i = 0; i < EMAIL_BUFFER_SIZE; i++) { - if (emergencyBuffer[i].nodeId == nodeId) { - if (now - emergencyBuffer[i].lastAlertTime >= EMAIL_SEND_TIMEOUT) { - return true; - } else { - return false; - } - } - } - - return true; -} - -void recordEmergency(String nodeId) { - unsigned long now = millis(); - - for (int i = 0; i < EMAIL_BUFFER_SIZE; i++) { - if (emergencyBuffer[i].nodeId == nodeId) { - emergencyBuffer[i].lastAlertTime = now; - return; - } - } - - if (emergencyBufferIndex >= EMAIL_BUFFER_SIZE) { - emergencyBufferIndex = 0; - } - - emergencyBuffer[emergencyBufferIndex].nodeId = nodeId; - emergencyBuffer[emergencyBufferIndex].lastAlertTime = now; - emergencyBufferIndex++; -} - -// * Send a text message to wristband via Edge Node relay -// * Central → (LoRa) → Edge → (ESP-NOW) → Wristband -// * -// * @param message The text message to send (max 120 characters recommended) -// * @return true if LoRa transmission succeeded, false otherwise -// */ -bool sendMessageToWristband(String message) { - if (!loraReady) { - Serial.println("❌ ERROR: LoRa not ready - cannot send message"); - return false; - } - - if (message.length() == 0) { - Serial.println("❌ ERROR: Empty message - not sending"); - return false; - } - - if (message.length() > 120) { - Serial.println("⚠️ WARNING: Message too long, truncating to 120 chars"); - message = message.substring(0, 120); - } - - // Create JSON packet with "message" field - // Edge node's receiveLoRaMessages() already looks for this field - DynamicJsonDocument doc(256); - doc["message"] = message; - doc["timestamp"] = millis(); - doc["from"] = "central_node"; - - String payload; - serializeJson(doc, payload); - - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.println("║ SENDING MESSAGE TO WRISTBAND VIA EDGE ║"); - Serial.println("╚════════════════════════════════════════════╝"); - Serial.println("Message: " + message); - Serial.println("Payload: " + payload); - Serial.println("Length: " + String(payload.length()) + " bytes"); - - // Send via LoRa - LoRa.beginPacket(); - LoRa.print(payload); - LoRa.endPacket(); - - messagesSentToEdge++; - - Serial.println("✓ Message transmitted via LoRa (#" + String(messagesSentToEdge) + ")"); - Serial.println(" → Edge node will forward to wristband via ESP-NOW"); - Serial.println("════════════════════════════════════════════\n"); - - return true; -} - -/** - * Check for serial commands to send messages - * Commands: - * msg - Send message to wristband - * testmsg - Send a test message - * msgstats - Show message statistics - */ -void handleMessageCommands() { - if (Serial.available() > 0) { - String command = Serial.readStringUntil('\n'); - command.trim(); - - if (command.length() == 0) return; - - // Convert to lowercase for comparison - String cmdLower = command; - cmdLower.toLowerCase(); - - if (cmdLower.startsWith("msg ")) { - // Extract message after "msg " - String message = command.substring(4); - message.trim(); - - if (message.length() > 0) { - Serial.println("\n>>> MANUAL MESSAGE COMMAND RECEIVED <<<"); - sendMessageToWristband(message); - } else { - Serial.println("ERROR: No message text provided"); - Serial.println("Usage: msg "); - } - } - else if (cmdLower == "testmsg") { - Serial.println("\n>>> TEST MESSAGE COMMAND <<<"); - sendMessageToWristband("This is a test message from central node"); - } - else if (cmdLower == "msgstats") { - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.println("║ MESSAGE RELAY STATISTICS ║"); - Serial.println("╚════════════════════════════════════════════╝"); - Serial.println("Messages sent to edge node: " + String(messagesSentToEdge)); - Serial.println("LoRa status: " + String(loraReady ? "✓ Ready" : "✗ Not Ready")); - Serial.println("════════════════════════════════════════════\n"); - } - else if (cmdLower == "help" || cmdLower == "commands") { - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.println("║ MESSAGE RELAY COMMANDS ║"); - Serial.println("╠════════════════════════════════════════════╣"); - Serial.println("║ msg - Send message to wristband ║"); - Serial.println("║ testmsg - Send test message ║"); - Serial.println("║ msgstats - Show message statistics ║"); - Serial.println("║ help - Show this menu ║"); - Serial.println("╚════════════════════════════════════════════╝\n"); - } - } -} - - -void sendEmergencyEmail(JsonDocument& sensorData, int rssi, float snr) { - if (!wifiConnected) { - Serial.println("❌ ERROR: WiFi disconnected - cannot send email"); - return; - } - - Serial.println("\n╔══════════════════════════════════════╗"); - Serial.println("║ PREPARING EMERGENCY EMAIL ║"); - Serial.println("╚══════════════════════════════════════╝\n"); - - String nodeId = sensorData["node"] | "UNKNOWN"; - float temperature = sensorData["temp"] | 0; - float humidity = sensorData["hum"] | 0; - int mq2 = sensorData["mq2"] | 0; - int mq9 = sensorData["mq9"] | 0; - int mq135 = sensorData["mq135"] | 0; - bool mq2Digital = sensorData["mq2_digital"] | false; // ✅ CORRECT - bool mq9Digital = sensorData["mq9_digital"] | false; // ✅ CORRECT - bool mq135Digital = sensorData["mq135_digital"] | false; // ✅ CORRECT - int bpm = sensorData["bpm"] | 0; - int spo2 = sensorData["spo2"] | 0; - bool wristbandConnected = sensorData["wristband_connected"] | 0; - float motionAccel = sensorData["motion_accel"] | 0; - float motionGyro = sensorData["motion_gyro"] | 0; - - String subject = "🚨 URGENT: Mine Worker Emergency - Node " + nodeId; - - String emailBody = "╔═══════════════════════════════════════════╗\n"; - emailBody += "║ EMERGENCY DISTRESS SIGNAL DETECTED ║\n"; - emailBody += "╚═══════════════════════════════════════════╝\n\n"; - emailBody += "Node ID: " + nodeId + "\n"; - emailBody += "Timestamp: " + getCurrentDateTime() + "\n"; - emailBody += "Emergency Count: #" + String(emergenciesDetected) + "\n\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "ENVIRONMENTAL CONDITIONS:\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "Temperature: " + String(temperature, 1) + "°C\n"; - emailBody += "Humidity: " + String(humidity, 1) + "%\n"; - emailBody += "MQ2 (Smoke): " + String(mq2) + "\n"; - emailBody += "MQ9 (CO): " + String(mq9) + "\n"; - emailBody += "MQ135 (Quality): " + String(mq135) + "\n\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "MOTION & VITALS:\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "Acceleration: " + String(motionAccel, 2) + " m/s²\n"; - emailBody += "Gyro Rotation: " + String(motionGyro, 2) + " °/s\n"; - emailBody += "Heart Rate: " + String(bpm) + " BPM\n"; - emailBody += "Blood Oxygen: " + String(spo2) + "%\n"; - emailBody += "Wristband: " + String(wristbandConnected ? "✓ Connected" : "✗ Disconnected") + "\n\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "SIGNAL QUALITY:\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "RSSI: " + String(rssi) + " dBm\n"; - emailBody += "SNR: " + String(snr) + " dB\n\n"; - emailBody += "⚠️ IMMEDIATE ACTION REQUIRED!\n"; - emailBody += " Contact emergency response team.\n"; - - Serial.println("Email Subject: " + subject); - Serial.println("Email Length: " + String(emailBody.length()) + " characters"); - Serial.println(); - - // Try to send email with retries - for (int attempt = 1; attempt <= 3; attempt++) { - Serial.println("📧 Email Send Attempt " + String(attempt) + "/3..."); - - if (sendSMTPEmail(subject, emailBody)) { - emailsSent++; - Serial.println("\n✅ Email sent successfully!"); - Serial.println(" Total emails sent: " + String(emailsSent)); - Serial.println("╚══════════════════════════════════════╝\n"); - return; - } - - if (attempt < 3) { - Serial.println("❌ Attempt " + String(attempt) + " failed. Retrying in 5 seconds...\n"); - delay(5000); - } - } - - Serial.println("\n❌ ERROR: Email send failed after 3 attempts"); - Serial.println(" Check:"); - Serial.println(" 1. WiFi connection"); - Serial.println(" 2. SMTP credentials"); - Serial.println(" 3. Gmail app password"); - Serial.println(" 4. Internet connectivity"); - Serial.println("╚══════════════════════════════════════╝\n"); -} - -String readSMTPResponse(WiFiClient& client, int timeout) { - unsigned long start = millis(); - String response = ""; - - while (millis() - start < timeout) { - if (client.available()) { - char c = client.read(); - response += c; - if (c == '\n') { - Serial.print("SMTP: "); - Serial.print(response); - if (response.indexOf("250") >= 0 || response.indexOf("334") >= 0 || - response.indexOf("235") >= 0 || response.indexOf("220") >= 0 || - response.indexOf("354") >= 0) { - return response; - } - if (response.indexOf("5") == 0) { // Error codes start with 5 - Serial.println("SMTP ERROR: " + response); - return response; - } - response = ""; - } - } - delay(10); - } - - Serial.println("SMTP Timeout"); - return ""; -} - -bool sendSMTPEmail(String subject, String body) { - // Use secure client from the start and connect to port 465 (SSL/TLS) - // Gmail supports both 587 (STARTTLS) and 465 (direct SSL) - // Port 465 is simpler as it's SSL from the start - - WiFiClientSecure secureClient; - secureClient.setInsecure(); - - Serial.println("Connecting to SMTP server (SSL)..."); - if (!secureClient.connect(SMTP_SERVER, 465)) { // Use port 465 for direct SSL - Serial.println("ERROR: Cannot connect to SMTP server"); - return false; - } - Serial.println("Connected to SMTP server via SSL"); - - // Wait for greeting - String response = readSMTPResponse(secureClient, 10000); - if (response.indexOf("220") < 0) { - Serial.println("ERROR: No greeting from server"); - secureClient.stop(); - return false; - } - - // EHLO - secureClient.println("EHLO ESP32"); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("250") < 0) { - Serial.println("ERROR: EHLO failed"); - secureClient.stop(); - return false; - } - - // Clear any remaining response lines - delay(500); - while (secureClient.available()) secureClient.read(); - - // AUTH LOGIN - secureClient.println("AUTH LOGIN"); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("334") < 0) { - Serial.println("ERROR: AUTH LOGIN failed"); - secureClient.stop(); - return false; - } - - // Username - secureClient.println(base64Encode(SENDER_EMAIL)); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("334") < 0) { - Serial.println("ERROR: Username rejected"); - secureClient.stop(); - return false; - } - - // Password - secureClient.println(base64Encode(SENDER_PASSWORD)); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("235") < 0) { - Serial.println("ERROR: Authentication failed - Check app password"); - secureClient.stop(); - return false; - } - Serial.println("Authentication successful"); - - // MAIL FROM - secureClient.print("MAIL FROM:<"); - secureClient.print(SENDER_EMAIL); - secureClient.println(">"); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("250") < 0) { - Serial.println("ERROR: MAIL FROM rejected"); - secureClient.stop(); - return false; - } - - // RCPT TO - secureClient.print("RCPT TO:<"); - secureClient.print(SUPERVISOR_EMAIL); - secureClient.println(">"); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("250") < 0) { - Serial.println("ERROR: RCPT TO rejected"); - secureClient.stop(); - return false; - } - - // DATA - secureClient.println("DATA"); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("354") < 0) { - Serial.println("ERROR: DATA command rejected"); - secureClient.stop(); - return false; - } - - // Email headers and body - secureClient.print("From: "); - secureClient.println(SENDER_EMAIL); - secureClient.print("To: "); - secureClient.println(SUPERVISOR_EMAIL); - secureClient.print("Subject: "); - secureClient.println(subject); - secureClient.println(); - secureClient.println(body); - secureClient.println("."); - - response = readSMTPResponse(secureClient, 10000); - if (response.indexOf("250") < 0) { - Serial.println("ERROR: Message rejected"); - secureClient.stop(); - return false; - } - - // QUIT - secureClient.println("QUIT"); - secureClient.stop(); - - Serial.println("Email sent successfully!"); - return true; -} - -String base64Encode(String input) { - const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - String result = ""; - int val = 0, valb = 0; - - for (byte c : input) { - val = (val << 8) + c; - valb += 8; - while (valb >= 6) { - valb -= 6; - result += base64_chars[(val >> valb) & 0x3F]; - } - } - - if (valb > 0) result += base64_chars[(val << (6 - valb)) & 0x3F]; - while (result.length() % 4) result += "="; - return result; -} - -void uploadToSupabase(String jsonData) { - if (!supabaseReady) return; - - String url = String(SUPABASE_URL) + "/rest/v1/sensor_data"; - - http.begin(client, url); - http.addHeader("Content-Type", "application/json"); - http.addHeader("apikey", SUPABASE_ANON_KEY); - http.addHeader("Authorization", "Bearer " + String(SUPABASE_ANON_KEY)); - - int httpResponseCode = http.POST(jsonData); - - if (httpResponseCode == 201 || httpResponseCode == 200) { - packetsUploaded++; - Serial.println("✓ Uploaded to Supabase\n"); - } else { - Serial.println("✗ Upload failed: " + String(httpResponseCode)); - String response = http.getString(); - Serial.println("Response: " + response + "\n"); - } - - http.end(); -} - -String getCurrentDateTime() { - if (!ntpSynced) { - return String(millis()/1000) + "s"; - } - - struct tm timeinfo; - if (!getLocalTime(&timeinfo)) { - return "Error"; - } - - char timeString[64]; - strftime(timeString, sizeof(timeString), "%Y-%m-%dT%H:%M:%S+05:30", &timeinfo); - return String(timeString); -} - -void displayStatistics() { - Serial.println("\n╔═══════════════════════════════════════╗"); - Serial.println("║ SYSTEM STATISTICS ║"); - Serial.println("╠═══════════════════════════════════════╣"); - Serial.println("║ Component Status: ║"); - Serial.println("║ WiFi: " + String(wifiConnected ? "✓ Connected " : "✗ Disconnected") + " ║"); - Serial.println("║ Supabase: " + String(supabaseReady ? "✓ Ready " : "✗ Not Ready ") + " ║"); - Serial.println("║ NTP: " + String(ntpSynced ? "✓ Synced " : "✗ Not Synced ") + " ║"); - Serial.println("║ LoRa: " + String(loraReady ? "✓ Active " : "✗ Offline ") + " ║"); - Serial.println("╠═══════════════════════════════════════╣"); - Serial.println("║ Packet Statistics: ║"); - Serial.println("║ Received: " + String(packetsReceived) + String(21 - String(packetsReceived).length(), ' ') + "║"); - Serial.println("║ Uploaded: " + String(packetsUploaded) + String(21 - String(packetsUploaded).length(), ' ') + "║"); - Serial.println("║ Corrupted: " + String(packetsCorrupted) + String(21 - String(packetsCorrupted).length(), ' ') + "║"); - - // ========== ADD THESE LINES HERE ========== - Serial.println("╠═══════════════════════════════════════╣"); - Serial.println("║ Message Relay Statistics: ║"); - Serial.println("║ Messages Sent: " + String(messagesSentToEdge) + String(17 - String(messagesSentToEdge).length(), ' ') + "║"); - // ========== END OF NEW LINES ========== - - Serial.println("╠═══════════════════════════════════════╣"); - Serial.println("║ Emergency Statistics: ║"); - Serial.println("║ 🚨 Emergencies: " + String(emergenciesDetected) + String(19 - String(emergenciesDetected).length(), ' ') + "║"); - Serial.println("║ 📧 Emails Sent: " + String(emailsSent) + String(19 - String(emailsSent).length(), ' ') + "║"); -Serial.println("╠═══════════════════════════════════════╣"); -Serial.println("║ Active Nodes (Packet Counts): ║"); -if (trackedNodesCount == 0) { - Serial.println("║ No nodes tracked yet ║"); -} else { - for (int i = 0; i < trackedNodesCount; i++) { - String nodeInfo = "║ " + nodeTrackers[i].nodeId + ": " + String(nodeTrackers[i].packetCount) + " pkts"; - int padding = 40 - nodeInfo.length(); - Serial.println(nodeInfo + String(padding, ' ') + "║"); - } -} -Serial.println("╚═══════════════════════════════════════╝\n"); - - Serial.println("Current Time: " + getCurrentDateTime()); - Serial.println(); -} diff --git a/AudioModule_test.cpp b/AudioModule_test.cpp deleted file mode 100644 index 4f9ea1d..0000000 --- a/AudioModule_test.cpp +++ /dev/null @@ -1,432 +0,0 @@ -#include - -HardwareSerial fnm16p(2); // use UART2 - -// FN-M16P Command Structure -const byte FRAME_START = 0x7E; -const byte FRAME_END = 0xEF; -const byte VERSION = 0xFF; -const byte NO_FEEDBACK = 0x00; -const byte WITH_FEEDBACK = 0x01; - -// Commands -const byte CMD_PLAY_TRACK = 0x03; -const byte CMD_VOLUME = 0x06; -const byte CMD_STOP = 0x16; - -// Audio file mapping -enum AudioFiles { - LOADING_AUDIO = 1, // 0001.mp3 - Boot/Loading - SUCCESS_AUDIO = 2, // 0002.mp3 - Success/Normal operation - ERROR_AUDIO = 3, // 0003.mp3 - Error/Fault detected - WARNING_AUDIO = 4, // 0004.mp3 - Warning condition - WELCOME_AUDIO = 5, // 0005.mp3 - Motion/Presence detected - GOODBYE_AUDIO = 6, // 0006.mp3 - System shutdown/standby - ALERT_AUDIO = 7, // 0007.mp3 - Critical alert - NOTIFICATION_AUDIO = 8 // 0008.mp3 - General notification -}; - -// System state variables -bool bootupComplete = false; -bool isPlaying = false; -unsigned long lastPlayTime = 0; -unsigned long lastSensorCheck = 0; -unsigned long bootTime = 0; - -// Simulated sensor values (replace with real sensors) -float temperature = 25.0; -int motionLevel = 0; -int lightLevel = 512; // 0-1023 (ADC range) -int soundLevel = 200; // 0-1023 -bool buttonState = false; -int batteryLevel = 100; // 0-100% -bool emergencyFlag = false; - -// Thresholds for triggering audio -const float TEMP_WARNING = 35.0; -const float TEMP_CRITICAL = 45.0; -const int MOTION_THRESHOLD = 500; -const int LIGHT_LOW_THRESHOLD = 200; -const int SOUND_HIGH_THRESHOLD = 700; -const int BATTERY_LOW_THRESHOLD = 20; - -// Timing controls -const unsigned long SENSOR_CHECK_INTERVAL = 2000; // Check sensors every 2 seconds -const unsigned long MIN_PLAY_INTERVAL = 5000; // Minimum 5 seconds between audio plays -const unsigned long BOOTUP_DELAY = 3000; // Wait 3 seconds before starting sensor monitoring - -void setup() { - Serial.begin(115200); - fnm16p.begin(9600, SERIAL_8N1, 16, 17); // RX=16, TX=17 - - Serial.println("=== FN-M16P Automated Audio System ==="); - Serial.println("Booting up..."); - - bootTime = millis(); - - // Initialize random seed - randomSeed(analogRead(A0)); - - delay(2000); // Give module time to initialize - - // Set volume - setVolume(22); - delay(200); - - // Play boot audio - Serial.println("🎵 Playing boot audio (0001.mp3)"); - playAudioFile(LOADING_AUDIO); - - delay(BOOTUP_DELAY); - bootupComplete = true; - - Serial.println("✅ System ready - Starting automated monitoring"); - Serial.println("📊 Monitoring: Temperature | Motion | Light | Sound | Battery"); - Serial.println("⌨️ Manual commands: temp, motion, light, sound, emergency, stop, vol+, vol-, status, help"); - Serial.println(); -} - -void loop() { - // Handle manual commands - handleManualCommands(); - - // Clear any module replies - clearModuleBuffer(); - - // Only start sensor monitoring after bootup is complete - if (bootupComplete) { - // Update simulated sensor values - updateSensorValues(); - - // Check sensors and trigger audio if needed - if (millis() - lastSensorCheck >= SENSOR_CHECK_INTERVAL) { - checkSensorsAndPlayAudio(); - lastSensorCheck = millis(); - } - } - - delay(100); -} - -void updateSensorValues() { - // Simulate realistic sensor data with random variations - static unsigned long lastUpdate = 0; - - if (millis() - lastUpdate >= 1000) { // Update every second - // Temperature: Slow random walk - temperature += random(-10, 11) / 10.0; // ±1°C variation - if (temperature < 15) temperature = 15; - if (temperature > 50) temperature = 50; - - // Motion: Random spikes - if (random(100) < 15) { // 15% chance of motion - motionLevel = random(400, 800); - } else { - motionLevel = random(0, 100); - } - - // Light: Gradual changes (day/night simulation) - lightLevel += random(-50, 51); - if (lightLevel < 0) lightLevel = 0; - if (lightLevel > 1023) lightLevel = 1023; - - // Sound: Random environmental noise - soundLevel = random(100, 300); - if (random(100) < 10) { // 10% chance of loud sound - soundLevel = random(600, 900); - } - - // Battery: Slowly drain - if (random(100) < 2) { // 2% chance to decrease - batteryLevel--; - if (batteryLevel < 0) batteryLevel = 0; - } - - // Emergency: Very rare random event - if (random(1000) < 2) { // 0.2% chance - emergencyFlag = true; - } - - lastUpdate = millis(); - } -} - -void checkSensorsAndPlayAudio() { - // Don't interrupt if already playing - if (isPlaying && (millis() - lastPlayTime < MIN_PLAY_INTERVAL)) { - return; - } - - // Priority order: Emergency > Critical > Warning > Normal events - - // EMERGENCY - Highest priority - if (emergencyFlag) { - Serial.println("🚨 EMERGENCY DETECTED!"); - printSensorValues(); - playAudioFile(ALERT_AUDIO); - emergencyFlag = false; // Reset flag - return; - } - - // CRITICAL CONDITIONS - if (temperature >= TEMP_CRITICAL) { - Serial.println("🌡️ CRITICAL: Temperature too high!"); - printSensorValues(); - playAudioFile(ERROR_AUDIO); - return; - } - - if (batteryLevel <= 5) { - Serial.println("🔋 CRITICAL: Battery critically low!"); - printSensorValues(); - playAudioFile(ERROR_AUDIO); - return; - } - - // WARNING CONDITIONS - if (temperature >= TEMP_WARNING) { - Serial.println("⚠️ WARNING: High temperature detected"); - printSensorValues(); - playAudioFile(WARNING_AUDIO); - return; - } - - if (batteryLevel <= BATTERY_LOW_THRESHOLD && batteryLevel > 5) { - Serial.println("⚠️ WARNING: Low battery"); - printSensorValues(); - playAudioFile(WARNING_AUDIO); - batteryLevel = 5; // Prevent repeated warnings - return; - } - - if (soundLevel >= SOUND_HIGH_THRESHOLD) { - Serial.println("🔊 WARNING: High noise level detected"); - printSensorValues(); - playAudioFile(WARNING_AUDIO); - return; - } - - // NORMAL EVENTS - if (motionLevel >= MOTION_THRESHOLD) { - Serial.println("👋 Motion detected - Welcome!"); - printSensorValues(); - playAudioFile(WELCOME_AUDIO); - return; - } - - if (lightLevel <= LIGHT_LOW_THRESHOLD) { - static bool lowLightAlerted = false; - if (!lowLightAlerted) { - Serial.println("🌙 Low light conditions"); - printSensorValues(); - playAudioFile(NOTIFICATION_AUDIO); - lowLightAlerted = true; - } - return; - } else { - static bool lowLightAlerted = false; - lowLightAlerted = false; // Reset when light is normal - } - - // RANDOM POSITIVE EVENTS (less frequent) - if (random(1000) < 3) { // 0.3% chance - Serial.println("✨ Random success event!"); - printSensorValues(); - playAudioFile(SUCCESS_AUDIO); - return; - } -} - -void handleManualCommands() { - if (Serial.available()) { - String command = Serial.readStringUntil('\n'); - command.trim(); - command.toLowerCase(); - - if (command == "temp") { - temperature = TEMP_WARNING + 5; // Trigger temperature warning - Serial.println("🌡️ Temperature spike simulated"); - } - else if (command == "motion") { - motionLevel = MOTION_THRESHOLD + 100; // Trigger motion - Serial.println("👋 Motion simulation triggered"); - } - else if (command == "light") { - lightLevel = LIGHT_LOW_THRESHOLD - 50; // Trigger low light - Serial.println("🌙 Low light simulation triggered"); - } - else if (command == "sound") { - soundLevel = SOUND_HIGH_THRESHOLD + 50; // Trigger high sound - Serial.println("🔊 High sound simulation triggered"); - } - else if (command == "emergency") { - emergencyFlag = true; - Serial.println("🚨 Emergency simulation triggered"); - } - else if (command == "battery") { - batteryLevel = BATTERY_LOW_THRESHOLD - 5; // Trigger low battery - Serial.println("🔋 Low battery simulation triggered"); - } - else if (command == "stop") { - stopAudio(); - } - else if (command == "vol+" || command == "volup") { - adjustVolume(3); - } - else if (command == "vol-" || command == "voldown") { - adjustVolume(-3); - } - else if (command == "status") { - printSystemStatus(); - } - else if (command == "1" || command == "2" || command == "3" || - command == "4" || command == "5" || command == "6" || - command == "7" || command == "8") { - playAudioFile(command.toInt()); - } - else if (command == "help") { - printHelp(); - } - } -} - -void playAudioFile(int fileNumber) { - if (fileNumber < 1 || fileNumber > 8) return; - - Serial.print("🎵 Playing: "); - Serial.print(getAudioDescription(fileNumber)); - Serial.print(" ("); - Serial.print(String(fileNumber, 4).c_str()); - Serial.println(".mp3)"); - - sendCommand(CMD_PLAY_TRACK, 0x00, fileNumber, false); - isPlaying = true; - lastPlayTime = millis(); -} - -String getAudioDescription(int fileNumber) { - switch(fileNumber) { - case 1: return "Boot/Loading"; - case 2: return "Success"; - case 3: return "Error/Critical"; - case 4: return "Warning"; - case 5: return "Welcome/Motion"; - case 6: return "Goodbye"; - case 7: return "Emergency Alert"; - case 8: return "Notification"; - default: return "Unknown"; - } -} - -void sendCommand(byte cmd, byte param1, byte param2, bool feedback) { - byte packet[10]; - - packet[0] = FRAME_START; - packet[1] = VERSION; - packet[2] = 0x06; - packet[3] = cmd; - packet[4] = feedback ? WITH_FEEDBACK : NO_FEEDBACK; - packet[5] = param1; - packet[6] = param2; - - uint16_t sum = packet[1] + packet[2] + packet[3] + packet[4] + packet[5] + packet[6]; - uint16_t checksum = 0xFFFF - sum + 1; - - packet[7] = (checksum >> 8) & 0xFF; - packet[8] = checksum & 0xFF; - packet[9] = FRAME_END; - - fnm16p.write(packet, 10); -} - -void setVolume(int volume) { - if (volume < 0) volume = 0; - if (volume > 30) volume = 30; - sendCommand(CMD_VOLUME, 0x00, volume, false); -} - -void adjustVolume(int change) { - static int currentVolume = 22; - currentVolume += change; - if (currentVolume < 0) currentVolume = 0; - if (currentVolume > 30) currentVolume = 30; - - Serial.print("🔊 Volume: "); - Serial.println(currentVolume); - setVolume(currentVolume); -} - -void stopAudio() { - Serial.println("⏹️ Stopping audio"); - sendCommand(CMD_STOP, 0x00, 0x00, false); - isPlaying = false; -} - -void clearModuleBuffer() { - while (fnm16p.available()) { - fnm16p.read(); // Clear any incoming data - } -} - -void printSensorValues() { - Serial.print("📊 Sensors: "); - Serial.print("Temp:"); - Serial.print(temperature, 1); - Serial.print("°C | Motion:"); - Serial.print(motionLevel); - Serial.print(" | Light:"); - Serial.print(lightLevel); - Serial.print(" | Sound:"); - Serial.print(soundLevel); - Serial.print(" | Battery:"); - Serial.print(batteryLevel); - Serial.println("%"); -} - -void printSystemStatus() { - Serial.println("\n=== SYSTEM STATUS ==="); - Serial.print("Uptime: "); - Serial.print((millis() - bootTime) / 1000); - Serial.println(" seconds"); - Serial.print("Boot Complete: "); - Serial.println(bootupComplete ? "YES" : "NO"); - Serial.print("Currently Playing: "); - Serial.println(isPlaying ? "YES" : "NO"); - printSensorValues(); - Serial.println("\n=== THRESHOLDS ==="); - Serial.print("Temperature Warning: >"); - Serial.print(TEMP_WARNING); - Serial.println("°C"); - Serial.print("Temperature Critical: >"); - Serial.print(TEMP_CRITICAL); - Serial.println("°C"); - Serial.print("Motion Threshold: >"); - Serial.println(MOTION_THRESHOLD); - Serial.print("Low Light: <"); - Serial.println(LIGHT_LOW_THRESHOLD); - Serial.print("High Sound: >"); - Serial.println(SOUND_HIGH_THRESHOLD); - Serial.print("Low Battery: <"); - Serial.print(BATTERY_LOW_THRESHOLD); - Serial.println("%"); - Serial.println("==================\n"); -} - -void printHelp() { - Serial.println("\n=== COMMAND REFERENCE ==="); - Serial.println("🎯 Sensor Triggers:"); - Serial.println(" temp - Simulate high temperature"); - Serial.println(" motion - Simulate motion detection"); - Serial.println(" light - Simulate low light"); - Serial.println(" sound - Simulate high sound"); - Serial.println(" battery - Simulate low battery"); - Serial.println(" emergency - Simulate emergency"); - Serial.println("\n🎵 Direct Audio:"); - Serial.println(" 1-8 - Play specific audio file"); - Serial.println("\n🎛️ Controls:"); - Serial.println(" stop - Stop current audio"); - Serial.println(" vol+/vol- - Adjust volume"); - Serial.println(" status - Show system status"); - Serial.println(" help - Show this help"); - Serial.println("========================\n"); -} diff --git a/CODE/package-lock.json b/CODE/package-lock.json index 28d6c1a..6d7972d 100644 --- a/CODE/package-lock.json +++ b/CODE/package-lock.json @@ -13789,6 +13789,22 @@ } } }, + "node_modules/tailwindcss/node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/tapable": { "version": "2.3.0", "license": "MIT", diff --git a/CTRL1/Controller1.php b/CTRL1/Controller1.php deleted file mode 100644 index 7fbe870..0000000 --- a/CTRL1/Controller1.php +++ /dev/null @@ -1,307 +0,0 @@ -/* -This is the controller for the SIREN gas detection system. -It is used to store and retrieve data from the Supabase database. -It is also used to analyze the gas readings and determine the danger level. -Created by: Shishir Dwivedi -Created on: 17/08/2025 -Version: 1.0 - -LOL won't work ;/ - -*/ - - - $response, - 'http_code' => $httpCode, - 'error' => $error - ]; - } - - private function validateSensorData($data) { - $required = ['mq9_v', 'mq135_d']; - - foreach ($required as $field) { - if (!isset($data[$field])) { - return "Missing required field: $field"; - } - } - - // Validate data types - if (!is_numeric($data['mq9_v'])) { - return "mq9_v must be numeric (voltage value)"; - } - - if (!is_bool($data['mq135_d']) && !in_array($data['mq135_d'], [0, 1, '0', '1', 'true', 'false'])) { - return "mq135_d must be boolean (true/false)"; - } - - // Validate GPS coordinates if provided - if (isset($data['lat']) && (!is_numeric($data['lat']) || $data['lat'] < -90 || $data['lat'] > 90)) { - return "lat must be numeric between -90 and 90"; - } - - if (isset($data['long']) && (!is_numeric($data['long']) || $data['long'] < -180 || $data['long'] > 180)) { - return "long must be numeric between -180 and 180"; - } - - return null; - } - - private function analyzeGasReadings($mq9_voltage, $mq135_digital) { - $mq9_analysis = "Normal - No dangerous gases detected"; - $mq135_analysis = "Good - Air quality acceptable"; - $danger_level = "LOW"; - - // MQ-9 Gas Analysis (CO, LPG, Methane detection) - if ($mq9_voltage > 2.5) { - $mq9_analysis = "CRITICAL - Very high gas concentration! Evacuate immediately!"; - $danger_level = "CRITICAL"; - } elseif ($mq9_voltage > 2.0) { - $mq9_analysis = "HIGH DANGER - Dangerous levels of CO/LPG/Methane detected"; - $danger_level = "HIGH"; - } elseif ($mq9_voltage > 1.5) { - $mq9_analysis = "MODERATE - Elevated gas levels detected"; - $danger_level = ($danger_level === "LOW") ? "MODERATE" : $danger_level; - } elseif ($mq9_voltage > 1.0) { - $mq9_analysis = "LOW - Trace amounts of gases detected"; - $danger_level = ($danger_level === "LOW") ? "LOW-MODERATE" : $danger_level; - } - - // MQ-135 Digital Analysis (Air quality/pollution) - if ($mq135_digital === false || $mq135_digital === 0 || $mq135_digital === '0') { - $mq135_analysis = "ALERT - Air pollution detected! Poor air quality"; - if ($danger_level === "LOW") { - $danger_level = "MODERATE"; - } - } - - return [ - 'mq9_gas_analysis' => $mq9_analysis, - 'mq135_air_quality' => $mq135_analysis, - 'overall_danger_level' => $danger_level - ]; - } - - public function storeSensorData() { - if ($_SERVER['REQUEST_METHOD'] !== 'POST') { - http_response_code(405); - echo json_encode([ - 'success' => false, - 'error' => 'Method not allowed. Use POST request.', - 'endpoint' => 'SIREN.great-site.net/controller-1.php?action=store' - ]); - return; - } - - $input = json_decode(file_get_contents('php://input'), true); - - if (!$input) { - http_response_code(400); - echo json_encode([ - 'success' => false, - 'error' => 'Invalid JSON data or empty request body' - ]); - return; - } - - // Validate required fields - $validation_error = $this->validateSensorData($input); - if ($validation_error) { - http_response_code(400); - echo json_encode([ - 'success' => false, - 'error' => $validation_error, - 'received_data' => $input - ]); - return; - } - - // Analyze gas readings - $analysis = $this->analyzeGasReadings($input['mq9_v'], $input['mq135_d']); - - // Prepare data according to your database structure - $sensor_data = [ - 'timestamp' => date('Y-m-d\TH:i:s.u\Z'), // ISO 8601 format with timezone - 'lat' => isset($input['lat']) ? (float)$input['lat'] : null, - 'long' => isset($input['long']) ? (float)$input['long'] : null, - 'mq9_v' => (float)$input['mq9_v'], - 'mq9_g' => $analysis['mq9_gas_analysis'], - 'mq135_d' => ($input['mq135_d'] === true || $input['mq135_d'] === 1 || $input['mq135_d'] === '1') ? true : false, - 'mq135_a' => $analysis['mq135_air_quality'] - ]; - - // Store in Supabase - $result = $this->makeSupabaseRequest($sensor_data); - - if ($result['http_code'] === 201) { - http_response_code(201); - echo json_encode([ - 'success' => true, - 'message' => 'Gas sensor data stored successfully in SIREN database', - 'data' => $sensor_data, - 'analysis' => $analysis, - 'timestamp' => $sensor_data['timestamp'], - 'location' => [ - 'latitude' => $sensor_data['lat'], - 'longitude' => $sensor_data['long'] - ] - ]); - } else { - http_response_code(500); - echo json_encode([ - 'success' => false, - 'error' => 'Failed to store data in Supabase', - 'details' => $result['response'], - 'curl_error' => $result['error'], - 'http_code' => $result['http_code'] - ]); - } - } - - public function getRecentData() { - if ($_SERVER['REQUEST_METHOD'] !== 'GET') { - http_response_code(405); - echo json_encode(['error' => 'Method not allowed. Use GET request.']); - return; - } - - $limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 10; - $danger_filter = $_GET['danger_level'] ?? null; - - $url = SUPABASE_API_URL . '?order=timestamp.desc&limit=' . $limit; - - // Filter by danger level if specified - if ($danger_filter) { - $url .= '&mq9_g=ilike.*' . urlencode($danger_filter) . '*'; - } - - $headers = [ - 'apikey: ' . SUPABASE_ANON_KEY, - 'Authorization: Bearer ' . SUPABASE_ANON_KEY - ]; - - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - - $response = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - - if ($httpCode === 200) { - $data = json_decode($response, true); - echo json_encode([ - 'success' => true, - 'count' => count($data), - 'data' => $data, - 'endpoint' => 'SIREN.great-site.net/php/controller-1.php?action=get' - ]); - } else { - http_response_code(500); - echo json_encode([ - 'success' => false, - 'error' => 'Failed to fetch data from Supabase', - 'http_code' => $httpCode - ]); - } - } - - public function getSystemStatus() { - echo json_encode([ - 'system' => 'SIREN Gas Detection System', - 'status' => 'ONLINE', - 'version' => '1.0', - 'endpoint' => 'SIREN.great-site.net/php/controller-1.php', - 'supported_actions' => [ - 'store' => 'POST - Store sensor data', - 'get' => 'GET - Retrieve recent data', - 'status' => 'GET - System status' - ], - 'database_structure' => [ - 'id' => 'int8 (auto)', - 'timestamp' => 'timestamptz', - 'lat' => 'numeric (GPS latitude)', - 'long' => 'numeric (GPS longitude)', - 'mq9_v' => 'numeric (voltage)', - 'mq9_g' => 'varchar (gas analysis)', - 'mq135_d' => 'bool (digital alert)', - 'mq135_a' => 'varchar (air quality)' - ], - 'server_time' => date('Y-m-d H:i:s T'), - 'timezone' => date_default_timezone_get() - ]); - } -} - -// Handle requests -$controller = new SirenGasSensorController(); -$action = $_GET['action'] ?? 'status'; - -switch ($action) { - case 'store': - $controller->storeSensorData(); - break; - case 'get': - $controller->getRecentData(); - break; - case 'status': - $controller->getSystemStatus(); - break; - default: - http_response_code(400); - echo json_encode([ - 'success' => false, - 'error' => 'Invalid action. Supported: store, get, status', - 'usage' => [ - 'store' => 'POST /controller-1.php?action=store', - 'get' => 'GET /controller-1.php?action=get', - 'status' => 'GET /controller-1.php?action=status' - ] - ]); -} -?> diff --git a/CTRL1/ingest.php b/CTRL1/ingest.php deleted file mode 100644 index 54f2d83..0000000 --- a/CTRL1/ingest.php +++ /dev/null @@ -1,16 +0,0 @@ - $data["device_id"] ?? "EDGE-001", - "ts" => date('c'), // keep your NTP ts in data if you have it - "gas_ppm" => $data["gas_ppm"] ?? null, - "danger" => $data["danger"] ?? false, - "meta" => $data -]; - -echo supabase_insert_telemetry($payload); \ No newline at end of file diff --git a/CTRL1/supabase_client.php b/CTRL1/supabase_client.php deleted file mode 100644 index 6b00e3e..0000000 --- a/CTRL1/supabase_client.php +++ /dev/null @@ -1,27 +0,0 @@ - true, - CURLOPT_HTTPHEADER => [ - "apikey: $service_key", - "Authorization: Bearer $service_key", - "Content-Type: application/json", - "Prefer: return=representation" - ], - CURLOPT_POSTFIELDS => json_encode($payload), - CURLOPT_RETURNTRANSFER => true, - ]); - $out = curl_exec($ch); - $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - - if ($code >= 200 && $code < 300) return $out; - http_response_code($code); - echo $out; - exit; -} \ No newline at end of file diff --git a/Central_node.ino b/Central_node.ino deleted file mode 100644 index 002a1b3..0000000 --- a/Central_node.ino +++ /dev/null @@ -1,1213 +0,0 @@ -/* - ESP32 LoRa Central Node - Supabase Gateway + Email Alerts - FIXED: Enhanced SMTP error logging and connection handling -*/ - -#include -#include -#include -#include -#include -#include -#include -#include - -#define WIFI_SSID "iPhone" -#define WIFI_PASSWORD "12345678" - -#define SUPABASE_URL "https://kfwngukvlsjjhwslktbn.supabase.co" -#define SUPABASE_ANON_KEY "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imtmd25ndWt2bHNqamh3c2xrdGJuIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTgzNzYwMzksImV4cCI6MjA3Mzk1MjAzOX0.qY_JlPE6g5ewfBodJZYDS6ABFySvEMLgqOhCeQg8U8I" - -#define SMTP_SERVER "smtp.gmail.com" -#define SMTP_PORT 465 -#define SENDER_EMAIL "caneriesiren@gmail.com" -#define SENDER_PASSWORD "jczhurwioeagagiw" // App password without spaces -#define SUPERVISOR_EMAIL "caneriesiren@gmail.com" - -#define DEBUG_MODE true - -// Multiple NTP servers for better reliability -const char* ntpServers[] = { - "pool.ntp.org", - "time.nist.gov", - "time.google.com", - "time.cloudflare.com", - "time.windows.com" -}; -const int ntpServerCount = 5; -const long gmtOffset_sec = 19800; -const int daylightOffset_sec = 0; - -#define LORA_SCK 5 -#define LORA_MISO 19 -#define LORA_MOSI 27 -#define LORA_SS 18 -#define LORA_RST 14 -#define LORA_DIO0 2 -#define LORA_BAND 433E6 -#define CENTRAL_NODE_ID "CENTRAL_GATEWAY_001" - -#define EMAIL_SEND_TIMEOUT 300000 -#define EMAIL_BUFFER_SIZE 10 - -#define MESSAGE_TEST_INTERVAL 300000 - -// Structure to track packet counts per node -struct NodePacketTracker { - String nodeId; - unsigned long packetCount; - unsigned long lastSeenTime; -}; - -#define MAX_TRACKED_NODES 10 -NodePacketTracker nodeTrackers[MAX_TRACKED_NODES]; -int trackedNodesCount = 0; - -bool wifiConnected = false; -bool supabaseReady = false; -bool ntpSynced = false; -bool loraReady = false; - -unsigned long packetsReceived = 0; -unsigned long packetsUploaded = 0; -unsigned long packetsCorrupted = 0; -unsigned long emergenciesDetected = 0; -unsigned long emailsSent = 0; -unsigned long lastStatsDisplay = 0; -unsigned long lastWiFiCheck = 0; -unsigned long lastNTPSync = 0; - -unsigned long messagesDeliveredToWristband = 0; // Successfully ACKed by wristband - -unsigned long messagesSentToEdge = 0; -unsigned long lastTestMessage = 0; - -unsigned long getNodePacketCount(String nodeId); -String determineAirQuality(int mq2, int mq9, int mq135, bool mq2Digital, bool mq9Digital, bool mq135Digital); - -struct EmergencyRecord { - String nodeId; - unsigned long lastAlertTime; -}; - -EmergencyRecord emergencyBuffer[EMAIL_BUFFER_SIZE]; -int emergencyBufferIndex = 0; - -HTTPClient http; -WiFiClientSecure client; -WebServer server(80); - -void displayStatistics(); -String getCurrentDateTime(); -bool canSendEmergencyEmail(String nodeId); -void recordEmergency(String nodeId); -void sendEmergencyEmail(JsonDocument& sensorData, int rssi, float snr); -bool sendSMTPEmail(String subject, String body); -String base64Encode(String input); -String readSMTPResponse(WiFiClient& client, int timeout = 5000); - -void setup() { - Serial.begin(115200); - delay(2000); - - Serial.println("\n====================================="); - Serial.println("ESP32 LoRa Central Node v2.1"); - Serial.println("Email Debug Enhanced"); - Serial.println("=====================================\n"); - - for (int i = 0; i < EMAIL_BUFFER_SIZE; i++) { - emergencyBuffer[i].nodeId = ""; - emergencyBuffer[i].lastAlertTime = 0; - } - - initializeLoRa(); - initializeWiFi(); - - // Register HTTP endpoints - server.on("/send", HTTP_POST, handleSendMessage); - server.on("/send", HTTP_OPTIONS, handleSendMessage); // CORS preflight - server.on("/health", HTTP_GET, []() { - server.sendHeader("Access-Control-Allow-Origin", "*"); - server.send(200, "application/json", "{\"status\":\"ok\",\"lora\":" + String(loraReady ? "true" : "false") + "}"); - }); - server.begin(); - Serial.println("HTTP server started on port 80"); - Serial.println("Endpoint: POST http://" + WiFi.localIP().toString() + "/send"); - - if (wifiConnected) { - initializeNTP(); - initializeSupabase(); - } - - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.println("║ MESSAGE RELAY SYSTEM ENABLED ║"); - Serial.println("╠════════════════════════════════════════════╣"); - Serial.println("║ Commands: ║"); - Serial.println("║ msg - Send message to wristband ║"); - Serial.println("║ testmsg - Send test message ║"); - Serial.println("║ msgstats - Show statistics ║"); - Serial.println("║ help - Show command list ║"); - Serial.println("╚════════════════════════════════════════════╝\n"); - // ========== END OF NEW LINES ========== - Serial.println("Central Node ready!\n"); -} - -void loop() { - server.handleClient(); - // Check WiFi connection (existing code - no changes) - if (millis() - lastWiFiCheck > 30000) { - checkWiFiConnection(); - lastWiFiCheck = millis(); - } - - // Check NTP sync (existing code - no changes) - if (!ntpSynced && wifiConnected && (millis() - lastNTPSync > 300000)) { - initializeNTP(); - lastNTPSync = millis(); - } - - // ========== ADD THIS BLOCK HERE ========== - // NEW: Handle message commands from Serial Monitor - handleMessageCommands(); - - // NEW: Optional - Send automatic test messages (comment out if not needed) - // Uncomment the block below to send test messages every 5 minutes - /* - if (millis() - lastTestMessage > MESSAGE_TEST_INTERVAL) { - sendMessageToWristband("Automatic test message at " + getCurrentDateTime()); - lastTestMessage = millis(); - } - */ - // ========== END OF NEW BLOCK ========== - - // Handle LoRa packets (existing code - no changes) - if (loraReady) { - handleLoRaPackets(); - } - - // Display statistics (existing code - no changes) - if (millis() - lastStatsDisplay > 60000) { - displayStatistics(); - lastStatsDisplay = millis(); - } - - delay(50); -} -void initializeLoRa() { - Serial.println("Initializing LoRa..."); - - SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_SS); - LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0); - - if (!LoRa.begin(LORA_BAND)) { // Now 433E6 - Serial.println("ERROR: LoRa failed!"); - loraReady = false; - return; - } - - LoRa.setTxPower(20); - LoRa.setSpreadingFactor(12); - LoRa.setSignalBandwidth(125E3); - LoRa.setCodingRate4(8); - LoRa.setPreambleLength(8); - LoRa.setSyncWord(0x34); - - // ADD these lines for better 433 MHz performance: - LoRa.enableCrc(); - LoRa.setOCP(240); - - Serial.println("LoRa OK\n"); - loraReady = true; -} - -void initializeWiFi() { - Serial.print("Connecting to WiFi"); - WiFi.mode(WIFI_STA); - WiFi.begin(WIFI_SSID, WIFI_PASSWORD); - - int attempts = 0; - while (WiFi.status() != WL_CONNECTED && attempts < 30) { - delay(500); - Serial.print("."); - attempts++; - } - - if (WiFi.status() == WL_CONNECTED) { - wifiConnected = true; - Serial.println("\nWiFi OK"); - Serial.println("IP: " + WiFi.localIP().toString() + "\n"); - } else { - wifiConnected = false; - Serial.println("\nWiFi FAILED\n"); - } -} - -void checkWiFiConnection() { - if (WiFi.status() != WL_CONNECTED && wifiConnected) { - Serial.println("WiFi lost. Reconnecting..."); - wifiConnected = false; - supabaseReady = false; - initializeWiFi(); - - if (wifiConnected && !supabaseReady) { - initializeSupabase(); - } - } -} - -void initializeNTP() { - if (!wifiConnected) return; - - Serial.println("Syncing NTP with multiple servers..."); - - struct tm timeinfo; - bool syncSuccess = false; - - // Try each NTP server until one works - for (int serverIndex = 0; serverIndex < ntpServerCount && !syncSuccess; serverIndex++) { - Serial.print(" Trying "); - Serial.print(ntpServers[serverIndex]); - Serial.print("... "); - - configTime(gmtOffset_sec, daylightOffset_sec, ntpServers[serverIndex]); - - int attempts = 0; - while (!getLocalTime(&timeinfo) && attempts < 10) { - delay(500); - attempts++; - } - - if (attempts < 10) { - syncSuccess = true; - ntpSynced = true; - Serial.println("✓ Success!"); - Serial.print(" Current time: "); - Serial.println(getCurrentDateTime()); - break; - } else { - Serial.println("✗ Failed"); - } - } - - if (!syncSuccess) { - Serial.println("❌ All NTP servers failed"); - ntpSynced = false; - } else { - Serial.println("NTP sync complete\n"); - } -} - -void initializeSupabase() { - if (!wifiConnected) return; - - Serial.println("Testing Supabase connection..."); - client.setInsecure(); - - String testUrl = String(SUPABASE_URL) + "/rest/v1/"; - http.begin(client, testUrl); - http.addHeader("Content-Type", "application/json"); - http.addHeader("apikey", SUPABASE_ANON_KEY); - - int httpResponseCode = http.GET(); - - if (httpResponseCode > 0) { - Serial.println("Supabase OK\n"); - supabaseReady = true; - } else { - Serial.println("Supabase FAILED\n"); - supabaseReady = false; - } - - http.end(); -} - -void handleLoRaPackets() { - int packetSize = LoRa.parsePacket(); - if (packetSize == 0) return; - - String receivedPacket = ""; - while (LoRa.available()) { - char c = (char)LoRa.read(); - if (c >= 20 && c <= 126) { - receivedPacket += c; - } - } - - if (receivedPacket.length() < 5) return; - - int rssi = LoRa.packetRssi(); - float snr = LoRa.packetSnr(); - - packetsReceived++; - - Serial.println("========================"); - Serial.println("LoRa Packet #" + String(packetsReceived)); - Serial.println("RSSI: " + String(rssi) + " | SNR: " + String(snr, 1)); - Serial.println("Size: " + String(receivedPacket.length()) + " bytes"); - - String cleanedPacket = cleanJsonPacket(receivedPacket); - if (cleanedPacket.length() > 0) { - processAndUploadPacket(cleanedPacket, rssi, snr); - } else { - packetsCorrupted++; - Serial.println("ERROR: Packet cleanup failed"); - } - Serial.println("========================\n"); -} - -String cleanJsonPacket(String rawPacket) { - DynamicJsonDocument testDoc(512); - if (deserializeJson(testDoc, rawPacket) == DeserializationError::Ok) { - return rawPacket; - } - - String cleaned = rawPacket; - - if (cleaned.endsWith(",p")) { - cleaned = cleaned.substring(0, cleaned.length() - 2) + "od\"}"; - } else if (cleaned.endsWith("Go,p")) { - cleaned = cleaned.substring(0, cleaned.length() - 4) + "Good\"}"; - } - - int openBraces = 0, closeBraces = 0; - for (char c : cleaned) { - if (c == '{') openBraces++; - if (c == '}') closeBraces++; - } - - while (closeBraces < openBraces) { - cleaned += "}"; - closeBraces++; - } - - int quotes = 0; - for (char c : cleaned) { - if (c == '"') quotes++; - } - - if (quotes % 2 != 0) cleaned += "\""; - - return cleaned; -} - -// Get or create packet count for a node -unsigned long getNodePacketCount(String nodeId) { - // Find existing node - for (int i = 0; i < trackedNodesCount; i++) { - if (nodeTrackers[i].nodeId == nodeId) { - nodeTrackers[i].packetCount++; - nodeTrackers[i].lastSeenTime = millis(); - return nodeTrackers[i].packetCount; - } - } - - // Node not found, add new one - if (trackedNodesCount < MAX_TRACKED_NODES) { - nodeTrackers[trackedNodesCount].nodeId = nodeId; - nodeTrackers[trackedNodesCount].packetCount = 1; - nodeTrackers[trackedNodesCount].lastSeenTime = millis(); - trackedNodesCount++; - return 1; - } - - // Buffer full, replace oldest entry - int oldestIndex = 0; - unsigned long oldestTime = nodeTrackers[0].lastSeenTime; - for (int i = 1; i < MAX_TRACKED_NODES; i++) { - if (nodeTrackers[i].lastSeenTime < oldestTime) { - oldestTime = nodeTrackers[i].lastSeenTime; - oldestIndex = i; - } - } - - nodeTrackers[oldestIndex].nodeId = nodeId; - nodeTrackers[oldestIndex].packetCount = 1; - nodeTrackers[oldestIndex].lastSeenTime = millis(); - return 1; -} - -// Determine air quality based on comprehensive sensor analysis -String determineAirQuality(int mq2, int mq9, int mq135, bool mq2Digital, bool mq9Digital, bool mq135Digital) { - /* - * COMPREHENSIVE AIR QUALITY ANALYSIS - * - * MQ2: Detects smoke, LPG, propane, methane, hydrogen - * Analog: 0-1023 (higher = more gas) - * Digital: true = gas detected above threshold - * - * MQ9: Detects carbon monoxide (CO) and combustible gases - * Analog: 0-1023 (higher = more CO) - * Digital: true = dangerous CO levels - * - * MQ135: General air quality - NH3, NOx, alcohol, benzene, smoke, CO2 - * Analog: 0-1023 (higher = worse air quality) - * Digital: true = poor air quality detected - * - * THRESHOLDS (calibrated for mining environments): - * - Safe: < 300 analog, no digital triggers - * - Fair: 300-400 analog, no digital triggers - * - Moderate: 400-500 analog, or 1 digital trigger - * - Bad: 500-650 analog, or 2 digital triggers - * - Danger: > 650 analog, or all 3 digital triggers - */ - - // Count digital triggers (immediate danger indicators) - int digitalAlerts = 0; - if (mq2Digital) digitalAlerts++; - if (mq9Digital) digitalAlerts++; - if (mq135Digital) digitalAlerts++; - - // CRITICAL: If all 3 digital sensors triggered = IMMEDIATE DANGER - if (digitalAlerts >= 3) { - return "DANGER"; - } - - // Analyze analog readings with weighted scoring - int dangerScore = 0; - int badScore = 0; - int moderateScore = 0; - int fairScore = 0; - - // MQ2 Analysis (Smoke/Flammable Gas) - HIGHEST PRIORITY in mines - if (mq2 > 700 || mq2Digital) { - dangerScore += 3; // Critical: explosion risk - } else if (mq2 > 550) { - badScore += 2; - } else if (mq2 > 320) { - moderateScore += 2; - } else if (mq2 > 100) { - fairScore += 1; - } - - // MQ9 Analysis (Carbon Monoxide) - HIGH PRIORITY (silent killer) - if (mq9 > 4000 || mq9Digital) { - dangerScore += 3; // Critical: CO poisoning risk - } else if (mq9 > 3200) { - badScore += 2; - } else if (mq9 > 2400) { - moderateScore += 2; - } else if (mq9 > 1600) { - fairScore += 1; - } - - // MQ135 Analysis (General Air Quality) - MEDIUM PRIORITY - if (mq135 > 2200 || mq135Digital) { - dangerScore += 2; // Critical: toxic air - } else if (mq135 > 1800) { - badScore += 2; - } else if (mq135 > 1400) { - moderateScore += 1; - } else if (mq135 > 1000) { - fairScore += 1; - } - - // Additional checks for digital alerts - if (digitalAlerts >= 2) { - dangerScore += 2; // Two sensors in digital alert = very dangerous - } else if (digitalAlerts == 1) { - badScore += 1; - } - - // DECISION LOGIC (prioritize worst conditions) - if (dangerScore >= 4) { - return "DANGER"; - } else if (dangerScore >= 2 || badScore >= 4) { - return "BAD"; - } else if (dangerScore >= 1 || badScore >= 2 || moderateScore >= 3) { - return "MODERATE"; - } else if (moderateScore >= 1 || fairScore >= 2) { - return "FAIR"; - } else { - return "GOOD"; - } -} - -void processAndUploadPacket(String cleanedPacket, int rssi, float snr) { - DynamicJsonDocument receivedDoc(900); - - DeserializationError error = deserializeJson(receivedDoc, cleanedPacket); - if (error) { - packetsCorrupted++; - Serial.println("ERROR: JSON parse failed: " + String(error.c_str())); - Serial.println("Raw packet: " + cleanedPacket); - return; - } - - // DEBUG: Print entire received JSON - if (DEBUG_MODE) { - Serial.println("\n=== RECEIVED JSON DEBUG ==="); - serializeJsonPretty(receivedDoc, Serial); - Serial.println("\n==========================="); - } - - // CRITICAL FIX: Check for emergency field more robustly - bool isEmergency = false; - - // Method 1: Check if field exists and is true - if (receivedDoc.containsKey("emergency")) { - JsonVariant emergencyVar = receivedDoc["emergency"]; - - if (emergencyVar.is()) { - isEmergency = emergencyVar.as(); - } else if (emergencyVar.is()) { - isEmergency = (emergencyVar.as() != 0); - } else if (emergencyVar.is()) { - String emergencyStr = emergencyVar.as(); - emergencyStr.toLowerCase(); - isEmergency = (emergencyStr == "true" || emergencyStr == "1"); - } - - Serial.println("Emergency field found: " + String(isEmergency ? "TRUE" : "FALSE")); - } else { - Serial.println("WARNING: No 'emergency' field in JSON"); - } - - String nodeId = receivedDoc["node"] | "UNKNOWN"; -unsigned long sensorPacketCount = getNodePacketCount(nodeId); - -// ⭐ ADD THESE 3 LINES HERE ⭐ -bool mq2Digital = receivedDoc["mq2_digital"] | false; -bool mq9Digital = receivedDoc["mq9_digital"] | false; -bool mq135Digital = receivedDoc["mq135_digital"] | false; - - - - // Handle emergency - if (isEmergency) { - emergenciesDetected++; - - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.println("║ 🚨 EMERGENCY SIGNAL RECEIVED! 🚨 ║"); - Serial.println("║ Node: " + nodeId + String(35 - nodeId.length(), ' ') + "║"); - Serial.println("║ Emergency Count: " + String(emergenciesDetected) + String(27 - String(emergenciesDetected).length(), ' ') + "║"); - Serial.println("╚════════════════════════════════════════════╝\n"); - - if (canSendEmergencyEmail(nodeId)) { - Serial.println(">>> SENDING EMERGENCY EMAIL <<<"); - sendEmergencyEmail(receivedDoc, rssi, snr); - recordEmergency(nodeId); - } else { - unsigned long timeSinceLastEmail = 0; - for (int i = 0; i < EMAIL_BUFFER_SIZE; i++) { - if (emergencyBuffer[i].nodeId == nodeId) { - timeSinceLastEmail = (millis() - emergencyBuffer[i].lastAlertTime) / 1000; - break; - } - } - Serial.println("⏱ Rate limit active for node " + nodeId); - Serial.println(" Time since last email: " + String(timeSinceLastEmail) + "s"); - Serial.println(" Cooldown period: " + String(EMAIL_SEND_TIMEOUT/1000) + "s"); - Serial.println(" Email skipped\n"); - } - } else { - if (DEBUG_MODE) { - Serial.println("📊 Normal packet (non-emergency) from node " + nodeId); - } - } - - // Build upload document - DynamicJsonDocument uploadDoc(1600); - uploadDoc["sensor_node_id"] = nodeId; - uploadDoc["sensor_packet_count"] = sensorPacketCount; - uploadDoc["sensor_timestamp"] = receivedDoc["timestamp"] | 0; - uploadDoc["temperature"] = receivedDoc["temp"]; - uploadDoc["humidity"] = receivedDoc["hum"] | 0; - uploadDoc["mq2_analog"] = receivedDoc["mq2"] | 0; - uploadDoc["mq9_analog"] = receivedDoc["mq9"] | 0; - uploadDoc["mq135_analog"] = receivedDoc["mq135"] | 0; - uploadDoc["mq2_digital"] = mq2Digital; -uploadDoc["mq9_digital"] = mq9Digital; -uploadDoc["mq135_digital"] = mq135Digital; - -// Calculate air quality with comprehensive logic -int mq2 = receivedDoc["mq2"] | 0; -int mq9 = receivedDoc["mq9"] | 0; -int mq135 = receivedDoc["mq135"] | 0; -String airQuality = determineAirQuality(mq2, mq9, mq135, mq2Digital, mq9Digital, mq135Digital); -uploadDoc["air_quality"] = airQuality; - -// Log air quality warnings -if (airQuality == "DANGER") { - Serial.println("🚨 CRITICAL AIR QUALITY: DANGER"); -} else if (airQuality == "BAD") { - Serial.println("⚠️ WARNING: BAD air quality detected"); -} else if (airQuality == "MODERATE") { - Serial.println("⚡ CAUTION: MODERATE air quality"); -} - // Use explicit float conversion - float motionAccel = receivedDoc["motion_accel"].as(); - float motionGyro = receivedDoc["motion_gyro"].as(); - - // Safety check - if (isnan(motionAccel)) motionAccel = 0.0f; - if (isnan(motionGyro)) motionGyro = 0.0f; - - uploadDoc["motion_accel"] = motionAccel; - uploadDoc["motion_gyro"] = motionGyro; - uploadDoc["bpm"] = receivedDoc["bpm"] | 0; - uploadDoc["spo2"] = receivedDoc["spo2"] | 0; - uploadDoc["body_temp"] = receivedDoc["body_temp"] | 0.0f; - uploadDoc["wristband_connected"] = receivedDoc["wristband_connected"] | 0; - uploadDoc["emergency"] = isEmergency; // Add emergency field to database - uploadDoc["central_node_id"] = CENTRAL_NODE_ID; - uploadDoc["received_time"] = getCurrentDateTime(); - uploadDoc["received_timestamp"] = millis(); - uploadDoc["rssi"] = rssi; - uploadDoc["snr"] = snr; - uploadDoc["gateway_packet_count"] = packetsReceived; - - String jsonString; - serializeJson(uploadDoc, jsonString); - - if (DEBUG_MODE || isEmergency) { - Serial.println("Upload Payload: " + jsonString); - } - - if (supabaseReady) { - uploadToSupabase(jsonString); - } else { - Serial.println("WARNING: Supabase not ready - data not uploaded"); - } -} - -bool canSendEmergencyEmail(String nodeId) { - unsigned long now = millis(); - - for (int i = 0; i < EMAIL_BUFFER_SIZE; i++) { - if (emergencyBuffer[i].nodeId == nodeId) { - if (now - emergencyBuffer[i].lastAlertTime >= EMAIL_SEND_TIMEOUT) { - return true; - } else { - return false; - } - } - } - - return true; -} - -void recordEmergency(String nodeId) { - unsigned long now = millis(); - - for (int i = 0; i < EMAIL_BUFFER_SIZE; i++) { - if (emergencyBuffer[i].nodeId == nodeId) { - emergencyBuffer[i].lastAlertTime = now; - return; - } - } - - if (emergencyBufferIndex >= EMAIL_BUFFER_SIZE) { - emergencyBufferIndex = 0; - } - - emergencyBuffer[emergencyBufferIndex].nodeId = nodeId; - emergencyBuffer[emergencyBufferIndex].lastAlertTime = now; - emergencyBufferIndex++; -} - -// * Send a text message to wristband via Edge Node relay -// * Central → (LoRa) → Edge → (ESP-NOW) → Wristband -// * -// * @param message The text message to send (max 120 characters recommended) -// * @return true if LoRa transmission succeeded, false otherwise -// */ -bool sendMessageToWristband(String message) { - if (!loraReady) { - Serial.println("❌ ERROR: LoRa not ready - cannot send message"); - return false; - } - - if (message.length() == 0) { - Serial.println("❌ ERROR: Empty message - not sending"); - return false; - } - - if (message.length() > 120) { - Serial.println("⚠️ WARNING: Message too long, truncating to 120 chars"); - message = message.substring(0, 120); - } - - // Create JSON packet with "message" field - // Edge node's receiveLoRaMessages() already looks for this field - DynamicJsonDocument doc(256); - doc["message"] = message; - doc["timestamp"] = millis(); - doc["from"] = "central_node"; - - String payload; - serializeJson(doc, payload); - - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.println("║ SENDING MESSAGE TO WRISTBAND VIA EDGE ║"); - Serial.println("╚════════════════════════════════════════════╝"); - Serial.println("Message: " + message); - Serial.println("Payload: " + payload); - Serial.println("Length: " + String(payload.length()) + " bytes"); - - // Send via LoRa - LoRa.beginPacket(); - LoRa.print(payload); - LoRa.endPacket(); - - messagesSentToEdge++; - - Serial.println("✓ Message transmitted via LoRa (#" + String(messagesSentToEdge) + ")"); - Serial.println(" → Edge node will forward to wristband via ESP-NOW"); - Serial.println("════════════════════════════════════════════\n"); - - return true; -} - -void handleSendMessage() { - // Handle CORS preflight (OPTIONS request from browser) - if (server.method() == HTTP_OPTIONS) { - server.sendHeader("Access-Control-Allow-Origin", "*"); - server.sendHeader("Access-Control-Allow-Methods", "POST, OPTIONS"); - server.sendHeader("Access-Control-Allow-Headers", "Content-Type"); - server.send(204); - return; - } - - // Add CORS header to all responses - server.sendHeader("Access-Control-Allow-Origin", "*"); - - if (server.method() != HTTP_POST) { - server.send(405, "application/json", "{\"error\":\"Method not allowed\"}"); - return; - } - - String body = server.arg("plain"); // Raw POST body - if (body.length() == 0) { - server.send(400, "application/json", "{\"error\":\"Empty body\"}"); - return; - } - - Serial.println("\n>>> HTTP REQUEST RECEIVED: POST /send <<<"); - Serial.println("Body: " + body); - - DynamicJsonDocument doc(256); - DeserializationError error = deserializeJson(doc, body); - if (error) { - server.send(400, "application/json", "{\"error\":\"Invalid JSON\"}"); - return; - } - - // Extract message (and optionally miner_id for logging) - String message = doc["message"] | ""; - String minerId = doc["miner_id"] | "unknown"; - - if (message.length() == 0) { - server.send(400, "application/json", "{\"error\":\"Missing message field\"}"); - return; - } - - Serial.println("Miner ID: " + minerId); - Serial.println("Message: " + message); - - // Send to wristband via existing function - bool success = sendMessageToWristband(message); - - if (success) { - String response = "{\"success\":true,\"message\":\"Delivered to LoRa\",\"miner_id\":\"" + minerId + "\"}"; - server.send(200, "application/json", response); - Serial.println("✓ HTTP response: 200 OK"); - } else { - server.send(500, "application/json", "{\"error\":\"LoRa transmission failed\"}"); - Serial.println("✗ HTTP response: 500 Error"); - } -} -/** - * Check for serial commands to send messages - * Commands: - * msg - Send message to wristband - * testmsg - Send a test message - * msgstats - Show message statistics - */ -void handleMessageCommands() { - if (Serial.available() > 0) { - String command = Serial.readStringUntil('\n'); - command.trim(); - - if (command.length() == 0) return; - - // Convert to lowercase for comparison - String cmdLower = command; - cmdLower.toLowerCase(); - - if (cmdLower.startsWith("msg ")) { - // Extract message after "msg " - String message = command.substring(4); - message.trim(); - - if (message.length() > 0) { - Serial.println("\n>>> MANUAL MESSAGE COMMAND RECEIVED <<<"); - sendMessageToWristband(message); - } else { - Serial.println("ERROR: No message text provided"); - Serial.println("Usage: msg "); - } - } - else if (cmdLower == "testmsg") { - Serial.println("\n>>> TEST MESSAGE COMMAND <<<"); - sendMessageToWristband("This is a test message from central node"); - } - else if (cmdLower == "msgstats") { - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.println("║ MESSAGE RELAY STATISTICS ║"); - Serial.println("╚════════════════════════════════════════════╝"); - Serial.println("Messages sent to edge node: " + String(messagesSentToEdge)); - Serial.println("LoRa status: " + String(loraReady ? "✓ Ready" : "✗ Not Ready")); - Serial.println("════════════════════════════════════════════\n"); - } - else if (cmdLower == "help" || cmdLower == "commands") { - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.println("║ MESSAGE RELAY COMMANDS ║"); - Serial.println("╠════════════════════════════════════════════╣"); - Serial.println("║ msg - Send message to wristband ║"); - Serial.println("║ testmsg - Send test message ║"); - Serial.println("║ msgstats - Show message statistics ║"); - Serial.println("║ help - Show this menu ║"); - Serial.println("╚════════════════════════════════════════════╝\n"); - } - } -} - - -void sendEmergencyEmail(JsonDocument& sensorData, int rssi, float snr) { - if (!wifiConnected) { - Serial.println("❌ ERROR: WiFi disconnected - cannot send email"); - return; - } - - Serial.println("\n╔══════════════════════════════════════╗"); - Serial.println("║ PREPARING EMERGENCY EMAIL ║"); - Serial.println("╚══════════════════════════════════════╝\n"); - - String nodeId = sensorData["node"] | "UNKNOWN"; - float temperature = sensorData["temp"] | 0; - float humidity = sensorData["hum"] | 0; - int mq2 = sensorData["mq2"] | 0; - int mq9 = sensorData["mq9"] | 0; - int mq135 = sensorData["mq135"] | 0; - bool mq2Digital = sensorData["mq2_digital"] | false; // ✅ CORRECT - bool mq9Digital = sensorData["mq9_digital"] | false; // ✅ CORRECT - bool mq135Digital = sensorData["mq135_digital"] | false; // ✅ CORRECT - int bpm = sensorData["bpm"] | 0; - int spo2 = sensorData["spo2"] | 0; - bool wristbandConnected = sensorData["wristband_connected"] | 0; - float motionAccel = sensorData["motion_accel"] | 0; - float motionGyro = sensorData["motion_gyro"] | 0; - - String subject = "🚨 URGENT: Mine Worker Emergency - Node " + nodeId; - - String emailBody = "╔═══════════════════════════════════════════╗\n"; - emailBody += "║ EMERGENCY DISTRESS SIGNAL DETECTED ║\n"; - emailBody += "╚═══════════════════════════════════════════╝\n\n"; - emailBody += "Node ID: " + nodeId + "\n"; - emailBody += "Timestamp: " + getCurrentDateTime() + "\n"; - emailBody += "Emergency Count: #" + String(emergenciesDetected) + "\n\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "ENVIRONMENTAL CONDITIONS:\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "Temperature: " + String(temperature, 1) + "°C\n"; - emailBody += "Humidity: " + String(humidity, 1) + "%\n"; - emailBody += "MQ2 (Smoke): " + String(mq2) + "\n"; - emailBody += "MQ9 (CO): " + String(mq9) + "\n"; - emailBody += "MQ135 (Quality): " + String(mq135) + "\n\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "MOTION & VITALS:\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "Acceleration: " + String(motionAccel, 2) + " m/s²\n"; - emailBody += "Gyro Rotation: " + String(motionGyro, 2) + " °/s\n"; - emailBody += "Heart Rate: " + String(bpm) + " BPM\n"; - emailBody += "Blood Oxygen: " + String(spo2) + "%\n"; - emailBody += "Wristband: " + String(wristbandConnected ? "✓ Connected" : "✗ Disconnected") + "\n\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "SIGNAL QUALITY:\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "RSSI: " + String(rssi) + " dBm\n"; - emailBody += "SNR: " + String(snr) + " dB\n\n"; - emailBody += "⚠️ IMMEDIATE ACTION REQUIRED!\n"; - emailBody += " Contact emergency response team.\n"; - - Serial.println("Email Subject: " + subject); - Serial.println("Email Length: " + String(emailBody.length()) + " characters"); - Serial.println(); - - // Try to send email with retries - for (int attempt = 1; attempt <= 3; attempt++) { - Serial.println("📧 Email Send Attempt " + String(attempt) + "/3..."); - - if (sendSMTPEmail(subject, emailBody)) { - emailsSent++; - Serial.println("\n✅ Email sent successfully!"); - Serial.println(" Total emails sent: " + String(emailsSent)); - Serial.println("╚══════════════════════════════════════╝\n"); - return; - } - - if (attempt < 3) { - Serial.println("❌ Attempt " + String(attempt) + " failed. Retrying in 5 seconds...\n"); - delay(5000); - } - } - - Serial.println("\n❌ ERROR: Email send failed after 3 attempts"); - Serial.println(" Check:"); - Serial.println(" 1. WiFi connection"); - Serial.println(" 2. SMTP credentials"); - Serial.println(" 3. Gmail app password"); - Serial.println(" 4. Internet connectivity"); - Serial.println("╚══════════════════════════════════════╝\n"); -} - -String readSMTPResponse(WiFiClient& client, int timeout) { - unsigned long start = millis(); - String response = ""; - - while (millis() - start < timeout) { - if (client.available()) { - char c = client.read(); - response += c; - if (c == '\n') { - Serial.print("SMTP: "); - Serial.print(response); - if (response.indexOf("250") >= 0 || response.indexOf("334") >= 0 || - response.indexOf("235") >= 0 || response.indexOf("220") >= 0 || - response.indexOf("354") >= 0) { - return response; - } - if (response.indexOf("5") == 0) { // Error codes start with 5 - Serial.println("SMTP ERROR: " + response); - return response; - } - response = ""; - } - } - delay(10); - } - - Serial.println("SMTP Timeout"); - return ""; -} - -bool sendSMTPEmail(String subject, String body) { - // Use secure client from the start and connect to port 465 (SSL/TLS) - // Gmail supports both 587 (STARTTLS) and 465 (direct SSL) - // Port 465 is simpler as it's SSL from the start - - WiFiClientSecure secureClient; - secureClient.setInsecure(); - - Serial.println("Connecting to SMTP server (SSL)..."); - if (!secureClient.connect(SMTP_SERVER, 465)) { // Use port 465 for direct SSL - Serial.println("ERROR: Cannot connect to SMTP server"); - return false; - } - Serial.println("Connected to SMTP server via SSL"); - - // Wait for greeting - String response = readSMTPResponse(secureClient, 10000); - if (response.indexOf("220") < 0) { - Serial.println("ERROR: No greeting from server"); - secureClient.stop(); - return false; - } - - // EHLO - secureClient.println("EHLO ESP32"); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("250") < 0) { - Serial.println("ERROR: EHLO failed"); - secureClient.stop(); - return false; - } - - // Clear any remaining response lines - delay(500); - while (secureClient.available()) secureClient.read(); - - // AUTH LOGIN - secureClient.println("AUTH LOGIN"); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("334") < 0) { - Serial.println("ERROR: AUTH LOGIN failed"); - secureClient.stop(); - return false; - } - - // Username - secureClient.println(base64Encode(SENDER_EMAIL)); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("334") < 0) { - Serial.println("ERROR: Username rejected"); - secureClient.stop(); - return false; - } - - // Password - secureClient.println(base64Encode(SENDER_PASSWORD)); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("235") < 0) { - Serial.println("ERROR: Authentication failed - Check app password"); - secureClient.stop(); - return false; - } - Serial.println("Authentication successful"); - - // MAIL FROM - secureClient.print("MAIL FROM:<"); - secureClient.print(SENDER_EMAIL); - secureClient.println(">"); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("250") < 0) { - Serial.println("ERROR: MAIL FROM rejected"); - secureClient.stop(); - return false; - } - - // RCPT TO - secureClient.print("RCPT TO:<"); - secureClient.print(SUPERVISOR_EMAIL); - secureClient.println(">"); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("250") < 0) { - Serial.println("ERROR: RCPT TO rejected"); - secureClient.stop(); - return false; - } - - // DATA - secureClient.println("DATA"); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("354") < 0) { - Serial.println("ERROR: DATA command rejected"); - secureClient.stop(); - return false; - } - - // Email headers and body - secureClient.print("From: "); - secureClient.println(SENDER_EMAIL); - secureClient.print("To: "); - secureClient.println(SUPERVISOR_EMAIL); - secureClient.print("Subject: "); - secureClient.println(subject); - secureClient.println(); - secureClient.println(body); - secureClient.println("."); - - response = readSMTPResponse(secureClient, 10000); - if (response.indexOf("250") < 0) { - Serial.println("ERROR: Message rejected"); - secureClient.stop(); - return false; - } - - // QUIT - secureClient.println("QUIT"); - secureClient.stop(); - - Serial.println("Email sent successfully!"); - return true; -} - -String base64Encode(String input) { - const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - String result = ""; - int val = 0, valb = 0; - - for (byte c : input) { - val = (val << 8) + c; - valb += 8; - while (valb >= 6) { - valb -= 6; - result += base64_chars[(val >> valb) & 0x3F]; - } - } - - if (valb > 0) result += base64_chars[(val << (6 - valb)) & 0x3F]; - while (result.length() % 4) result += "="; - return result; -} - -void uploadToSupabase(String jsonData) { - if (!supabaseReady) return; - - String url = String(SUPABASE_URL) + "/rest/v1/sensor_data"; - - http.begin(client, url); - http.addHeader("Content-Type", "application/json"); - http.addHeader("apikey", SUPABASE_ANON_KEY); - http.addHeader("Authorization", "Bearer " + String(SUPABASE_ANON_KEY)); - - int httpResponseCode = http.POST(jsonData); - - if (httpResponseCode == 201 || httpResponseCode == 200) { - packetsUploaded++; - Serial.println("✓ Uploaded to Supabase\n"); - } else { - Serial.println("✗ Upload failed: " + String(httpResponseCode)); - String response = http.getString(); - Serial.println("Response: " + response + "\n"); - } - - http.end(); -} - -String getCurrentDateTime() { - if (!ntpSynced) { - return String(millis()/1000) + "s"; - } - - struct tm timeinfo; - if (!getLocalTime(&timeinfo)) { - return "Error"; - } - - char timeString[64]; - strftime(timeString, sizeof(timeString), "%Y-%m-%dT%H:%M:%S+05:30", &timeinfo); - return String(timeString); -} - -void displayStatistics() { - Serial.println("\n╔═══════════════════════════════════════╗"); - Serial.println("║ SYSTEM STATISTICS ║"); - Serial.println("╠═══════════════════════════════════════╣"); - Serial.println("║ Component Status: ║"); - Serial.println("║ WiFi: " + String(wifiConnected ? "✓ Connected " : "✗ Disconnected") + " ║"); - Serial.println("║ Supabase: " + String(supabaseReady ? "✓ Ready " : "✗ Not Ready ") + " ║"); - Serial.println("║ NTP: " + String(ntpSynced ? "✓ Synced " : "✗ Not Synced ") + " ║"); - Serial.println("║ LoRa: " + String(loraReady ? "✓ Active " : "✗ Offline ") + " ║"); - Serial.println("╠═══════════════════════════════════════╣"); - Serial.println("║ Packet Statistics: ║"); - Serial.println("║ Received: " + String(packetsReceived) + String(21 - String(packetsReceived).length(), ' ') + "║"); - Serial.println("║ Uploaded: " + String(packetsUploaded) + String(21 - String(packetsUploaded).length(), ' ') + "║"); - Serial.println("║ Corrupted: " + String(packetsCorrupted) + String(21 - String(packetsCorrupted).length(), ' ') + "║"); - - // ========== ADD THESE LINES HERE ========== - Serial.println("╠═══════════════════════════════════════╣"); - Serial.println("║ Message Relay Statistics: ║"); - Serial.println("║ Messages Sent: " + String(messagesSentToEdge) + String(17 - String(messagesSentToEdge).length(), ' ') + "║"); - // ========== END OF NEW LINES ========== - - Serial.println("╠═══════════════════════════════════════╣"); - Serial.println("║ Emergency Statistics: ║"); - Serial.println("║ 🚨 Emergencies: " + String(emergenciesDetected) + String(19 - String(emergenciesDetected).length(), ' ') + "║"); - Serial.println("║ 📧 Emails Sent: " + String(emailsSent) + String(19 - String(emailsSent).length(), ' ') + "║"); -Serial.println("╠═══════════════════════════════════════╣"); -Serial.println("║ Active Nodes (Packet Counts): ║"); -if (trackedNodesCount == 0) { - Serial.println("║ No nodes tracked yet ║"); -} else { - for (int i = 0; i < trackedNodesCount; i++) { - String nodeInfo = "║ " + nodeTrackers[i].nodeId + ": " + String(nodeTrackers[i].packetCount) + " pkts"; - int padding = 40 - nodeInfo.length(); - Serial.println(nodeInfo + String(padding, ' ') + "║"); - } -} -Serial.println("╚═══════════════════════════════════════╝\n"); - - Serial.println("Current Time: " + getCurrentDateTime()); - Serial.println(); -} diff --git a/Edge_Node_ESPNOW_Working.ino b/Edge_Node_ESPNOW_Working.ino deleted file mode 100644 index 7330011..0000000 --- a/Edge_Node_ESPNOW_Working.ino +++ /dev/null @@ -1,1163 +0,0 @@ -/* - edge_node_espnow.ino - Edge Node with ESP-NOW integration for wristband vitals & text relay. - Full, self-contained sketch. Make sure to copy the entire file into Arduino IDE, - no extra lines above the first #include. -*/ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// ========================= MPU6050 I2C Register Definitions ========================= -#define MPU6050_ADDR 0x68 -#define PWR_MGMT_1 0x6B -#define ACCEL_XOUT_H 0x3B -#define GYRO_XOUT_H 0x43 -#define CONFIG 0x1A -#define GYRO_CONFIG 0x1B -#define ACCEL_CONFIG 0x1C -#define WHO_AM_I 0x75 - -// ========================= Pin definitions (preserved) ========================= -// MQ sensors -#define MQ2_DIGITAL_PIN 27 -#define MQ2_ANALOG_PIN 32 -#define MQ9_DIGITAL_PIN 14 -#define MQ9_ANALOG_PIN 33 -#define MQ135_DIGITAL_PIN 13 -#define MQ135_ANALOG_PIN 35 - -// FN-M16P Audio Module pins (UART2) -#define FN_M16P_RX 16 -#define FN_M16P_TX 17 - -// DHT11 pin -#define DHT11_PIN 25 -#define DHT_TYPE DHT11 - -// MPU6050 pins (I2C) -#define MPU6050_SDA 21 -#define MPU6050_SCL 22 -#define MPU6050_INT 34 - -// LoRa Module pins (SPI) -#define LORA_SCK 18 -#define LORA_MISO 19 -#define LORA_MOSI 23 -#define LORA_SS 5 -#define LORA_RST 4 -#define LORA_DIO0 26 - -// EMERGENCY BUTTON PIN -#define EMERGENCY_BUTTON_PIN 15 - -// LoRa frequency -#define LORA_BAND 915E6 - -// ========================= System constants ========================= -#define NODE_ID "001" - -#define MQ2_DANGER_THRESHOLD 1600 -#define MQ9_DANGER_THRESHOLD 3800 -#define MQ135_DANGER_THRESHOLD 1800 - -#define FALLBACK_TEMPERATURE 27.0 -#define FALLBACK_HUMIDITY 47.0 - -#define CALIBRATION_SAMPLES 10 -#define DANGER_MULTIPLIER 2.0 -#define CALIBRATION_DELAY 2000 - -const int TAP_TIMEOUT = 600; -const int REQUIRED_TAPS = 3; - -const byte FRAME_START = 0x7E; -const byte FRAME_END = 0xEF; -const byte VERSION = 0xFF; -const byte NO_FEEDBACK = 0x00; -const byte WITH_FEEDBACK = 0x01; -const byte CMD_PLAY_TRACK = 0x03; -const byte CMD_VOLUME = 0x06; -const byte CMD_STOP = 0x16; - -// Audio files mapping -enum AudioFiles { - BOOT_AUDIO = 1, - SMOKE_ALERT = 2, - CO_ALERT = 3, - AIR_QUALITY_WARNING = 4, - HIGH_TEMP_ALERT = 5, - LOW_TEMP_ALERT = 6, - HIGH_HUMIDITY_ALERT = 7, - LOW_HUMIDITY_ALERT = 8, - FALL_DETECTED = 9, - MOTION_ALERT = 10 -}; - -// ========================= ESP-NOW message types ========================= -#define MSG_TYPE_VITALS 0x01 -#define MSG_TYPE_TEXT 0x02 -#define MSG_TYPE_ACK 0x03 - -#define MAX_TEXT_LEN 128 - -typedef struct __attribute__((packed)) { - uint8_t msgType; // MSG_TYPE_VITALS - uint8_t bpm; // 0..255 - uint8_t spo2; // 0..100 - uint8_t finger; // 0/1 - uint32_t timestamp; -} espnow_vitals_t; - -typedef struct __attribute__((packed)) { - uint8_t msgType; // MSG_TYPE_TEXT - uint32_t messageId; - uint8_t length; - char text[MAX_TEXT_LEN]; -} espnow_text_t; - -typedef struct __attribute__((packed)) { - uint8_t msgType; // MSG_TYPE_ACK - uint32_t messageId; - uint8_t success; // 0/1 -} espnow_ack_t; - -// ========================= Types & Globals ========================= -struct SensorCalibration { - float baseline; - float dangerThreshold; - bool calibrated; -}; - -struct MotionData { - float totalAccel; - float totalGyro; - bool fallDetected; - bool impactDetected; - bool motionDetected; - unsigned long lastMotionTime; -}; - -struct SensorData { - float temperature; - float humidity; - int mq2_analog; - int mq9_analog; - int mq135_analog; - bool mq2_digital; - bool mq9_digital; - bool mq135_digital; - bool emergency; - unsigned long timestamp; - MotionData motion; -}; - -SensorCalibration mq2_cal = {0,0,false}; -SensorCalibration mq9_cal = {0,0,false}; -SensorCalibration mq135_cal = {0,0,false}; - -DHT dht(DHT11_PIN, DHT_TYPE); -HardwareSerial fnM16pSerial(2); - -bool audioReady = false; -bool loraReady = false; -bool dhtReady = false; -bool mpuReady = false; - -unsigned long lastLoRaSend = 0; -int loraInterval = 30000; -int packetCount = 0; - -unsigned long lastDHTReading = 0; -const unsigned long DHT_READING_INTERVAL = 2000; -float lastValidTemperature = FALLBACK_TEMPERATURE; -float lastValidHumidity = FALLBACK_HUMIDITY; - -// Emergency Button Variables -volatile int tapCount = 0; -volatile unsigned long lastTapTime = 0; -volatile bool emergencyTriggered = false; - -// Motion variables -MotionData motionData = {0}; - -// MPU internals -const float G = 9.80665f; -const float FREE_FALL_G_THRESHOLD = 0.6f; -const unsigned long FREE_FALL_MIN_MS = 120; -const float IMPACT_G_THRESHOLD = 3.5f; -const unsigned long IMPACT_WINDOW_MS = 1200; -const unsigned long STATIONARY_CONFIRM_MS = 800; -const float ROTATION_IMPACT_THRESHOLD = 400.0f; - -bool inFreeFall = false; -bool fallInProgress = false; -bool impactSeen = false; -unsigned long freeFallStart = 0; -unsigned long fallStartTime = 0; -unsigned long impactTime = 0; -unsigned long stationarySince = 0; -float accelFiltered = G; -const float ALPHA = 0.85f; - -unsigned long lastI2CAttempt = 0; - -// ========================= ESP-NOW / Wristband status globals ========================= -struct WristbandStatus { - uint8_t bpm; - uint8_t spo2; - bool fingerDetected; - unsigned long lastUpdate; - bool connected; - uint32_t lastMessageId; - bool messageAcknowledged; -} wristbandStatus = {0,0,false,0,false,0,false}; - -// Wristband MAC address (update if different) -uint8_t wristbandMac[6] = {0x0C, 0x4E, 0xA0, 0x66, 0xB2, 0x78}; - -bool espnowReady = false; -esp_err_t lastEspNowSendStatus = ESP_OK; -uint32_t outgoingMessageCounter = 1; - -// ========================= Forward declarations ========================= -void printTestMenu(); -void handleTestCommand(String cmd); -void testDHT(); -void testMQ2(); -void testMQ9(); -void testMQ135(); -void testAllMQ(); -void testAudio(int fileNum); -void testLoRa(); -void testEmergency(); -void testButton(); -void testAllSensors(); -void printSystemStatus(); -void scanI2CDevices(); -void setVolume(int volume); -void playAudioFile(int fileNumber); -void stopAudio(); -void calibrateSensors(); -void calibrateSensor(int pin, SensorCalibration* cal, String sensorName, int minThreshold); -bool checkSensorDanger(int currentValue, SensorCalibration* cal, int staticDangerThreshold); -SensorData readAllSensors(); -void displayReadings(SensorData data); -void checkAlerts(SensorData data); -void sendLoRaData(SensorData data); -String getAirQualityRating(int value); -void checkEmergencyButton(); -void handleEmergency(); -void sendCommand(byte cmd, byte param1, byte param2, bool feedback); - -// I2C helpers & MPU -bool i2cBusRecover(); -bool safeWireRequest(uint8_t addr, uint8_t reg, uint8_t *buf, size_t len, int retries=3); -bool safeWireWrite(uint8_t addr, uint8_t reg, uint8_t val, int retries=3); -bool initMPU6050(); -void readMPU6050Data(int16_t *ax, int16_t *ay, int16_t *az, int16_t *gx, int16_t *gy, int16_t *gz); -void monitorMotion(); -void detectFallAndHandle(); - -// ESP-NOW -void initESPNOW(); -void onDataRecv(const esp_now_recv_info_t *recv_info, const uint8_t *data, int len); -void onDataSent(const wifi_tx_info_t *tx_info, esp_now_send_status_t status); -bool sendTextToWristband(const String &message); -void checkWristbandConnection(); -void receiveLoRaMessages(); - -// -------------------------- Implementations -------------------------- -// I2C recovery & safeWire -bool i2cBusRecover() { - Wire.end(); - delay(10); - pinMode(MPU6050_SCL, OUTPUT); - pinMode(MPU6050_SDA, INPUT_PULLUP); - if (digitalRead(MPU6050_SDA) == HIGH) { - Wire.begin(MPU6050_SDA, MPU6050_SCL); - Wire.setClock(100000); - return true; - } - for (int i=0;i<9;i++) { - digitalWrite(MPU6050_SCL, HIGH); - delayMicroseconds(500); - digitalWrite(MPU6050_SCL, LOW); - delayMicroseconds(500); - if (digitalRead(MPU6050_SDA) == HIGH) break; - } - pinMode(MPU6050_SDA, OUTPUT); - digitalWrite(MPU6050_SDA, LOW); - delayMicroseconds(200); - digitalWrite(MPU6050_SCL, HIGH); - delayMicroseconds(200); - digitalWrite(MPU6050_SDA, HIGH); - delayMicroseconds(200); - Wire.begin(MPU6050_SDA, MPU6050_SCL); - Wire.setClock(100000); - pinMode(MPU6050_SDA, INPUT_PULLUP); - pinMode(MPU6050_SCL, INPUT_PULLUP); - return digitalRead(MPU6050_SDA) == HIGH; -} - -bool safeWireRequest(uint8_t addr, uint8_t reg, uint8_t *buf, size_t len, int retries) { - for (int attempt=0; attempt 0.2f * G || motionData.totalGyro > 25.0f) { - motionData.lastMotionTime = millis(); - motionData.motionDetected = true; - } else motionData.motionDetected = false; - detectFallAndHandle(); -} - -void detectFallAndHandle() { - unsigned long now = millis(); - float totG = motionData.totalAccel / G; - float totGyro = motionData.totalGyro; - if (totG < FREE_FALL_G_THRESHOLD) { - if (!inFreeFall) { inFreeFall = true; freeFallStart = now; } - else if ((now - freeFallStart) >= FREE_FALL_MIN_MS && !fallInProgress) { - fallInProgress = true; - fallStartTime = now; - impactSeen = false; - motionData.impactDetected = false; - } - } else { if (inFreeFall) inFreeFall = false; } - if (fallInProgress && !impactSeen) { - if (totG >= IMPACT_G_THRESHOLD || totGyro >= ROTATION_IMPACT_THRESHOLD) { - impactSeen = true; impactTime = now; motionData.impactDetected = true; - } else if (now - fallStartTime > IMPACT_WINDOW_MS) { fallInProgress = false; impactSeen = false; motionData.impactDetected = false; } - } - if (impactSeen) { - float accelVariationG = fabs((motionData.totalAccel / G) - 1.0f); - if (accelVariationG < 0.35f && motionData.totalGyro < 50.0f) { - if (stationarySince == 0) stationarySince = now; - if (now - stationarySince >= STATIONARY_CONFIRM_MS) { - motionData.fallDetected = true; - emergencyTriggered = true; - if (audioReady) playAudioFile(FALL_DETECTED); - Serial.println("\n╔════════════════════════════════════╗"); - Serial.println("║ FALL CONFIRMED - IMPACT + STATIONARY ║"); - Serial.println("╚════════════════════════════════════╝"); - Serial.printf("Acceleration: %.2f g\n", motionData.totalAccel / G); - Serial.printf("Gyroscope: %.2f °/s\n", motionData.totalGyro); - fallInProgress = false; impactSeen = false; stationarySince = 0; - } - } else { - stationarySince = 0; - if (now - impactTime > IMPACT_WINDOW_MS) { fallInProgress = false; impactSeen = false; stationarySince = 0; motionData.impactDetected = false; } - } - } -} - -// Air Quality Rating function -String getAirQualityRating(int value) { - if (value < 800) return "Excellent"; - else if (value < 1200) return "Good"; - else if (value < 1800) return "Moderate"; - else if (value < 2400) return "Poor"; - else return "Very Poor"; -} - -// Emergency Button handler -void checkEmergencyButton() { - static bool lastState = HIGH; // using INPUT_PULLUP -> HIGH when released - static unsigned long lastChangeTime = 0; - bool currentState = digitalRead(EMERGENCY_BUTTON_PIN); - if (currentState != lastState && (millis() - lastChangeTime > 50)) { - lastChangeTime = millis(); - if (currentState == LOW) { // pressed - unsigned long now = millis(); - if (now - lastTapTime < TAP_TIMEOUT) tapCount++; - else tapCount = 1; - lastTapTime = now; - Serial.printf("BUTTON TAP #%d/%d\n", tapCount, REQUIRED_TAPS); - if (tapCount >= REQUIRED_TAPS) { - Serial.println("TRIPLE TAP - EMERGENCY TRIGGERED"); - emergencyTriggered = true; - tapCount = 0; - } - } - } - if (millis() - lastTapTime > TAP_TIMEOUT && tapCount > 0) tapCount = 0; - lastState = currentState; -} - -// Emergency Handler function -void handleEmergency() { - Serial.println("\n████████ EMERGENCY MODE █████████"); - SensorData s = readAllSensors(); - s.emergency = true; - s.motion = motionData; - Serial.println("EMERGENCY SNAPSHOT:"); - Serial.printf(" Temp: %.2f C Hum: %.2f %%\n", s.temperature, s.humidity); - Serial.printf(" MQ2:%d MQ9:%d MQ135:%d\n", s.mq2_analog, s.mq9_analog, s.mq135_analog); - Serial.printf(" Fall: %s\n", s.motion.fallDetected ? "YES":"NO"); - if (loraReady) sendLoRaData(s); else Serial.println("LoRa not ready."); - if (audioReady) playAudioFile(FALL_DETECTED); - delay(800); -} - -// -------------------------- Setup & Loop -------------------------- -void setup() { - Serial.begin(115200); - delay(800); - Serial.println("\n\nESP32 Multi-Sensor System - MPU6050 Clone WHO_AM_I Fix + ESP-NOW"); - Serial.println("================================="); - - // CRITICAL: Initialize WiFi properly BEFORE ESP-NOW - WiFi.mode(WIFI_STA); - WiFi.disconnect(false); // Changed from true to false - delay(100); - WiFi.begin(); - delay(100); - - initESPNOW(); - delay(200); - - Wire.begin(MPU6050_SDA, MPU6050_SCL); - Wire.setClock(100000); - pinMode(MPU6050_SDA, INPUT_PULLUP); - pinMode(MPU6050_SCL, INPUT_PULLUP); - - pinMode(MQ2_DIGITAL_PIN, INPUT); - pinMode(MQ9_DIGITAL_PIN, INPUT); - pinMode(MQ135_DIGITAL_PIN, INPUT); - - pinMode(EMERGENCY_BUTTON_PIN, INPUT_PULLUP); - Serial.printf("Emergency button: GPIO%d (INPUT_PULLUP). Press => LOW\n", EMERGENCY_BUTTON_PIN); - - pinMode(MPU6050_INT, INPUT); - - Serial.println("\n--- Initializing MPU6050 ---"); - if (!initMPU6050()) { - Serial.println("⚠ MPU6050 init failed. Motion features will be disabled."); - mpuReady = false; - } else { - Serial.println("✓ MPU6050 ready for motion detection"); - mpuReady = true; - motionData.lastMotionTime = millis(); - accelFiltered = G; - } - - Serial.println("\nInitializing DHT11 sensor..."); - dht.begin(); - delay(1500); - bool dhtWorking = false; - for (int i=0;i<3;i++) { - float t = dht.readTemperature(); - float h = dht.readHumidity(); - if (!isnan(t) && !isnan(h)) { - dhtWorking = true; - dhtReady = true; - lastValidTemperature = t; - float corr = h - 30.0f; - if (corr < 0.0f) corr = 0.0f; - if (corr > 100.0f) corr = 100.0f; - lastValidHumidity = corr; - Serial.printf("✓ DHT11: Temp %.1fC Humidity (corrected) %.1f%%\n", lastValidTemperature, lastValidHumidity); - break; - } - delay(1000); - } - if (!dhtWorking) { - Serial.println("⚠ DHT11 ERROR - Check DATA wiring to GPIO25 and power."); - dhtReady = false; - } - - fnM16pSerial.begin(9600, SERIAL_8N1, FN_M16P_RX, FN_M16P_TX); - delay(200); - setVolume(30); - audioReady = true; - Serial.println("✓ Audio module initialized."); - - Serial.println("Initializing LoRa..."); - pinMode(LORA_SS, OUTPUT); - digitalWrite(LORA_SS, HIGH); - pinMode(LORA_RST, OUTPUT); - digitalWrite(LORA_RST, HIGH); - SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_SS); - LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0); - digitalWrite(LORA_RST, LOW); delay(10); - digitalWrite(LORA_RST, HIGH); delay(10); - if (!LoRa.begin(LORA_BAND)) { - Serial.println("⚠ LoRa initialization failed. Check wiring/antenna."); - loraReady = false; - } else { - loraReady = true; - Serial.println("✓ LoRa initialized."); - } - - Serial.println("Warming gas sensors (15s)..."); - delay(15000); - - Serial.println("Calibrating MQ sensors..."); - calibrateSensors(); - - Serial.println("\nSYSTEM READY."); - printTestMenu(); - - if (audioReady) { playAudioFile(BOOT_AUDIO); delay(1000); } -} - -void loop() { - if (Serial.available() > 0) { - String cmd = Serial.readStringUntil('\n'); - cmd.trim(); - cmd.toLowerCase(); - handleTestCommand(cmd); - } - - checkEmergencyButton(); - - if (mpuReady) monitorMotion(); - - if (emergencyTriggered) { - handleEmergency(); - emergencyTriggered = false; - } - - if (loraReady) receiveLoRaMessages(); - - static unsigned long lastNormal = 0; - if (millis() - lastNormal >= 10000) { - lastNormal = millis(); - SensorData data = readAllSensors(); - data.emergency = false; - data.motion = motionData; - displayReadings(data); - checkAlerts(data); - if (loraReady && (millis() - lastLoRaSend > loraInterval)) { - sendLoRaData(data); - lastLoRaSend = millis(); - } - Serial.println("------------------------"); - } - - checkWristbandConnection(); - delay(50); -} - -// ---------------- Test commands & helpers ---------------- -void printTestMenu() { - Serial.println("\n╔═══════════════════════════════════════╗"); - Serial.println("║ TEST COMMANDS MENU ║"); - Serial.println("╠═══════════════════════════════════════╣"); - Serial.println("║ dht, mq2, mq9, mq135, mq, all ║"); - Serial.println("║ audio1..audio10, stop, volume+, volume-║"); - Serial.println("║ lora, button, emergency, calibrate ║"); - Serial.println("║ status, scan/i2c, help/menu ║"); - Serial.println("║ sendmsg (ESP-NOW -> wristband)║"); - Serial.println("╚═══════════════════════════════════════╝\n"); -} - -void handleTestCommand(String cmd) { - Serial.println("\n>>> EXECUTING TEST: " + cmd + " <<<\n"); - if (cmd == "help" || cmd == "menu") printTestMenu(); - else if (cmd == "dht") testDHT(); - else if (cmd == "mq2") testMQ2(); - else if (cmd == "mq9") testMQ9(); - else if (cmd == "mq135") testMQ135(); - else if (cmd == "mq") testAllMQ(); - else if (cmd.startsWith("audio")) { - int n = cmd.substring(5).toInt(); - if (n >= 1 && n <= 10) testAudio(n); - } else if (cmd == "stop") { stopAudio(); Serial.println("Audio stopped."); } - else if (cmd == "volume+") { setVolume(25); Serial.println("Volume 25"); } - else if (cmd == "volume-") { setVolume(15); Serial.println("Volume 15"); } - else if (cmd == "lora") testLoRa(); - else if (cmd == "button") testButton(); - else if (cmd == "emergency") testEmergency(); - else if (cmd == "all") testAllSensors(); - else if (cmd == "calibrate") calibrateSensors(); - else if (cmd == "status") printSystemStatus(); - else if (cmd == "scan" || cmd == "i2c") scanI2CDevices(); - else if (cmd.startsWith("sendmsg ")) { - String txt = cmd.substring(8); - if (txt.length() == 0) Serial.println("No message provided."); - else { - bool ok = sendTextToWristband(txt); - Serial.printf("sendTextToWristband('%s') => %s\n", txt.c_str(), ok ? "SENT":"FAILED"); - } - } - else Serial.println("❌ Unknown command: " + cmd); - Serial.println("\n>>> TEST COMPLETE <<<\n"); -} - -void testDHT() { - Serial.println("Testing DHT11 (5 samples) with manual -30% humidity correction:"); - for (int i=0;i<5;i++) { - float t = dht.readTemperature(); - float h = dht.readHumidity(); - if (!isnan(t) && !isnan(h)) { - float corr = h - 30.0f; - if (corr < 0) corr = 0; if (corr > 100) corr = 100; - Serial.printf(" #%d: Temp=%.2f C, Hum(corrected)=%.2f %%\n", i+1, t, corr); - } else { - Serial.printf(" #%d: FAILED (NaN)\n", i+1); - } - delay(2000); - } -} - -void testMQ2() { - Serial.println("Testing MQ2 (10 samples):"); - for (int i=0;i<10;i++) { - int a = analogRead(MQ2_ANALOG_PIN); - bool d = digitalRead(MQ2_DIGITAL_PIN) == LOW; - Serial.printf(" #%d: analog=%d digital=%s\n", i+1, a, d?"ACTIVE":"INACTIVE"); - delay(500); - } - Serial.printf("Calibration baseline=%.1f threshold=%.1f\n", mq2_cal.baseline, mq2_cal.dangerThreshold); -} - -void testMQ9() { - Serial.println("Testing MQ9 (10 samples):"); - for (int i=0;i<10;i++) { - int a = analogRead(MQ9_ANALOG_PIN); - bool d = digitalRead(MQ9_DIGITAL_PIN) == LOW; - Serial.printf(" #%d: analog=%d digital=%s\n", i+1, a, d?"ACTIVE":"INACTIVE"); - delay(500); - } - Serial.printf("Calibration baseline=%.1f threshold=%.1f\n", mq9_cal.baseline, mq9_cal.dangerThreshold); -} - -void testMQ135() { - Serial.println("Testing MQ135 (10 samples):"); - for (int i=0;i<10;i++) { - int a = analogRead(MQ135_ANALOG_PIN); - bool d = digitalRead(MQ135_DIGITAL_PIN) == LOW; - Serial.printf(" #%d: analog=%d digital=%s rating=%s\n", i+1, a, d?"POOR":"GOOD", getAirQualityRating(a).c_str()); - delay(500); - } - Serial.printf("Calibration baseline=%.1f threshold=%.1f\n", mq135_cal.baseline, mq135_cal.dangerThreshold); -} - -void testAllMQ() { testMQ2(); testMQ9(); testMQ135(); } - -void testAudio(int fileNum) { - if (!audioReady) { Serial.println("Audio not ready"); return; } - Serial.printf("Playing audio file #%d\n", fileNum); - playAudioFile(fileNum); -} - -void testLoRa() { - if (!loraReady) { Serial.println("LoRa not ready"); return; } - Serial.println("Sending test LoRa packet..."); - SensorData d = readAllSensors(); - d.emergency = false; - d.motion = motionData; - sendLoRaData(d); - Serial.println("Test LoRa sent."); -} - -void testEmergency() { - Serial.println("Triggering emergency now..."); - emergencyTriggered = true; -} - -void testButton() { - Serial.println("Testing emergency button for 10s..."); - unsigned long start = millis(); - bool last = digitalRead(EMERGENCY_BUTTON_PIN); - while (millis() - start < 10000) { - bool cur = digitalRead(EMERGENCY_BUTTON_PIN); - if (cur != last) { - Serial.printf("Button state change: %s\n", cur ? "HIGH" : "LOW"); - last = cur; - } - delay(100); - } - Serial.println("Button test complete."); -} - -void testAllSensors() { - testDHT(); - testAllMQ(); - testLoRa(); -} - -// ---------------- System status and misc helpers ---------------- -void scanI2CDevices() { - Serial.println("\n=== I2C Device Scanner ==="); - Serial.println("Scanning I2C bus (0x00 to 0x7F)..."); - int devicesFound = 0; - for (byte address = 1; address < 127; address++) { - Wire.beginTransmission(address); - byte error = Wire.endTransmission(); - if (error == 0) { - Serial.printf("✓ Device found at 0x%02X\n", address); - devicesFound++; - if (address == 0x68 || address == 0x69) Serial.println(" → This is likely the MPU6050!"); - } - } - if (devicesFound == 0) { - Serial.println("\n❌ NO I2C devices found!"); - Serial.println("\nTroubleshooting:"); - Serial.println("1. Check VCC is connected to 3.3V (NOT 5V)"); - Serial.println("2. Check GND is connected"); - Serial.println("3. Verify SDA → GPIO21, SCL → GPIO22"); - Serial.println("4. Check all connections are firm"); - Serial.println("5. Add 4.7kΩ pull-up resistors to SDA & SCL"); - Serial.println("6. Try a different MPU6050 module (may be faulty)"); - } else { - Serial.printf("\n✓ Total devices found: %d\n", devicesFound); - } - Serial.println("=========================\n"); -} - -void sendCommand(byte cmd, byte param1, byte param2, bool feedback) { - byte packet[10]; - packet[0] = FRAME_START; - packet[1] = VERSION; - packet[2] = 0x06; - packet[3] = cmd; - packet[4] = feedback ? WITH_FEEDBACK : NO_FEEDBACK; - packet[5] = param1; - packet[6] = param2; - uint16_t sum = packet[1] + packet[2] + packet[3] + packet[4] + packet[5] + packet[6]; - uint16_t checksum = 0xFFFF - sum + 1; - packet[7] = (checksum >> 8) & 0xFF; - packet[8] = checksum & 0xFF; - packet[9] = FRAME_END; - fnM16pSerial.write(packet, 10); -} - -void setVolume(int volume) { - if (volume < 0) volume = 0; - if (volume > 30) volume = 30; - sendCommand(CMD_VOLUME, 0x00, volume, false); -} - -void playAudioFile(int fileNumber) { - if (fileNumber < 1 || fileNumber > 10) return; - sendCommand(CMD_PLAY_TRACK, 0x00, fileNumber, false); - delay(60); -} - -void stopAudio() { - sendCommand(CMD_STOP, 0x00, 0x00, false); -} - -void calibrateSensors() { - Serial.println("Starting sensor calibration..."); - delay(500); - calibrateSensor(MQ2_ANALOG_PIN, &mq2_cal, "MQ2", MQ2_DANGER_THRESHOLD); - calibrateSensor(MQ9_ANALOG_PIN, &mq9_cal, "MQ9", MQ9_DANGER_THRESHOLD); - calibrateSensor(MQ135_ANALOG_PIN, &mq135_cal, "MQ135", MQ135_DANGER_THRESHOLD); - Serial.println("Calibration completed."); -} - -void calibrateSensor(int pin, SensorCalibration* cal, String sensorName, int minThreshold) { - Serial.print("Calibrating " + sensorName + " ..."); - float sum = 0; - for (int i=0;ibaseline = sum / CALIBRATION_SAMPLES; - float calculatedThreshold = cal->baseline * DANGER_MULTIPLIER; - cal->dangerThreshold = (calculatedThreshold > minThreshold) ? calculatedThreshold : minThreshold; - cal->calibrated = true; - Serial.println(" Done"); -} - -bool checkSensorDanger(int currentValue, SensorCalibration* cal, int staticDangerThreshold) { - if (!cal->calibrated) return currentValue >= staticDangerThreshold; - return currentValue >= cal->dangerThreshold; -} - -SensorData readAllSensors() { - SensorData data; - data.timestamp = millis(); - if (millis() - lastDHTReading > DHT_READING_INTERVAL) { - float rt = dht.readTemperature(); - float rh = dht.readHumidity(); - if (!isnan(rt) && rt >= -40 && rt <= 80) { data.temperature = rt; lastValidTemperature = rt; } - else data.temperature = lastValidTemperature; - if (!isnan(rh) && rh >= 0 && rh <= 100) { - float corr = rh - 30.0f; - if (corr < 0.0f) corr = 0.0f; - if (corr > 100.0f) corr = 100.0f; - data.humidity = corr; - lastValidHumidity = corr; - } else data.humidity = lastValidHumidity; - lastDHTReading = millis(); - } else { - data.temperature = lastValidTemperature; - data.humidity = lastValidHumidity; - } - data.mq2_analog = analogRead(MQ2_ANALOG_PIN); - data.mq9_analog = analogRead(MQ9_ANALOG_PIN); - data.mq135_analog = analogRead(MQ135_ANALOG_PIN); - data.mq2_digital = digitalRead(MQ2_DIGITAL_PIN) == LOW; - data.mq9_digital = digitalRead(MQ9_DIGITAL_PIN) == LOW; - data.mq135_digital = digitalRead(MQ135_DIGITAL_PIN) == LOW; - data.emergency = digitalRead(EMERGENCY_BUTTON_PIN) == LOW; - data.motion = motionData; - return data; -} - -void displayReadings(SensorData data) { - Serial.println("\n--- SENSOR SNAPSHOT ---"); - Serial.printf("Timestamp: %lu\n", data.timestamp); - Serial.printf("Temp: %.1f C Hum: %.1f %%\n", data.temperature, data.humidity); - Serial.printf("MQ2: %d (%s) MQ9: %d (%s) MQ135: %d (%s)\n", - data.mq2_analog, data.mq2_digital ? "ALERT":"OK", - data.mq9_analog, data.mq9_digital ? "ALERT":"OK", - data.mq135_analog, data.mq135_digital ? "ALERT":"OK"); - Serial.printf("Motion: accel=%.2fm/s2 gyro=%.2f deg/s\n", data.motion.totalAccel, data.motion.totalGyro); - Serial.printf("Emergency Button: %s\n", data.emergency ? "PRESSED" : "RELEASED"); - if (wristbandStatus.connected) { - unsigned long age = (millis() - wristbandStatus.lastUpdate) / 1000; - Serial.printf("Wristband: CONNECTED (BPM=%u SpO2=%u finger=%s, %lus ago)\n", - wristbandStatus.bpm, wristbandStatus.spo2, wristbandStatus.fingerDetected?"YES":"NO", age); - } else { - Serial.println("Wristband: DISCONNECTED"); - } -} - -void checkAlerts(SensorData data) { - if (checkSensorDanger(data.mq2_analog, &mq2_cal, MQ2_DANGER_THRESHOLD)) { - Serial.println("!!! MQ2 Danger detected"); - if (audioReady) playAudioFile(SMOKE_ALERT); - } - if (checkSensorDanger(data.mq9_analog, &mq9_cal, MQ9_DANGER_THRESHOLD)) { - Serial.println("!!! MQ9 Danger detected"); - if (audioReady) playAudioFile(CO_ALERT); - } - if (checkSensorDanger(data.mq135_analog, &mq135_cal, MQ135_DANGER_THRESHOLD)) { - Serial.println("!!! MQ135 Air quality degraded"); - if (audioReady) playAudioFile(AIR_QUALITY_WARNING); - } -} - -void sendLoRaData(SensorData data) { - StaticJsonDocument<512> doc; - doc["node"] = NODE_ID; - doc["timestamp"] = data.timestamp; - doc["temp"] = data.temperature; - doc["hum"] = data.humidity; - doc["mq2"] = data.mq2_analog; - doc["mq9"] = data.mq9_analog; - doc["mq135"] = data.mq135_analog; - doc["emergency"] = data.emergency ? 1 : 0; - doc["motion_accel"] = data.motion.totalAccel; - doc["motion_gyro"] = data.motion.totalGyro; - doc["bpm"] = wristbandStatus.connected ? wristbandStatus.bpm : 0; - doc["spo2"] = wristbandStatus.connected ? wristbandStatus.spo2 : 0; - doc["wristband_connected"] = wristbandStatus.connected ? 1 : 0; - char payload[512]; - size_t n = serializeJson(doc, payload, sizeof(payload)); - if (!loraReady) { Serial.println("LoRa not ready - cannot send"); return; } - LoRa.beginPacket(); - LoRa.print(payload); - LoRa.endPacket(); - packetCount++; - Serial.printf("LoRa packet #%d sent (%d bytes): %s\n", packetCount, (int)n, payload); -} - -// ---------------- ESP-NOW Implementation ---------------- -void initESPNOW() { - // Ensure WiFi is properly initialized first - WiFi.mode(WIFI_STA); - WiFi.disconnect(false); - delay(100); - WiFi.begin(); - delay(100); - - int channel = 1; - esp_err_t cherr = esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE); - if (cherr == ESP_OK) { - Serial.printf("✓ WiFi channel set to %d for ESP-NOW\n", channel); - } else { - Serial.printf("⚠ Failed to set WiFi channel (%d) err=%d\n", channel, (int)cherr); - } - - if (esp_now_init() != ESP_OK) { - Serial.println("⚠ ESP-NOW init failed - retrying..."); - delay(500); - if (esp_now_init() != ESP_OK) { - Serial.println("✗ ESP-NOW init failed after retry"); - espnowReady = false; - return; - } - } - espnowReady = true; - Serial.println("✓ ESP-NOW initialized"); - - esp_now_register_send_cb(onDataSent); - esp_now_register_recv_cb(onDataRecv); - - delay(100); - - esp_now_peer_info_t peerInfo; - memset(&peerInfo, 0, sizeof(peerInfo)); - memcpy(peerInfo.peer_addr, wristbandMac, 6); - peerInfo.channel = channel; - #if defined(ESP_IF_WIFI_STA) - peerInfo.ifidx = ESP_IF_WIFI_STA; - #elif defined(WIFI_IF_STA) - peerInfo.ifidx = WIFI_IF_STA; - #else - peerInfo.ifidx = (wifi_interface_t)0; - #endif - peerInfo.encrypt = false; - - if (!esp_now_is_peer_exist(wristbandMac)) { - esp_err_t addStatus = esp_now_add_peer(&peerInfo); - if (addStatus != ESP_OK) { - Serial.printf("⚠ Failed to add ESP-NOW peer (wristband) - error: %d\n", addStatus); - } else { - Serial.println("✓ Wristband ESP-NOW peer added"); - Serial.printf(" Peer MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", - wristbandMac[0], wristbandMac[1], wristbandMac[2], - wristbandMac[3], wristbandMac[4], wristbandMac[5]); - } - } else { - Serial.println("✓ Wristband peer already exists"); - } - - delay(100); -} - - -void onDataRecv(const esp_now_recv_info_t *recv_info, const uint8_t *data, int len) { - if (data == NULL || len < 1) return; - uint8_t msgType = data[0]; - if (msgType == MSG_TYPE_VITALS) { - if (len < (int)sizeof(espnow_vitals_t)) { Serial.println("⚠ Received VITALS unexpected length"); return; } - espnow_vitals_t vitals; memcpy(&vitals, data, sizeof(vitals)); - wristbandStatus.bpm = vitals.bpm; - wristbandStatus.spo2 = vitals.spo2; - wristbandStatus.fingerDetected = (vitals.finger != 0); - wristbandStatus.lastUpdate = millis(); - wristbandStatus.connected = true; - Serial.printf("[ESP-NOW RX] VITALS -> BPM=%u SpO2=%u finger=%s ts=%lu\n", - wristbandStatus.bpm, wristbandStatus.spo2, wristbandStatus.fingerDetected?"YES":"NO", (unsigned long)vitals.timestamp); - } else if (msgType == MSG_TYPE_ACK) { - if (len < (int)sizeof(espnow_ack_t)) { Serial.println("⚠ Received ACK unexpected length"); return; } - espnow_ack_t ack; memcpy(&ack, data, sizeof(ack)); - if (ack.messageId == wristbandStatus.lastMessageId) { - wristbandStatus.messageAcknowledged = (ack.success != 0); - Serial.printf("[ESP-NOW RX] ACK for msgId=%lu success=%s\n", (unsigned long)ack.messageId, ack.success ? "YES":"NO"); - } else Serial.printf("[ESP-NOW RX] ACK for unknown msgId=%lu\n", (unsigned long)ack.messageId); - } else Serial.printf("[ESP-NOW RX] Unknown msgType: 0x%02X\n", msgType); -} - -void onDataSent(const wifi_tx_info_t *tx_info, esp_now_send_status_t status) { - lastEspNowSendStatus = (status == ESP_NOW_SEND_SUCCESS) ? ESP_OK : ESP_FAIL; - char macStr[18]; - sprintf(macStr, "%02X:%02X:%02X:%02X:%02X:%02X", - wristbandMac[0], wristbandMac[1], wristbandMac[2], - wristbandMac[3], wristbandMac[4], wristbandMac[5]); - Serial.printf("[ESP-NOW TX] to %s status=%s\n", macStr, (status == ESP_NOW_SEND_SUCCESS) ? "OK":"FAIL"); -} - -bool sendTextToWristband(const String &message) { - if (!espnowReady) { Serial.println("ESP-NOW not ready - cannot send text"); return false; } - espnow_text_t pkt; memset(&pkt,0,sizeof(pkt)); - pkt.msgType = MSG_TYPE_TEXT; - pkt.messageId = outgoingMessageCounter++; - size_t len = message.length(); - if (len > MAX_TEXT_LEN - 1) len = MAX_TEXT_LEN - 1; - pkt.length = (uint8_t)len; - memcpy(pkt.text, message.c_str(), pkt.length); - pkt.text[pkt.length] = '\0'; - esp_err_t rc = esp_now_send(wristbandMac, (uint8_t *)&pkt, sizeof(pkt)); - if (rc == ESP_OK) { - wristbandStatus.lastMessageId = pkt.messageId; - wristbandStatus.messageAcknowledged = false; - Serial.printf("Sent TEXT msgId=%lu len=%u\n", (unsigned long)pkt.messageId, pkt.length); - return true; - } else { - Serial.printf("Failed to send TEXT (esp_now_send rc=%d)\n", rc); - return false; - } -} - -void checkWristbandConnection() { - unsigned long now = millis(); - if (wristbandStatus.connected && (now - wristbandStatus.lastUpdate > 35000UL)) { - wristbandStatus.connected = false; - Serial.println("Wristband connection timed out (35s) -> DISCONNECTED"); - } -} - -void receiveLoRaMessages() { - int packetSize = LoRa.parsePacket(); - if (packetSize <= 0) return; - String incoming = ""; - while (LoRa.available()) incoming += (char)LoRa.read(); - incoming.trim(); - if (incoming.length() == 0) return; - Serial.printf("[LoRa RX] Packet: %s\n", incoming.c_str()); - StaticJsonDocument<256> doc; - DeserializationError err = deserializeJson(doc, incoming); - if (err) { Serial.println("LoRa JSON parse error"); return; } - if (doc.containsKey("message")) { - String message = doc["message"].as(); - Serial.printf("Forwarding LoRa 'message' to wristband via ESP-NOW: %s\n", message.c_str()); - bool ok = sendTextToWristband(message); - Serial.printf("Forward result: %s\n", ok ? "SENT":"FAILED"); - } else Serial.println("LoRa packet has no 'message' field"); -} - -void printSystemStatus() { - Serial.println("\n--- SYSTEM STATUS ---"); - Serial.printf("Audio: %s LoRa: %s DHT: %s MPU: %s ESP-NOW: %s\n", - audioReady ? "OK":"NO", - loraReady ? "OK":"NO", - dhtReady ? "OK":"NO", - mpuReady ? "OK":"NO", - espnowReady ? "OK":"NO"); - if (wristbandStatus.connected) { - Serial.printf("Wristband: CONNECTED BPM=%u SpO2=%u LastUpdate=%lums ago\n", - wristbandStatus.bpm, wristbandStatus.spo2, (unsigned long)(millis() - wristbandStatus.lastUpdate)); - Serial.printf("Last message id sent=%lu acked=%s\n", (unsigned long)wristbandStatus.lastMessageId, - wristbandStatus.messageAcknowledged ? "YES":"NO"); - } else { - Serial.println("Wristband: DISCONNECTED"); - } - Serial.println("---------------------\n"); -} diff --git a/IshitaKaGhr/central.ino b/IshitaKaGhr/central.ino deleted file mode 100644 index 34edabf..0000000 --- a/IshitaKaGhr/central.ino +++ /dev/null @@ -1,1141 +0,0 @@ -/* - ESP32 LoRa Central Node - Supabase Gateway + Email Alerts - FIXED: Enhanced SMTP error logging and connection handling -*/ - -#include -#include -#include -#include -#include -#include -#include - -#define WIFI_SSID "iPhone" -#define WIFI_PASSWORD "12345678" - -#define SUPABASE_URL "https://kfwngukvlsjjhwslktbn.supabase.co" -#define SUPABASE_ANON_KEY "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imtmd25ndWt2bHNqamh3c2xrdGJuIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTgzNzYwMzksImV4cCI6MjA3Mzk1MjAzOX0.qY_JlPE6g5ewfBodJZYDS6ABFySvEMLgqOhCeQg8U8I" - -#define SMTP_SERVER "smtp.gmail.com" -#define SMTP_PORT 465 -#define SENDER_EMAIL "caneriesiren@gmail.com" -#define SENDER_PASSWORD "jczhurwioeagagiw" // App password without spaces -#define SUPERVISOR_EMAIL "caneriesiren@gmail.com" - -#define DEBUG_MODE true - -// Multiple NTP servers for better reliability -const char* ntpServers[] = { - "pool.ntp.org", - "time.nist.gov", - "time.google.com", - "time.cloudflare.com", - "time.windows.com" -}; -const int ntpServerCount = 5; -const long gmtOffset_sec = 19800; -const int daylightOffset_sec = 0; - -#define LORA_SCK 5 -#define LORA_MISO 19 -#define LORA_MOSI 27 -#define LORA_SS 18 -#define LORA_RST 14 -#define LORA_DIO0 2 -#define LORA_BAND 433E6 -#define CENTRAL_NODE_ID "CENTRAL_GATEWAY_001" - -#define EMAIL_SEND_TIMEOUT 300000 -#define EMAIL_BUFFER_SIZE 10 - -#define MESSAGE_TEST_INTERVAL 300000 - -// Structure to track packet counts per node -struct NodePacketTracker { - String nodeId; - unsigned long packetCount; - unsigned long lastSeenTime; -}; - -#define MAX_TRACKED_NODES 10 -NodePacketTracker nodeTrackers[MAX_TRACKED_NODES]; -int trackedNodesCount = 0; - -bool wifiConnected = false; -bool supabaseReady = false; -bool ntpSynced = false; -bool loraReady = false; - -unsigned long packetsReceived = 0; -unsigned long packetsUploaded = 0; -unsigned long packetsCorrupted = 0; -unsigned long emergenciesDetected = 0; -unsigned long emailsSent = 0; -unsigned long lastStatsDisplay = 0; -unsigned long lastWiFiCheck = 0; -unsigned long lastNTPSync = 0; - -unsigned long messagesDeliveredToWristband = 0; // Successfully ACKed by wristband - -unsigned long messagesSentToEdge = 0; -unsigned long lastTestMessage = 0; - -unsigned long getNodePacketCount(String nodeId); -String determineAirQuality(int mq2, int mq9, int mq135, bool mq2Digital, bool mq9Digital, bool mq135Digital); - -struct EmergencyRecord { - String nodeId; - unsigned long lastAlertTime; -}; - -EmergencyRecord emergencyBuffer[EMAIL_BUFFER_SIZE]; -int emergencyBufferIndex = 0; - -HTTPClient http; -WiFiClientSecure client; - -void displayStatistics(); -String getCurrentDateTime(); -bool canSendEmergencyEmail(String nodeId); -void recordEmergency(String nodeId); -void sendEmergencyEmail(JsonDocument& sensorData, int rssi, float snr); -bool sendSMTPEmail(String subject, String body); -String base64Encode(String input); -String readSMTPResponse(WiFiClient& client, int timeout = 5000); - -void setup() { - Serial.begin(115200); - delay(2000); - - Serial.println("\n====================================="); - Serial.println("ESP32 LoRa Central Node v2.1"); - Serial.println("Email Debug Enhanced"); - Serial.println("=====================================\n"); - - for (int i = 0; i < EMAIL_BUFFER_SIZE; i++) { - emergencyBuffer[i].nodeId = ""; - emergencyBuffer[i].lastAlertTime = 0; - } - - initializeLoRa(); - initializeWiFi(); - - if (wifiConnected) { - initializeNTP(); - initializeSupabase(); - } - - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.println("║ MESSAGE RELAY SYSTEM ENABLED ║"); - Serial.println("╠════════════════════════════════════════════╣"); - Serial.println("║ Commands: ║"); - Serial.println("║ msg - Send message to wristband ║"); - Serial.println("║ testmsg - Send test message ║"); - Serial.println("║ msgstats - Show statistics ║"); - Serial.println("║ help - Show command list ║"); - Serial.println("╚════════════════════════════════════════════╝\n"); - // ========== END OF NEW LINES ========== - Serial.println("Central Node ready!\n"); -} - -void loop() { - // Check WiFi connection (existing code - no changes) - if (millis() - lastWiFiCheck > 30000) { - checkWiFiConnection(); - lastWiFiCheck = millis(); - } - - // Check NTP sync (existing code - no changes) - if (!ntpSynced && wifiConnected && (millis() - lastNTPSync > 300000)) { - initializeNTP(); - lastNTPSync = millis(); - } - - // ========== ADD THIS BLOCK HERE ========== - // NEW: Handle message commands from Serial Monitor - handleMessageCommands(); - - // NEW: Optional - Send automatic test messages (comment out if not needed) - // Uncomment the block below to send test messages every 5 minutes - /* - if (millis() - lastTestMessage > MESSAGE_TEST_INTERVAL) { - sendMessageToWristband("Automatic test message at " + getCurrentDateTime()); - lastTestMessage = millis(); - } - */ - // ========== END OF NEW BLOCK ========== - - // Handle LoRa packets (existing code - no changes) - if (loraReady) { - handleLoRaPackets(); - } - - // Display statistics (existing code - no changes) - if (millis() - lastStatsDisplay > 60000) { - displayStatistics(); - lastStatsDisplay = millis(); - } - - delay(50); -} -void initializeLoRa() { - Serial.println("Initializing LoRa..."); - - SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_SS); - LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0); - - if (!LoRa.begin(LORA_BAND)) { // Now 433E6 - Serial.println("ERROR: LoRa failed!"); - loraReady = false; - return; - } - - LoRa.setTxPower(20); - LoRa.setSpreadingFactor(12); - LoRa.setSignalBandwidth(125E3); - LoRa.setCodingRate4(8); - LoRa.setPreambleLength(8); - LoRa.setSyncWord(0x34); - - // ADD these lines for better 433 MHz performance: - LoRa.enableCrc(); - LoRa.setOCP(240); - - Serial.println("LoRa OK\n"); - loraReady = true; -} - -void initializeWiFi() { - Serial.print("Connecting to WiFi"); - WiFi.mode(WIFI_STA); - WiFi.begin(WIFI_SSID, WIFI_PASSWORD); - - int attempts = 0; - while (WiFi.status() != WL_CONNECTED && attempts < 30) { - delay(500); - Serial.print("."); - attempts++; - } - - if (WiFi.status() == WL_CONNECTED) { - wifiConnected = true; - Serial.println("\nWiFi OK"); - Serial.println("IP: " + WiFi.localIP().toString() + "\n"); - } else { - wifiConnected = false; - Serial.println("\nWiFi FAILED\n"); - } -} - -void checkWiFiConnection() { - if (WiFi.status() != WL_CONNECTED && wifiConnected) { - Serial.println("WiFi lost. Reconnecting..."); - wifiConnected = false; - supabaseReady = false; - initializeWiFi(); - - if (wifiConnected && !supabaseReady) { - initializeSupabase(); - } - } -} - -void initializeNTP() { - if (!wifiConnected) return; - - Serial.println("Syncing NTP with multiple servers..."); - - struct tm timeinfo; - bool syncSuccess = false; - - // Try each NTP server until one works - for (int serverIndex = 0; serverIndex < ntpServerCount && !syncSuccess; serverIndex++) { - Serial.print(" Trying "); - Serial.print(ntpServers[serverIndex]); - Serial.print("... "); - - configTime(gmtOffset_sec, daylightOffset_sec, ntpServers[serverIndex]); - - int attempts = 0; - while (!getLocalTime(&timeinfo) && attempts < 10) { - delay(500); - attempts++; - } - - if (attempts < 10) { - syncSuccess = true; - ntpSynced = true; - Serial.println("✓ Success!"); - Serial.print(" Current time: "); - Serial.println(getCurrentDateTime()); - break; - } else { - Serial.println("✗ Failed"); - } - } - - if (!syncSuccess) { - Serial.println("❌ All NTP servers failed"); - ntpSynced = false; - } else { - Serial.println("NTP sync complete\n"); - } -} - -void initializeSupabase() { - if (!wifiConnected) return; - - Serial.println("Testing Supabase connection..."); - client.setInsecure(); - - String testUrl = String(SUPABASE_URL) + "/rest/v1/"; - http.begin(client, testUrl); - http.addHeader("Content-Type", "application/json"); - http.addHeader("apikey", SUPABASE_ANON_KEY); - - int httpResponseCode = http.GET(); - - if (httpResponseCode > 0) { - Serial.println("Supabase OK\n"); - supabaseReady = true; - } else { - Serial.println("Supabase FAILED\n"); - supabaseReady = false; - } - - http.end(); -} - -void handleLoRaPackets() { - int packetSize = LoRa.parsePacket(); - if (packetSize == 0) return; - - String receivedPacket = ""; - while (LoRa.available()) { - char c = (char)LoRa.read(); - if (c >= 20 && c <= 126) { - receivedPacket += c; - } - } - - if (receivedPacket.length() < 5) return; - - int rssi = LoRa.packetRssi(); - float snr = LoRa.packetSnr(); - - packetsReceived++; - - Serial.println("========================"); - Serial.println("LoRa Packet #" + String(packetsReceived)); - Serial.println("RSSI: " + String(rssi) + " | SNR: " + String(snr, 1)); - Serial.println("Size: " + String(receivedPacket.length()) + " bytes"); - - String cleanedPacket = cleanJsonPacket(receivedPacket); - if (cleanedPacket.length() > 0) { - processAndUploadPacket(cleanedPacket, rssi, snr); - } else { - packetsCorrupted++; - Serial.println("ERROR: Packet cleanup failed"); - } - Serial.println("========================\n"); -} - -String cleanJsonPacket(String rawPacket) { - DynamicJsonDocument testDoc(512); - if (deserializeJson(testDoc, rawPacket) == DeserializationError::Ok) { - return rawPacket; - } - - String cleaned = rawPacket; - - if (cleaned.endsWith(",p")) { - cleaned = cleaned.substring(0, cleaned.length() - 2) + "od\"}"; - } else if (cleaned.endsWith("Go,p")) { - cleaned = cleaned.substring(0, cleaned.length() - 4) + "Good\"}"; - } - - int openBraces = 0, closeBraces = 0; - for (char c : cleaned) { - if (c == '{') openBraces++; - if (c == '}') closeBraces++; - } - - while (closeBraces < openBraces) { - cleaned += "}"; - closeBraces++; - } - - int quotes = 0; - for (char c : cleaned) { - if (c == '"') quotes++; - } - - if (quotes % 2 != 0) cleaned += "\""; - - return cleaned; -} - -// Get or create packet count for a node -unsigned long getNodePacketCount(String nodeId) { - // Find existing node - for (int i = 0; i < trackedNodesCount; i++) { - if (nodeTrackers[i].nodeId == nodeId) { - nodeTrackers[i].packetCount++; - nodeTrackers[i].lastSeenTime = millis(); - return nodeTrackers[i].packetCount; - } - } - - // Node not found, add new one - if (trackedNodesCount < MAX_TRACKED_NODES) { - nodeTrackers[trackedNodesCount].nodeId = nodeId; - nodeTrackers[trackedNodesCount].packetCount = 1; - nodeTrackers[trackedNodesCount].lastSeenTime = millis(); - trackedNodesCount++; - return 1; - } - - // Buffer full, replace oldest entry - int oldestIndex = 0; - unsigned long oldestTime = nodeTrackers[0].lastSeenTime; - for (int i = 1; i < MAX_TRACKED_NODES; i++) { - if (nodeTrackers[i].lastSeenTime < oldestTime) { - oldestTime = nodeTrackers[i].lastSeenTime; - oldestIndex = i; - } - } - - nodeTrackers[oldestIndex].nodeId = nodeId; - nodeTrackers[oldestIndex].packetCount = 1; - nodeTrackers[oldestIndex].lastSeenTime = millis(); - return 1; -} - -// Determine air quality based on comprehensive sensor analysis -String determineAirQuality(int mq2, int mq9, int mq135, bool mq2Digital, bool mq9Digital, bool mq135Digital) { - /* - * COMPREHENSIVE AIR QUALITY ANALYSIS - * - * MQ2: Detects smoke, LPG, propane, methane, hydrogen - * Analog: 0-1023 (higher = more gas) - * Digital: true = gas detected above threshold - * - * MQ9: Detects carbon monoxide (CO) and combustible gases - * Analog: 0-1023 (higher = more CO) - * Digital: true = dangerous CO levels - * - * MQ135: General air quality - NH3, NOx, alcohol, benzene, smoke, CO2 - * Analog: 0-1023 (higher = worse air quality) - * Digital: true = poor air quality detected - * - * THRESHOLDS (calibrated for mining environments): - * - Safe: < 300 analog, no digital triggers - * - Fair: 300-400 analog, no digital triggers - * - Moderate: 400-500 analog, or 1 digital trigger - * - Bad: 500-650 analog, or 2 digital triggers - * - Danger: > 650 analog, or all 3 digital triggers - */ - - // Count digital triggers (immediate danger indicators) - int digitalAlerts = 0; - if (mq2Digital) digitalAlerts++; - if (mq9Digital) digitalAlerts++; - if (mq135Digital) digitalAlerts++; - - // CRITICAL: If all 3 digital sensors triggered = IMMEDIATE DANGER - if (digitalAlerts >= 3) { - return "DANGER"; - } - - // Analyze analog readings with weighted scoring - int dangerScore = 0; - int badScore = 0; - int moderateScore = 0; - int fairScore = 0; - - // MQ2 Analysis (Smoke/Flammable Gas) - HIGHEST PRIORITY in mines - if (mq2 > 700 || mq2Digital) { - dangerScore += 3; // Critical: explosion risk - } else if (mq2 > 550) { - badScore += 2; - } else if (mq2 > 320) { - moderateScore += 2; - } else if (mq2 > 100) { - fairScore += 1; - } - - // MQ9 Analysis (Carbon Monoxide) - HIGH PRIORITY (silent killer) - if (mq9 > 4000 || mq9Digital) { - dangerScore += 3; // Critical: CO poisoning risk - } else if (mq9 > 3200) { - badScore += 2; - } else if (mq9 > 2400) { - moderateScore += 2; - } else if (mq9 > 1600) { - fairScore += 1; - } - - // MQ135 Analysis (General Air Quality) - MEDIUM PRIORITY - if (mq135 > 2200 || mq135Digital) { - dangerScore += 2; // Critical: toxic air - } else if (mq135 > 1800) { - badScore += 2; - } else if (mq135 > 1400) { - moderateScore += 1; - } else if (mq135 > 1000) { - fairScore += 1; - } - - // Additional checks for digital alerts - if (digitalAlerts >= 2) { - dangerScore += 2; // Two sensors in digital alert = very dangerous - } else if (digitalAlerts == 1) { - badScore += 1; - } - - // DECISION LOGIC (prioritize worst conditions) - if (dangerScore >= 4) { - return "DANGER"; - } else if (dangerScore >= 2 || badScore >= 4) { - return "BAD"; - } else if (dangerScore >= 1 || badScore >= 2 || moderateScore >= 3) { - return "MODERATE"; - } else if (moderateScore >= 1 || fairScore >= 2) { - return "FAIR"; - } else { - return "GOOD"; - } -} - -void processAndUploadPacket(String cleanedPacket, int rssi, float snr) { - DynamicJsonDocument receivedDoc(900); - - DeserializationError error = deserializeJson(receivedDoc, cleanedPacket); - if (error) { - packetsCorrupted++; - Serial.println("ERROR: JSON parse failed: " + String(error.c_str())); - Serial.println("Raw packet: " + cleanedPacket); - return; - } - - // DEBUG: Print entire received JSON - if (DEBUG_MODE) { - Serial.println("\n=== RECEIVED JSON DEBUG ==="); - serializeJsonPretty(receivedDoc, Serial); - Serial.println("\n==========================="); - } - - // CRITICAL FIX: Check for emergency field more robustly - bool isEmergency = false; - - // Method 1: Check if field exists and is true - if (receivedDoc.containsKey("emergency")) { - JsonVariant emergencyVar = receivedDoc["emergency"]; - - if (emergencyVar.is()) { - isEmergency = emergencyVar.as(); - } else if (emergencyVar.is()) { - isEmergency = (emergencyVar.as() != 0); - } else if (emergencyVar.is()) { - String emergencyStr = emergencyVar.as(); - emergencyStr.toLowerCase(); - isEmergency = (emergencyStr == "true" || emergencyStr == "1"); - } - - Serial.println("Emergency field found: " + String(isEmergency ? "TRUE" : "FALSE")); - } else { - Serial.println("WARNING: No 'emergency' field in JSON"); - } - - String nodeId = receivedDoc["node"] | "UNKNOWN"; -unsigned long sensorPacketCount = getNodePacketCount(nodeId); - -// ⭐ ADD THESE 3 LINES HERE ⭐ -bool mq2Digital = receivedDoc["mq2_digital"] | false; -bool mq9Digital = receivedDoc["mq9_digital"] | false; -bool mq135Digital = receivedDoc["mq135_digital"] | false; - - - - // Handle emergency - if (isEmergency) { - emergenciesDetected++; - - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.println("║ 🚨 EMERGENCY SIGNAL RECEIVED! 🚨 ║"); - Serial.println("║ Node: " + nodeId + String(35 - nodeId.length(), ' ') + "║"); - Serial.println("║ Emergency Count: " + String(emergenciesDetected) + String(27 - String(emergenciesDetected).length(), ' ') + "║"); - Serial.println("╚════════════════════════════════════════════╝\n"); - - if (canSendEmergencyEmail(nodeId)) { - Serial.println(">>> SENDING EMERGENCY EMAIL <<<"); - sendEmergencyEmail(receivedDoc, rssi, snr); - recordEmergency(nodeId); - } else { - unsigned long timeSinceLastEmail = 0; - for (int i = 0; i < EMAIL_BUFFER_SIZE; i++) { - if (emergencyBuffer[i].nodeId == nodeId) { - timeSinceLastEmail = (millis() - emergencyBuffer[i].lastAlertTime) / 1000; - break; - } - } - Serial.println("⏱ Rate limit active for node " + nodeId); - Serial.println(" Time since last email: " + String(timeSinceLastEmail) + "s"); - Serial.println(" Cooldown period: " + String(EMAIL_SEND_TIMEOUT/1000) + "s"); - Serial.println(" Email skipped\n"); - } - } else { - if (DEBUG_MODE) { - Serial.println("📊 Normal packet (non-emergency) from node " + nodeId); - } - } - - // Build upload document - DynamicJsonDocument uploadDoc(1600); - uploadDoc["sensor_node_id"] = nodeId; - uploadDoc["sensor_packet_count"] = sensorPacketCount; - uploadDoc["sensor_timestamp"] = receivedDoc["timestamp"] | 0; - uploadDoc["temperature"] = receivedDoc["temp"]; - uploadDoc["humidity"] = receivedDoc["hum"] | 0; - uploadDoc["mq2_analog"] = receivedDoc["mq2"] | 0; - uploadDoc["mq9_analog"] = receivedDoc["mq9"] | 0; - uploadDoc["mq135_analog"] = receivedDoc["mq135"] | 0; - uploadDoc["mq2_digital"] = mq2Digital; -uploadDoc["mq9_digital"] = mq9Digital; -uploadDoc["mq135_digital"] = mq135Digital; - -// Calculate air quality with comprehensive logic -int mq2 = receivedDoc["mq2"] | 0; -int mq9 = receivedDoc["mq9"] | 0; -int mq135 = receivedDoc["mq135"] | 0; -String airQuality = determineAirQuality(mq2, mq9, mq135, mq2Digital, mq9Digital, mq135Digital); -uploadDoc["air_quality"] = airQuality; - -// Log air quality warnings -if (airQuality == "DANGER") { - Serial.println("🚨 CRITICAL AIR QUALITY: DANGER"); -} else if (airQuality == "BAD") { - Serial.println("⚠️ WARNING: BAD air quality detected"); -} else if (airQuality == "MODERATE") { - Serial.println("⚡ CAUTION: MODERATE air quality"); -} - // Use explicit float conversion - float motionAccel = receivedDoc["motion_accel"].as(); - float motionGyro = receivedDoc["motion_gyro"].as(); - - // Safety check - if (isnan(motionAccel)) motionAccel = 0.0f; - if (isnan(motionGyro)) motionGyro = 0.0f; - - uploadDoc["motion_accel"] = motionAccel; - uploadDoc["motion_gyro"] = motionGyro; - uploadDoc["bpm"] = receivedDoc["bpm"] | 0; - uploadDoc["spo2"] = receivedDoc["spo2"] | 0; - uploadDoc["body_temp"] = receivedDoc["body_temp"] | 0.0f; - uploadDoc["wristband_connected"] = receivedDoc["wristband_connected"] | 0; - uploadDoc["emergency"] = isEmergency; // Add emergency field to database - uploadDoc["central_node_id"] = CENTRAL_NODE_ID; - uploadDoc["received_time"] = getCurrentDateTime(); - uploadDoc["received_timestamp"] = millis(); - uploadDoc["rssi"] = rssi; - uploadDoc["snr"] = snr; - uploadDoc["gateway_packet_count"] = packetsReceived; - - String jsonString; - serializeJson(uploadDoc, jsonString); - - if (DEBUG_MODE || isEmergency) { - Serial.println("Upload Payload: " + jsonString); - } - - if (supabaseReady) { - uploadToSupabase(jsonString); - } else { - Serial.println("WARNING: Supabase not ready - data not uploaded"); - } -} - -bool canSendEmergencyEmail(String nodeId) { - unsigned long now = millis(); - - for (int i = 0; i < EMAIL_BUFFER_SIZE; i++) { - if (emergencyBuffer[i].nodeId == nodeId) { - if (now - emergencyBuffer[i].lastAlertTime >= EMAIL_SEND_TIMEOUT) { - return true; - } else { - return false; - } - } - } - - return true; -} - -void recordEmergency(String nodeId) { - unsigned long now = millis(); - - for (int i = 0; i < EMAIL_BUFFER_SIZE; i++) { - if (emergencyBuffer[i].nodeId == nodeId) { - emergencyBuffer[i].lastAlertTime = now; - return; - } - } - - if (emergencyBufferIndex >= EMAIL_BUFFER_SIZE) { - emergencyBufferIndex = 0; - } - - emergencyBuffer[emergencyBufferIndex].nodeId = nodeId; - emergencyBuffer[emergencyBufferIndex].lastAlertTime = now; - emergencyBufferIndex++; -} - -// * Send a text message to wristband via Edge Node relay -// * Central → (LoRa) → Edge → (ESP-NOW) → Wristband -// * -// * @param message The text message to send (max 120 characters recommended) -// * @return true if LoRa transmission succeeded, false otherwise -// */ -bool sendMessageToWristband(String message) { - if (!loraReady) { - Serial.println("❌ ERROR: LoRa not ready - cannot send message"); - return false; - } - - if (message.length() == 0) { - Serial.println("❌ ERROR: Empty message - not sending"); - return false; - } - - if (message.length() > 120) { - Serial.println("⚠️ WARNING: Message too long, truncating to 120 chars"); - message = message.substring(0, 120); - } - - // Create JSON packet with "message" field - // Edge node's receiveLoRaMessages() already looks for this field - DynamicJsonDocument doc(256); - doc["message"] = message; - doc["timestamp"] = millis(); - doc["from"] = "central_node"; - - String payload; - serializeJson(doc, payload); - - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.println("║ SENDING MESSAGE TO WRISTBAND VIA EDGE ║"); - Serial.println("╚════════════════════════════════════════════╝"); - Serial.println("Message: " + message); - Serial.println("Payload: " + payload); - Serial.println("Length: " + String(payload.length()) + " bytes"); - - // Send via LoRa - LoRa.beginPacket(); - LoRa.print(payload); - LoRa.endPacket(); - - messagesSentToEdge++; - - Serial.println("✓ Message transmitted via LoRa (#" + String(messagesSentToEdge) + ")"); - Serial.println(" → Edge node will forward to wristband via ESP-NOW"); - Serial.println("════════════════════════════════════════════\n"); - - return true; -} - -/** - * Check for serial commands to send messages - * Commands: - * msg - Send message to wristband - * testmsg - Send a test message - * msgstats - Show message statistics - */ -void handleMessageCommands() { - if (Serial.available() > 0) { - String command = Serial.readStringUntil('\n'); - command.trim(); - - if (command.length() == 0) return; - - // Convert to lowercase for comparison - String cmdLower = command; - cmdLower.toLowerCase(); - - if (cmdLower.startsWith("msg ")) { - // Extract message after "msg " - String message = command.substring(4); - message.trim(); - - if (message.length() > 0) { - Serial.println("\n>>> MANUAL MESSAGE COMMAND RECEIVED <<<"); - sendMessageToWristband(message); - } else { - Serial.println("ERROR: No message text provided"); - Serial.println("Usage: msg "); - } - } - else if (cmdLower == "testmsg") { - Serial.println("\n>>> TEST MESSAGE COMMAND <<<"); - sendMessageToWristband("This is a test message from central node"); - } - else if (cmdLower == "msgstats") { - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.println("║ MESSAGE RELAY STATISTICS ║"); - Serial.println("╚════════════════════════════════════════════╝"); - Serial.println("Messages sent to edge node: " + String(messagesSentToEdge)); - Serial.println("LoRa status: " + String(loraReady ? "✓ Ready" : "✗ Not Ready")); - Serial.println("════════════════════════════════════════════\n"); - } - else if (cmdLower == "help" || cmdLower == "commands") { - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.println("║ MESSAGE RELAY COMMANDS ║"); - Serial.println("╠════════════════════════════════════════════╣"); - Serial.println("║ msg - Send message to wristband ║"); - Serial.println("║ testmsg - Send test message ║"); - Serial.println("║ msgstats - Show message statistics ║"); - Serial.println("║ help - Show this menu ║"); - Serial.println("╚════════════════════════════════════════════╝\n"); - } - } -} - - -void sendEmergencyEmail(JsonDocument& sensorData, int rssi, float snr) { - if (!wifiConnected) { - Serial.println("❌ ERROR: WiFi disconnected - cannot send email"); - return; - } - - Serial.println("\n╔══════════════════════════════════════╗"); - Serial.println("║ PREPARING EMERGENCY EMAIL ║"); - Serial.println("╚══════════════════════════════════════╝\n"); - - String nodeId = sensorData["node"] | "UNKNOWN"; - float temperature = sensorData["temp"] | 0; - float humidity = sensorData["hum"] | 0; - int mq2 = sensorData["mq2"] | 0; - int mq9 = sensorData["mq9"] | 0; - int mq135 = sensorData["mq135"] | 0; - bool mq2Digital = sensorData["mq2_digital"] | false; // ✅ CORRECT - bool mq9Digital = sensorData["mq9_digital"] | false; // ✅ CORRECT - bool mq135Digital = sensorData["mq135_digital"] | false; // ✅ CORRECT - int bpm = sensorData["bpm"] | 0; - int spo2 = sensorData["spo2"] | 0; - bool wristbandConnected = sensorData["wristband_connected"] | 0; - float motionAccel = sensorData["motion_accel"] | 0; - float motionGyro = sensorData["motion_gyro"] | 0; - - String subject = "🚨 URGENT: Mine Worker Emergency - Node " + nodeId; - - String emailBody = "╔═══════════════════════════════════════════╗\n"; - emailBody += "║ EMERGENCY DISTRESS SIGNAL DETECTED ║\n"; - emailBody += "╚═══════════════════════════════════════════╝\n\n"; - emailBody += "Node ID: " + nodeId + "\n"; - emailBody += "Timestamp: " + getCurrentDateTime() + "\n"; - emailBody += "Emergency Count: #" + String(emergenciesDetected) + "\n\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "ENVIRONMENTAL CONDITIONS:\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "Temperature: " + String(temperature, 1) + "°C\n"; - emailBody += "Humidity: " + String(humidity, 1) + "%\n"; - emailBody += "MQ2 (Smoke): " + String(mq2) + "\n"; - emailBody += "MQ9 (CO): " + String(mq9) + "\n"; - emailBody += "MQ135 (Quality): " + String(mq135) + "\n\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "MOTION & VITALS:\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "Acceleration: " + String(motionAccel, 2) + " m/s²\n"; - emailBody += "Gyro Rotation: " + String(motionGyro, 2) + " °/s\n"; - emailBody += "Heart Rate: " + String(bpm) + " BPM\n"; - emailBody += "Blood Oxygen: " + String(spo2) + "%\n"; - emailBody += "Wristband: " + String(wristbandConnected ? "✓ Connected" : "✗ Disconnected") + "\n\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "SIGNAL QUALITY:\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "RSSI: " + String(rssi) + " dBm\n"; - emailBody += "SNR: " + String(snr) + " dB\n\n"; - emailBody += "⚠️ IMMEDIATE ACTION REQUIRED!\n"; - emailBody += " Contact emergency response team.\n"; - - Serial.println("Email Subject: " + subject); - Serial.println("Email Length: " + String(emailBody.length()) + " characters"); - Serial.println(); - - // Try to send email with retries - for (int attempt = 1; attempt <= 3; attempt++) { - Serial.println("📧 Email Send Attempt " + String(attempt) + "/3..."); - - if (sendSMTPEmail(subject, emailBody)) { - emailsSent++; - Serial.println("\n✅ Email sent successfully!"); - Serial.println(" Total emails sent: " + String(emailsSent)); - Serial.println("╚══════════════════════════════════════╝\n"); - return; - } - - if (attempt < 3) { - Serial.println("❌ Attempt " + String(attempt) + " failed. Retrying in 5 seconds...\n"); - delay(5000); - } - } - - Serial.println("\n❌ ERROR: Email send failed after 3 attempts"); - Serial.println(" Check:"); - Serial.println(" 1. WiFi connection"); - Serial.println(" 2. SMTP credentials"); - Serial.println(" 3. Gmail app password"); - Serial.println(" 4. Internet connectivity"); - Serial.println("╚══════════════════════════════════════╝\n"); -} - -String readSMTPResponse(WiFiClient& client, int timeout) { - unsigned long start = millis(); - String response = ""; - - while (millis() - start < timeout) { - if (client.available()) { - char c = client.read(); - response += c; - if (c == '\n') { - Serial.print("SMTP: "); - Serial.print(response); - if (response.indexOf("250") >= 0 || response.indexOf("334") >= 0 || - response.indexOf("235") >= 0 || response.indexOf("220") >= 0 || - response.indexOf("354") >= 0) { - return response; - } - if (response.indexOf("5") == 0) { // Error codes start with 5 - Serial.println("SMTP ERROR: " + response); - return response; - } - response = ""; - } - } - delay(10); - } - - Serial.println("SMTP Timeout"); - return ""; -} - -bool sendSMTPEmail(String subject, String body) { - // Use secure client from the start and connect to port 465 (SSL/TLS) - // Gmail supports both 587 (STARTTLS) and 465 (direct SSL) - // Port 465 is simpler as it's SSL from the start - - WiFiClientSecure secureClient; - secureClient.setInsecure(); - - Serial.println("Connecting to SMTP server (SSL)..."); - if (!secureClient.connect(SMTP_SERVER, 465)) { // Use port 465 for direct SSL - Serial.println("ERROR: Cannot connect to SMTP server"); - return false; - } - Serial.println("Connected to SMTP server via SSL"); - - // Wait for greeting - String response = readSMTPResponse(secureClient, 10000); - if (response.indexOf("220") < 0) { - Serial.println("ERROR: No greeting from server"); - secureClient.stop(); - return false; - } - - // EHLO - secureClient.println("EHLO ESP32"); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("250") < 0) { - Serial.println("ERROR: EHLO failed"); - secureClient.stop(); - return false; - } - - // Clear any remaining response lines - delay(500); - while (secureClient.available()) secureClient.read(); - - // AUTH LOGIN - secureClient.println("AUTH LOGIN"); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("334") < 0) { - Serial.println("ERROR: AUTH LOGIN failed"); - secureClient.stop(); - return false; - } - - // Username - secureClient.println(base64Encode(SENDER_EMAIL)); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("334") < 0) { - Serial.println("ERROR: Username rejected"); - secureClient.stop(); - return false; - } - - // Password - secureClient.println(base64Encode(SENDER_PASSWORD)); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("235") < 0) { - Serial.println("ERROR: Authentication failed - Check app password"); - secureClient.stop(); - return false; - } - Serial.println("Authentication successful"); - - // MAIL FROM - secureClient.print("MAIL FROM:<"); - secureClient.print(SENDER_EMAIL); - secureClient.println(">"); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("250") < 0) { - Serial.println("ERROR: MAIL FROM rejected"); - secureClient.stop(); - return false; - } - - // RCPT TO - secureClient.print("RCPT TO:<"); - secureClient.print(SUPERVISOR_EMAIL); - secureClient.println(">"); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("250") < 0) { - Serial.println("ERROR: RCPT TO rejected"); - secureClient.stop(); - return false; - } - - // DATA - secureClient.println("DATA"); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("354") < 0) { - Serial.println("ERROR: DATA command rejected"); - secureClient.stop(); - return false; - } - - // Email headers and body - secureClient.print("From: "); - secureClient.println(SENDER_EMAIL); - secureClient.print("To: "); - secureClient.println(SUPERVISOR_EMAIL); - secureClient.print("Subject: "); - secureClient.println(subject); - secureClient.println(); - secureClient.println(body); - secureClient.println("."); - - response = readSMTPResponse(secureClient, 10000); - if (response.indexOf("250") < 0) { - Serial.println("ERROR: Message rejected"); - secureClient.stop(); - return false; - } - - // QUIT - secureClient.println("QUIT"); - secureClient.stop(); - - Serial.println("Email sent successfully!"); - return true; -} - -String base64Encode(String input) { - const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - String result = ""; - int val = 0, valb = 0; - - for (byte c : input) { - val = (val << 8) + c; - valb += 8; - while (valb >= 6) { - valb -= 6; - result += base64_chars[(val >> valb) & 0x3F]; - } - } - - if (valb > 0) result += base64_chars[(val << (6 - valb)) & 0x3F]; - while (result.length() % 4) result += "="; - return result; -} - -void uploadToSupabase(String jsonData) { - if (!supabaseReady) return; - - String url = String(SUPABASE_URL) + "/rest/v1/sensor_data"; - - http.begin(client, url); - http.addHeader("Content-Type", "application/json"); - http.addHeader("apikey", SUPABASE_ANON_KEY); - http.addHeader("Authorization", "Bearer " + String(SUPABASE_ANON_KEY)); - - int httpResponseCode = http.POST(jsonData); - - if (httpResponseCode == 201 || httpResponseCode == 200) { - packetsUploaded++; - Serial.println("✓ Uploaded to Supabase\n"); - } else { - Serial.println("✗ Upload failed: " + String(httpResponseCode)); - String response = http.getString(); - Serial.println("Response: " + response + "\n"); - } - - http.end(); -} - -String getCurrentDateTime() { - if (!ntpSynced) { - return String(millis()/1000) + "s"; - } - - struct tm timeinfo; - if (!getLocalTime(&timeinfo)) { - return "Error"; - } - - char timeString[64]; - strftime(timeString, sizeof(timeString), "%Y-%m-%dT%H:%M:%S+05:30", &timeinfo); - return String(timeString); -} - -void displayStatistics() { - Serial.println("\n╔═══════════════════════════════════════╗"); - Serial.println("║ SYSTEM STATISTICS ║"); - Serial.println("╠═══════════════════════════════════════╣"); - Serial.println("║ Component Status: ║"); - Serial.println("║ WiFi: " + String(wifiConnected ? "✓ Connected " : "✗ Disconnected") + " ║"); - Serial.println("║ Supabase: " + String(supabaseReady ? "✓ Ready " : "✗ Not Ready ") + " ║"); - Serial.println("║ NTP: " + String(ntpSynced ? "✓ Synced " : "✗ Not Synced ") + " ║"); - Serial.println("║ LoRa: " + String(loraReady ? "✓ Active " : "✗ Offline ") + " ║"); - Serial.println("╠═══════════════════════════════════════╣"); - Serial.println("║ Packet Statistics: ║"); - Serial.println("║ Received: " + String(packetsReceived) + String(21 - String(packetsReceived).length(), ' ') + "║"); - Serial.println("║ Uploaded: " + String(packetsUploaded) + String(21 - String(packetsUploaded).length(), ' ') + "║"); - Serial.println("║ Corrupted: " + String(packetsCorrupted) + String(21 - String(packetsCorrupted).length(), ' ') + "║"); - - // ========== ADD THESE LINES HERE ========== - Serial.println("╠═══════════════════════════════════════╣"); - Serial.println("║ Message Relay Statistics: ║"); - Serial.println("║ Messages Sent: " + String(messagesSentToEdge) + String(17 - String(messagesSentToEdge).length(), ' ') + "║"); - // ========== END OF NEW LINES ========== - - Serial.println("╠═══════════════════════════════════════╣"); - Serial.println("║ Emergency Statistics: ║"); - Serial.println("║ 🚨 Emergencies: " + String(emergenciesDetected) + String(19 - String(emergenciesDetected).length(), ' ') + "║"); - Serial.println("║ 📧 Emails Sent: " + String(emailsSent) + String(19 - String(emailsSent).length(), ' ') + "║"); -Serial.println("╠═══════════════════════════════════════╣"); -Serial.println("║ Active Nodes (Packet Counts): ║"); -if (trackedNodesCount == 0) { - Serial.println("║ No nodes tracked yet ║"); -} else { - for (int i = 0; i < trackedNodesCount; i++) { - String nodeInfo = "║ " + nodeTrackers[i].nodeId + ": " + String(nodeTrackers[i].packetCount) + " pkts"; - int padding = 40 - nodeInfo.length(); - Serial.println(nodeInfo + String(padding, ' ') + "║"); - } -} -Serial.println("╚═══════════════════════════════════════╝\n"); - - Serial.println("Current Time: " + getCurrentDateTime()); - Serial.println(); -} diff --git a/IshitaKaGhr/edge.ino b/IshitaKaGhr/edge.ino deleted file mode 100644 index d8b2c88..0000000 --- a/IshitaKaGhr/edge.ino +++ /dev/null @@ -1,1773 +0,0 @@ -/* - edge_node_espnow.ino - Edge Node with ESP-NOW integration for wristband vitals & text relay. - Full, self-contained sketch. Make sure to copy the entire file into Arduino IDE, - no extra lines above the first #include. -*/ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// ========================= MPU6050 I2C Register Definitions ========================= -#define MPU6050_ADDR 0x68 -#define PWR_MGMT_1 0x6B -#define ACCEL_XOUT_H 0x3B -#define GYRO_XOUT_H 0x43 -#define CONFIG 0x1A -#define GYRO_CONFIG 0x1B -#define ACCEL_CONFIG 0x1C -#define WHO_AM_I 0x75 - -// ========================= Pin definitions (preserved) ========================= -// MQ sensors -#define MQ2_DIGITAL_PIN 27 -#define MQ2_ANALOG_PIN 32 -#define MQ9_DIGITAL_PIN 14 -#define MQ9_ANALOG_PIN 33 -#define MQ135_DIGITAL_PIN 13 -#define MQ135_ANALOG_PIN 35 - -// FN-M16P Audio Module pins (UART2) -#define FN_M16P_RX 16 -#define FN_M16P_TX 17 - -// DHT11 pin -#define DHT11_PIN 25 -#define DHT_TYPE DHT11 - -// MPU6050 pins (I2C) -#define MPU6050_SDA 21 -#define MPU6050_SCL 22 -#define MPU6050_INT 34 - -// LoRa Module pins (SPI) -#define LORA_SCK 18 -#define LORA_MISO 19 -#define LORA_MOSI 23 -#define LORA_SS 5 -#define LORA_RST 4 -#define LORA_DIO0 26 - -// EMERGENCY BUTTON PIN -#define EMERGENCY_BUTTON_PIN 15 - -// LoRa frequency -#define LORA_BAND 433E6 - -// ========================= System constants ========================= -#define NODE_ID "001" - -#define MQ2_DANGER_THRESHOLD 1000 -#define MQ9_DANGER_THRESHOLD 3800 -#define MQ135_DANGER_THRESHOLD 1800 - -#define FALLBACK_TEMPERATURE 27.0 -#define FALLBACK_HUMIDITY 47.0 - -#define CALIBRATION_SAMPLES 10 -#define DANGER_MULTIPLIER 2.0 -#define CALIBRATION_DELAY 2000 - -bool pendingEmergency = false; - -const int TAP_TIMEOUT = 600; -const int REQUIRED_TAPS = 3; - -const byte FRAME_START = 0x7E; -const byte FRAME_END = 0xEF; -const byte VERSION = 0xFF; -const byte NO_FEEDBACK = 0x00; -const byte WITH_FEEDBACK = 0x01; -const byte CMD_PLAY_TRACK = 0x03; -const byte CMD_VOLUME = 0x06; -const byte CMD_STOP = 0x16; - -// Audio files mapping -enum AudioFiles { - BOOT_AUDIO = 1, // 0001.mp3 - System Boot - SMOKE_ALERT = 2, // 0002.mp3 - Smoke and Gas - CO_ALERT = 3, // 0003.mp3 - Carbon Monoxide - AIR_QUALITY_WARNING = 4, // 0004.mp3 - Air Quality Warning - HIGH_TEMP_ALERT = 5, // 0005.mp3 - High Temperature - LOW_TEMP_ALERT = 6, // 0006.mp3 - Low Temperature - HIGH_HUMIDITY_ALERT = 7, // 0007.mp3 - High Humidity - LOW_HUMIDITY_ALERT = 8, // 0008.mp3 - Low Humidity - MESSAGE_RECEIVED = 9, // 0009.mp3 - NEW: Message received on wristband - EMERGENCY_TRIPLE_TAP = 10, // 0010.mp3 - NEW: Triple tap emergency - FALL_ALERT = 11 // 0011.mp3 - NEW: Fall detection alert -}; - -// ========================= ESP-NOW message types ========================= -#define MSG_TYPE_VITALS 0x01 -#define MSG_TYPE_TEXT 0x02 -#define MSG_TYPE_ACK 0x03 - -#define MAX_TEXT_LEN 128 - -typedef struct __attribute__((packed)) { - uint8_t msgType; - uint8_t bpm; - uint8_t spo2; - uint8_t finger; - int8_t temperature; // body temp from wristband (°C) - uint32_t timestamp; -} espnow_vitals_t; - -typedef struct __attribute__((packed)) { - uint8_t msgType; // MSG_TYPE_TEXT - uint32_t messageId; - uint8_t length; - char text[MAX_TEXT_LEN]; -} espnow_text_t; - -typedef struct __attribute__((packed)) { - uint8_t msgType; // MSG_TYPE_ACK - uint32_t messageId; - uint8_t success; // 0/1 -} espnow_ack_t; - -// ========================= Types & Globals ========================= -struct SensorCalibration { - float baseline; - float dangerThreshold; - bool calibrated; -}; - -struct MotionData { - float totalAccel; - float totalGyro; - bool fallDetected; - bool impactDetected; - bool motionDetected; - unsigned long lastMotionTime; -}; - -struct SensorData { - float temperature; - float humidity; - int mq2_analog; - int mq9_analog; - int mq135_analog; - bool mq2_digital; - bool mq9_digital; - bool mq135_digital; - bool emergency; - unsigned long timestamp; - MotionData motion; -}; - -SensorCalibration mq2_cal = {0,0,false}; -SensorCalibration mq9_cal = {0,0,false}; -SensorCalibration mq135_cal = {0,0,false}; - -DHT dht(DHT11_PIN, DHT_TYPE); -HardwareSerial fnM16pSerial(2); - -bool audioReady = false; -bool loraReady = false; -bool dhtReady = false; -bool mpuReady = false; - -unsigned long lastLoRaSend = 0; -int loraInterval = 30000; -int packetCount = 0; - -unsigned long lastDHTReading = 0; -const unsigned long DHT_READING_INTERVAL = 2000; -float lastValidTemperature = FALLBACK_TEMPERATURE; -float lastValidHumidity = FALLBACK_HUMIDITY; - -// Emergency Button Variables -volatile int tapCount = 0; -volatile unsigned long lastTapTime = 0; -volatile bool emergencyTriggered = false; - -// Motion variables -MotionData motionData = {0}; - -// MPU internals -const float G = 9.80665f; -const float FREE_FALL_G_THRESHOLD = 0.6f; -const unsigned long FREE_FALL_MIN_MS = 120; -const float IMPACT_G_THRESHOLD = 3.5f; -const unsigned long IMPACT_WINDOW_MS = 1200; -const unsigned long STATIONARY_CONFIRM_MS = 800; -const float ROTATION_IMPACT_THRESHOLD = 400.0f; - -bool inFreeFall = false; -bool fallInProgress = false; -bool impactSeen = false; -unsigned long freeFallStart = 0; -unsigned long fallStartTime = 0; -unsigned long impactTime = 0; -unsigned long stationarySince = 0; -float accelFiltered = G; -const float ALPHA = 0.85f; - -unsigned long lastI2CAttempt = 0; - -// ========================= ESP-NOW / Wristband status globals ========================= -struct WristbandStatus { - uint8_t bpm; - uint8_t spo2; - bool fingerDetected; - float bodyTemp; - unsigned long lastUpdate; - bool connected; - uint32_t lastMessageId; - bool messageAcknowledged; -} wristbandStatus = {0, 0, false, 0.0f, 0, false, 0, false}; - -// Wristband MAC address (update if different) -uint8_t wristbandMac[6] = {0x0C, 0x4E, 0xA0, 0x66, 0xB2, 0x78}; - -bool espnowReady = false; -esp_err_t lastEspNowSendStatus = ESP_OK; -uint32_t outgoingMessageCounter = 1; - -unsigned long messagesRelayedToWristband = 0; - -// ========================= Forward declarations ========================= -void printTestMenu(); -void handleTestCommand(String cmd); -void testDHT(); -void testMQ2(); -void testMQ9(); -void testMQ135(); -void testAllMQ(); -void testAudio(int fileNum); -void testLoRa(); -void testEmergency(); -void testButton(); -void testAllSensors(); -void printSystemStatus(); -void scanI2CDevices(); -void setVolume(int volume); -void playAudioFile(int fileNumber); -void stopAudio(); -void calibrateSensors(); -void calibrateSensor(int pin, SensorCalibration* cal, String sensorName, int minThreshold); -bool checkSensorDanger(int currentValue, SensorCalibration* cal, int staticDangerThreshold); -SensorData readAllSensors(); -void displayReadings(SensorData data); -void checkAlerts(SensorData data); -void sendLoRaData(SensorData data); -String getAirQualityRating(int value); -void checkEmergencyButton(); -void handleEmergency(); -void sendCommand(byte cmd, byte param1, byte param2, bool feedback); - -// I2C helpers & MPU -bool i2cBusRecover(); -bool safeWireRequest(uint8_t addr, uint8_t reg, uint8_t *buf, size_t len, int retries=3); -bool safeWireWrite(uint8_t addr, uint8_t reg, uint8_t val, int retries=3); -bool initMPU6050(); -void readMPU6050Data(int16_t *ax, int16_t *ay, int16_t *az, int16_t *gx, int16_t *gy, int16_t *gz); -void monitorMotion(); -void detectFallAndHandle(); - -// ESP-NOW -void initESPNOW(); -void onDataRecv(const esp_now_recv_info_t *recv_info, const uint8_t *data, int len); -void onDataSent(const wifi_tx_info_t *tx_info, esp_now_send_status_t status); -bool sendTextToWristband(const String &message); -void checkWristbandConnection(); -void receiveLoRaMessages(); - -// -------------------------- Implementations -------------------------- -// I2C recovery & safeWire -bool i2cBusRecover() { - Wire.end(); - delay(10); - pinMode(MPU6050_SCL, OUTPUT); - pinMode(MPU6050_SDA, INPUT_PULLUP); - if (digitalRead(MPU6050_SDA) == HIGH) { - Wire.begin(MPU6050_SDA, MPU6050_SCL); - Wire.setClock(100000); - return true; - } - for (int i=0;i<9;i++) { - digitalWrite(MPU6050_SCL, HIGH); - delayMicroseconds(500); - digitalWrite(MPU6050_SCL, LOW); - delayMicroseconds(500); - if (digitalRead(MPU6050_SDA) == HIGH) break; - } - pinMode(MPU6050_SDA, OUTPUT); - digitalWrite(MPU6050_SDA, LOW); - delayMicroseconds(200); - digitalWrite(MPU6050_SCL, HIGH); - delayMicroseconds(200); - digitalWrite(MPU6050_SDA, HIGH); - delayMicroseconds(200); - Wire.begin(MPU6050_SDA, MPU6050_SCL); - Wire.setClock(100000); - pinMode(MPU6050_SDA, INPUT_PULLUP); - pinMode(MPU6050_SCL, INPUT_PULLUP); - return digitalRead(MPU6050_SDA) == HIGH; -} - -bool safeWireRequest(uint8_t addr, uint8_t reg, uint8_t *buf, size_t len, int retries) { - for (int attempt=0; attempt 0.2f * G || motionData.totalGyro > 25.0f) { - motionData.lastMotionTime = millis(); - motionData.motionDetected = true; - } else motionData.motionDetected = false; - detectFallAndHandle(); -} - -void detectFallAndHandle() { - unsigned long now = millis(); - float totG = motionData.totalAccel / G; - float totGyro = motionData.totalGyro; - if (totG < FREE_FALL_G_THRESHOLD) { - if (!inFreeFall) { inFreeFall = true; freeFallStart = now; } - else if ((now - freeFallStart) >= FREE_FALL_MIN_MS && !fallInProgress) { - fallInProgress = true; - fallStartTime = now; - impactSeen = false; - motionData.impactDetected = false; - } - } else { if (inFreeFall) inFreeFall = false; } - if (fallInProgress && !impactSeen) { - if (totG >= IMPACT_G_THRESHOLD || totGyro >= ROTATION_IMPACT_THRESHOLD) { - impactSeen = true; impactTime = now; motionData.impactDetected = true; - } else if (now - fallStartTime > IMPACT_WINDOW_MS) { fallInProgress = false; impactSeen = false; motionData.impactDetected = false; } - } - if (impactSeen) { - float accelVariationG = fabs((motionData.totalAccel / G) - 1.0f); - if (accelVariationG < 0.35f && motionData.totalGyro < 50.0f) { - if (stationarySince == 0) stationarySince = now; - if (now - stationarySince >= STATIONARY_CONFIRM_MS) { - motionData.fallDetected = true; - emergencyTriggered = true; - if (audioReady) playAudioFile(EMERGENCY_TRIPLE_TAP); - Serial.println("\n╔════════════════════════════════════╗"); - Serial.println("║ FALL CONFIRMED - IMPACT + STATIONARY ║"); - Serial.println("╚════════════════════════════════════╝"); - Serial.printf("Acceleration: %.2f g\n", motionData.totalAccel / G); - Serial.printf("Gyroscope: %.2f °/s\n", motionData.totalGyro); - fallInProgress = false; impactSeen = false; stationarySince = 0; - } - } else { - stationarySince = 0; - if (now - impactTime > IMPACT_WINDOW_MS) { fallInProgress = false; impactSeen = false; stationarySince = 0; motionData.impactDetected = false; } - } - } -} - -// Air Quality Rating function -String getAirQualityRating(int value) { - if (value < 800) return "Excellent"; - else if (value < 1200) return "Good"; - else if (value < 1800) return "Moderate"; - else if (value < 2400) return "Poor"; - else return "Very Poor"; -} - -// Emergency Button handler -void checkEmergencyButton() { - static bool lastState = HIGH; // using INPUT_PULLUP -> HIGH when released - static unsigned long lastChangeTime = 0; - bool currentState = digitalRead(EMERGENCY_BUTTON_PIN); - if (currentState != lastState && (millis() - lastChangeTime > 50)) { - lastChangeTime = millis(); - if (currentState == LOW) { // pressed - unsigned long now = millis(); - if (now - lastTapTime < TAP_TIMEOUT) tapCount++; - else tapCount = 1; - lastTapTime = now; - Serial.printf("BUTTON TAP #%d/%d\n", tapCount, REQUIRED_TAPS); - if (tapCount >= REQUIRED_TAPS) { - Serial.println("TRIPLE TAP - EMERGENCY TRIGGERED"); - emergencyTriggered = true; - tapCount = 0; - } - } - } - if (millis() - lastTapTime > TAP_TIMEOUT && tapCount > 0) tapCount = 0; - lastState = currentState; -} - -// Emergency Handler function -void handleEmergency() { - Serial.println("\n████████ EMERGENCY MODE █████████"); - - // Play emergency audio FIRST before doing anything else - if (audioReady) { - playAudioFile(EMERGENCY_TRIPLE_TAP); // Play 0010.mp3 for triple tap emergency - Serial.println("✓ Playing emergency triple-tap audio"); - delay(100); // Small delay to ensure audio command is sent - } - - // Set the pending emergency flag so the main loop will send it - pendingEmergency = true; - - SensorData s = readAllSensors(); - s.emergency = true; - s.motion = motionData; - - Serial.println("EMERGENCY SNAPSHOT:"); - Serial.printf(" Temp: %.2f C Hum: %.2f %%\n", s.temperature, s.humidity); - Serial.printf(" MQ2:%d MQ9:%d MQ135:%d\n", s.mq2_analog, s.mq9_analog, s.mq135_analog); - Serial.printf(" Fall: %s\n", s.motion.fallDetected ? "YES":"NO"); - - // Send emergency packet immediately - if (loraReady) { - sendLoRaData(s); - Serial.println("✓ Emergency packet sent via LoRa"); - } else { - Serial.println("⚠ LoRa not ready - emergency packet queued"); - } - - delay(500); // Give some time for audio to play -} - -// -------------------------- Setup & Loop -------------------------- -/* - COMPLETE SETUP AND LOOP FOR EDGE NODE - Optimized boot sequence to prevent conflicts between: - - LoRa (SPI) - - ESP-NOW (WiFi) - - MPU6050 (I2C) - - All other peripherals -*/ - -// ==================== SETUP FUNCTION ==================== -void setup() { - Serial.begin(115200); - delay(1000); - - Serial.println("\n\n"); - Serial.println("╔════════════════════════════════════════════════════╗"); - Serial.println("║ ESP32 Multi-Sensor Edge Node v3.0 ║"); - Serial.println("║ Optimized Boot Sequence ║"); - Serial.println("╚════════════════════════════════════════════════════╝\n"); - - // ======================================== - // PHASE 1: GPIO INITIALIZATION (No conflicts) - // ======================================== - Serial.println("PHASE 1: Initializing GPIO pins..."); - - pinMode(MQ2_DIGITAL_PIN, INPUT); - pinMode(MQ9_DIGITAL_PIN, INPUT); - pinMode(MQ135_DIGITAL_PIN, INPUT); - pinMode(EMERGENCY_BUTTON_PIN, INPUT_PULLUP); - pinMode(MPU6050_INT, INPUT); - - Serial.println("✓ GPIO pins configured"); - delay(100); - - // ======================================== - // PHASE 2: I2C INITIALIZATION (MPU6050) - // Do this BEFORE SPI to avoid bus conflicts - // ======================================== - Serial.println("\nPHASE 2: Initializing I2C (MPU6050)..."); - - Wire.begin(MPU6050_SDA, MPU6050_SCL); - Wire.setClock(100000); // 100kHz for compatibility - pinMode(MPU6050_SDA, INPUT_PULLUP); - pinMode(MPU6050_SCL, INPUT_PULLUP); - delay(100); - - // Multiple attempts for MPU6050 (clones can be finicky) - mpuReady = false; - for (int attempt = 1; attempt <= 3; attempt++) { - Serial.printf(" MPU6050 initialization attempt %d/3...\n", attempt); - if (initMPU6050()) { - mpuReady = true; - Serial.println("✓ MPU6050 initialized successfully"); - motionData.lastMotionTime = millis(); - accelFiltered = G; - break; - } - delay(500); - } - - if (!mpuReady) { - Serial.println("⚠ MPU6050 initialization failed - motion features disabled"); - Serial.println(" System will continue without motion detection"); - } - delay(200); - - // ======================================== - // PHASE 3: SPI INITIALIZATION (LoRa) - // Do this BEFORE WiFi to claim SPI bus - // ======================================== - Serial.println("\nPHASE 3: Initializing SPI (LoRa)..."); - - // Prepare LoRa pins - pinMode(LORA_SS, OUTPUT); - digitalWrite(LORA_SS, HIGH); - pinMode(LORA_RST, OUTPUT); - digitalWrite(LORA_RST, HIGH); - - // Initialize SPI bus - SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_SS); - delay(50); - - // Set LoRa pins - LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0); - - // Hardware reset sequence - digitalWrite(LORA_RST, LOW); - delay(10); - digitalWrite(LORA_RST, HIGH); - delay(10); - - // Initialize LoRa - loraReady = false; - if (!LoRa.begin(LORA_BAND)) { - Serial.println("✗ LoRa initialization FAILED"); - Serial.println(" Check: Wiring, antenna, power supply"); - } else { - // CRITICAL: Configure LoRa parameters to match central node - LoRa.setTxPower(20); // Maximum power - LoRa.setSpreadingFactor(12); // Maximum range - LoRa.setSignalBandwidth(125E3); // 125 kHz - LoRa.setCodingRate4(8); // Error correction - LoRa.setPreambleLength(8); // Standard preamble - LoRa.setSyncWord(0x34); // Must match central node - LoRa.enableCrc(); // Enable CRC checking - LoRa.setOCP(240); // Over current protection (240mA max) - - loraReady = true; - Serial.println("✓ LoRa initialized successfully"); - Serial.println(" Configuration:"); - Serial.println(" - Frequency: 433 MHz"); // Changed from 915 MHz - Serial.println(" - TX Power: 20 dBm"); - Serial.println(" - Spreading Factor: 12"); - Serial.println(" - Bandwidth: 125 kHz"); - Serial.println(" - Coding Rate: 4/8"); - Serial.println(" - Sync Word: 0x34"); - } - delay(200); - - // ======================================== - // PHASE 4: WiFi INITIALIZATION (for ESP-NOW) - // Do this AFTER LoRa to avoid interference - // ======================================== - Serial.println("\nPHASE 4: Initializing WiFi (ESP-NOW)..."); - - // Turn off WiFi completely first - WiFi.disconnect(true); - WiFi.mode(WIFI_OFF); - delay(100); - - // Now configure for ESP-NOW (Station mode only) - WiFi.mode(WIFI_STA); - WiFi.disconnect(false); // Don't erase config - delay(100); - - // Set WiFi to low power to minimize interference with LoRa - esp_wifi_set_ps(WIFI_PS_MIN_MODEM); - - // Set WiFi channel (same as wristband) - int wifiChannel = 1; - esp_wifi_set_promiscuous(true); -esp_err_t channelResult = esp_wifi_set_channel(wifiChannel, WIFI_SECOND_CHAN_NONE); -esp_wifi_set_promiscuous(false); -delay(100); - if (channelResult == ESP_OK) { - Serial.printf("✓ WiFi channel set to %d\n", wifiChannel); - } else { - Serial.printf("⚠ Failed to set WiFi channel (error: %d)\n", channelResult); - } - - delay(200); - - // ======================================== - // PHASE 5: ESP-NOW INITIALIZATION - // ======================================== - Serial.println("\nPHASE 5: Initializing ESP-NOW..."); - - espnowReady = false; - - // Initialize ESP-NOW - esp_err_t espnowResult = esp_now_init(); - if (espnowResult != ESP_OK) { - Serial.printf("✗ ESP-NOW init failed (error: %d)\n", espnowResult); - Serial.println(" Retrying once..."); - delay(500); - espnowResult = esp_now_init(); - } - - if (espnowResult == ESP_OK) { - espnowReady = true; - Serial.println("✓ ESP-NOW initialized"); - - // Register callbacks - esp_now_register_send_cb(onDataSent); - esp_now_register_recv_cb(onDataRecv); - delay(100); - - // Add wristband as peer - esp_now_peer_info_t peerInfo; - memset(&peerInfo, 0, sizeof(peerInfo)); - memcpy(peerInfo.peer_addr, wristbandMac, 6); - peerInfo.channel = wifiChannel; - peerInfo.encrypt = false; - - #if defined(ESP_IF_WIFI_STA) - peerInfo.ifidx = ESP_IF_WIFI_STA; - #elif defined(WIFI_IF_STA) - peerInfo.ifidx = WIFI_IF_STA; - #else - peerInfo.ifidx = (wifi_interface_t)0; - #endif - - if (!esp_now_is_peer_exist(wristbandMac)) { - esp_err_t addPeerResult = esp_now_add_peer(&peerInfo); - if (addPeerResult == ESP_OK) { - Serial.println("✓ Wristband peer added"); - Serial.printf(" MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", - wristbandMac[0], wristbandMac[1], wristbandMac[2], - wristbandMac[3], wristbandMac[4], wristbandMac[5]); - } else { - Serial.printf("⚠ Failed to add wristband peer (error: %d)\n", addPeerResult); - } - } else { - Serial.println("✓ Wristband peer already exists"); - } - } else { - Serial.println("✗ ESP-NOW initialization failed"); - Serial.println(" System will continue without wristband communication"); - } - - delay(200); - - // ======================================== - // PHASE 6: UART INITIALIZATION (Audio Module) - // ======================================== - Serial.println("\nPHASE 6: Initializing UART (Audio)..."); - - fnM16pSerial.begin(9600, SERIAL_8N1, FN_M16P_RX, FN_M16P_TX); - delay(200); - - // Set initial volume - setVolume(25); - delay(100); - - audioReady = true; - Serial.println("✓ Audio module initialized (volume: 25)"); - delay(200); - - // ======================================== - // PHASE 7: DHT11 INITIALIZATION - // ======================================== - Serial.println("\nPHASE 7: Initializing DHT11..."); - - dht.begin(); - delay(1500); // DHT11 needs time to stabilize - - // Test DHT11 with multiple attempts - dhtReady = false; - for (int attempt = 1; attempt <= 3; attempt++) { - Serial.printf(" DHT11 reading attempt %d/3...\n", attempt); - float t = dht.readTemperature(); - float h = dht.readHumidity(); - - if (!isnan(t) && !isnan(h)) { - dhtReady = true; - lastValidTemperature = t; - - // Apply humidity correction - float corrected = h - 30.0f; - if (corrected < 0.0f) corrected = 0.0f; - if (corrected > 100.0f) corrected = 100.0f; - lastValidHumidity = corrected; - - Serial.printf("✓ DHT11 initialized: %.1f°C, %.1f%% (corrected)\n", - lastValidTemperature, lastValidHumidity); - break; - } - delay(1000); - } - - if (!dhtReady) { - Serial.println("⚠ DHT11 initialization failed"); - Serial.println(" Using fallback values: 27.0°C, 47.0%"); - Serial.println(" Check: DATA pin → GPIO25, VCC → 3.3V, GND"); - } - - delay(200); - - // ======================================== - // PHASE 8: GAS SENSOR WARMUP & CALIBRATION - // ======================================== - Serial.println("\nPHASE 8: Gas sensor warmup..."); - Serial.println(" Please wait 15 seconds for sensors to stabilize"); - - // Progress indicator - for (int i = 0; i < 15; i++) { - Serial.print("."); - delay(1000); - } - Serial.println(" Done!"); - - Serial.println("\nCalibrating MQ sensors..."); - calibrateSensors(); - - delay(200); - - // ======================================== - // PHASE 9: SYSTEM READY - // ======================================== - Serial.println("\n╔════════════════════════════════════════════════════╗"); - Serial.println("║ SYSTEM INITIALIZATION COMPLETE ║"); - Serial.println("╚════════════════════════════════════════════════════╝\n"); - - // Print status summary - printBootSummary(); - - // Play boot sound - if (audioReady) { - Serial.println("Playing boot audio..."); - playAudioFile(BOOT_AUDIO); - delay(1500); - } - - // Print menu - printTestMenu(); - - Serial.println("\n🚀 Edge node is now operational!"); - Serial.println("Listening for commands and monitoring sensors...\n"); -} - -// ==================== LOOP FUNCTION ==================== -void loop() { - static unsigned long lastNormalReading = 0; - static unsigned long lastStatusPrint = 0; - - // ======================================== - // PRIORITY 1: Check for serial commands - // ======================================== - if (Serial.available() > 0) { - String cmd = Serial.readStringUntil('\n'); - cmd.trim(); - cmd.toLowerCase(); - if (cmd.length() > 0) { - handleTestCommand(cmd); - } - } - - // ======================================== - // PRIORITY 2: Emergency button monitoring (high frequency) - // ======================================== - checkEmergencyButton(); - - // ======================================== - // PRIORITY 3: Motion detection (if available) - // ======================================== - if (mpuReady) { - monitorMotion(); - } - - // ======================================== - // PRIORITY 4: Handle emergency trigger - // ======================================== - if (emergencyTriggered || pendingEmergency) { - handleEmergency(); - emergencyTriggered = false; - motionData.fallDetected = false; - pendingEmergency = false; // Clear the flag after handling -} - - // ======================================== - // PRIORITY 5: Check for incoming LoRa messages - // ======================================== - if (loraReady) { - receiveLoRaMessages(); - } - - // ======================================== - // PRIORITY 6: Wristband connection monitoring - // ======================================== - if (espnowReady) { - checkWristbandConnection(); - } - - // ======================================== - // PRIORITY 7: Normal sensor reading & transmission (10 second interval) - // ======================================== - if (millis() - lastNormalReading >= 10000) { - lastNormalReading = millis(); - - // Read all sensors - SensorData data = readAllSensors(); - data.emergency = false; // Normal reading, not emergency - data.motion = motionData; - - // Display readings - displayReadings(data); - - // Check for alerts - checkAlerts(data); - - // Send via LoRa if ready and interval passed - if (loraReady && (millis() - lastLoRaSend >= loraInterval)) { - sendLoRaData(data); - lastLoRaSend = millis(); - } - - Serial.println("────────────────────────────────────"); - } - - // ======================================== - // PRIORITY 8: Periodic status update (60 second interval) - // ======================================== - if (millis() - lastStatusPrint >= 60000) { - lastStatusPrint = millis(); - printSystemStatus(); - } - - // ======================================== - // Small delay to prevent watchdog issues - // ======================================== - delay(50); -} - -// ==================== HELPER FUNCTIONS ==================== - -void printBootSummary() { - Serial.println("Component Status:"); - Serial.println("┌─────────────────────┬─────────┐"); - Serial.printf("│ %-19s │ %7s │\n", "LoRa Module", loraReady ? "✓ OK" : "✗ FAIL"); - Serial.printf("│ %-19s │ %7s │\n", "ESP-NOW", espnowReady ? "✓ OK" : "✗ FAIL"); - Serial.printf("│ %-19s │ %7s │\n", "MPU6050 Motion", mpuReady ? "✓ OK" : "✗ FAIL"); - Serial.printf("│ %-19s │ %7s │\n", "DHT11 Temp/Hum", dhtReady ? "✓ OK" : "⚠ WARN"); - Serial.printf("│ %-19s │ %7s │\n", "Audio Module", audioReady ? "✓ OK" : "✗ FAIL"); - Serial.printf("│ %-19s │ %7s │\n", "MQ Gas Sensors", "✓ OK"); - Serial.printf("│ %-19s │ %7s │\n", "Emergency Button", "✓ OK"); - Serial.println("└─────────────────────┴─────────┘"); - - // Critical warnings - if (!loraReady) { - Serial.println("\n⚠ WARNING: LoRa not functional - cannot send data!"); - } - if (!espnowReady) { - Serial.println("\n⚠ WARNING: ESP-NOW not functional - no wristband communication!"); - } - if (!mpuReady) { - Serial.println("\n⚠ WARNING: Motion detection disabled - fall detection unavailable!"); - } - - Serial.println(); -} - -// ---------------- Test commands & helpers ---------------- -void printTestMenu() { - Serial.println("\n╔═══════════════════════════════════════╗"); - Serial.println("║ TEST COMMANDS MENU ║"); - Serial.println("╠═══════════════════════════════════════╣"); - Serial.println("║ dht, mq2, mq9, mq135, mq, all ║"); - Serial.println("║ audio1..audio10, stop, volume+, volume-║"); - Serial.println("║ lora, button, emergency, calibrate ║"); - Serial.println("║ status, scan/i2c, help/menu ║"); - Serial.println("║ sendmsg (ESP-NOW -> wristband)║"); - // ========== ADD THIS LINE ========== - Serial.println("║ relaystats (Message relay stats) ║"); - // ========== END OF NEW LINE ========== - Serial.println("╚═══════════════════════════════════════╝\n"); -} - -void handleTestCommand(String cmd) { - Serial.println("\n>>> EXECUTING TEST: " + cmd + " <<<\n"); - if (cmd == "help" || cmd == "menu") printTestMenu(); - else if (cmd == "dht") testDHT(); - else if (cmd == "mq2") testMQ2(); - else if (cmd == "mq9") testMQ9(); - else if (cmd == "mq135") testMQ135(); - else if (cmd == "mq") testAllMQ(); - else if (cmd.startsWith("audio")) { - int n = cmd.substring(5).toInt(); - if (n >= 1 && n <= 10) testAudio(n); - } else if (cmd == "stop") { stopAudio(); Serial.println("Audio stopped."); } - else if (cmd == "volume+") { setVolume(25); Serial.println("Volume 25"); } - else if (cmd == "volume-") { setVolume(15); Serial.println("Volume 15"); } - else if (cmd == "lora") testLoRa(); - else if (cmd == "button") testButton(); - else if (cmd == "emergency") testEmergency(); - else if (cmd == "all") testAllSensors(); - else if (cmd == "calibrate") calibrateSensors(); - else if (cmd == "status") printSystemStatus(); - else if (cmd == "scan" || cmd == "i2c") scanI2CDevices(); - else if (cmd.startsWith("sendmsg ")) { - String txt = cmd.substring(8); - if (txt.length() == 0) Serial.println("No message provided."); - else { - bool ok = sendTextToWristband(txt); - Serial.printf("sendTextToWristband('%s') => %s\n", txt.c_str(), ok ? "SENT":"FAILED"); - } - } - // ========== ADD THIS BLOCK HERE ========== - else if (cmd == "relaystats") { - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.println("║ MESSAGE RELAY STATISTICS ║"); - Serial.println("╚════════════════════════════════════════════╝"); - Serial.printf("Messages relayed to wristband: %lu\n", messagesRelayedToWristband); - Serial.printf("Last message ID sent: %lu\n", (unsigned long)(outgoingMessageCounter - 1)); - Serial.printf("ESP-NOW status: %s\n", espnowReady ? "✓ Ready" : "✗ Not Ready"); - Serial.printf("Wristband connected: %s\n", wristbandStatus.connected ? "✓ Yes" : "✗ No"); - if (wristbandStatus.lastMessageId > 0) { - Serial.printf("Last message acknowledged: %s\n", - wristbandStatus.messageAcknowledged ? "✓ Yes" : "✗ No"); - } - Serial.println("════════════════════════════════════════════\n"); - } - // ========== END OF NEW BLOCK ========== - else Serial.println("❌ Unknown command: " + cmd); - Serial.println("\n>>> TEST COMPLETE <<<\n"); -} - -void testDHT() { - Serial.println("Testing DHT11 (5 samples) with manual -30% humidity correction:"); - for (int i=0;i<5;i++) { - float t = dht.readTemperature(); - float h = dht.readHumidity(); - if (!isnan(t) && !isnan(h)) { - float corr = h - 30.0f; - if (corr < 0) corr = 0; if (corr > 100) corr = 100; - Serial.printf(" #%d: Temp=%.2f C, Hum(corrected)=%.2f %%\n", i+1, t, corr); - } else { - Serial.printf(" #%d: FAILED (NaN)\n", i+1); - } - delay(2000); - } -} - -void testMQ2() { - Serial.println("Testing MQ2 (10 samples):"); - for (int i=0;i<10;i++) { - int a = analogRead(MQ2_ANALOG_PIN); - bool d = digitalRead(MQ2_DIGITAL_PIN) == LOW; - Serial.printf(" #%d: analog=%d digital=%s\n", i+1, a, d?"ACTIVE":"INACTIVE"); - delay(500); - } - Serial.printf("Calibration baseline=%.1f threshold=%.1f\n", mq2_cal.baseline, mq2_cal.dangerThreshold); -} - -void testMQ9() { - Serial.println("Testing MQ9 (10 samples):"); - for (int i=0;i<10;i++) { - int a = analogRead(MQ9_ANALOG_PIN); - bool d = digitalRead(MQ9_DIGITAL_PIN) == LOW; - Serial.printf(" #%d: analog=%d digital=%s\n", i+1, a, d?"ACTIVE":"INACTIVE"); - delay(500); - } - Serial.printf("Calibration baseline=%.1f threshold=%.1f\n", mq9_cal.baseline, mq9_cal.dangerThreshold); -} - -void testMQ135() { - Serial.println("Testing MQ135 (10 samples):"); - for (int i=0;i<10;i++) { - int a = analogRead(MQ135_ANALOG_PIN); - bool d = digitalRead(MQ135_DIGITAL_PIN) == LOW; - Serial.printf(" #%d: analog=%d digital=%s rating=%s\n", i+1, a, d?"POOR":"GOOD", getAirQualityRating(a).c_str()); - delay(500); - } - Serial.printf("Calibration baseline=%.1f threshold=%.1f\n", mq135_cal.baseline, mq135_cal.dangerThreshold); -} - -void testAllMQ() { testMQ2(); testMQ9(); testMQ135(); } - -void testAudio(int fileNum) { - if (!audioReady) { Serial.println("Audio not ready"); return; } - Serial.printf("Playing audio file #%d\n", fileNum); - playAudioFile(fileNum); -} - -void testLoRa() { - if (!loraReady) { Serial.println("LoRa not ready"); return; } - Serial.println("Sending test LoRa packet..."); - SensorData d = readAllSensors(); - d.emergency = false; - d.motion = motionData; - sendLoRaData(d); - Serial.println("Test LoRa sent."); -} - -void testEmergency() { - Serial.println("Triggering emergency now..."); - emergencyTriggered = true; -} - -void testButton() { - Serial.println("Testing emergency button for 10s..."); - unsigned long start = millis(); - bool last = digitalRead(EMERGENCY_BUTTON_PIN); - while (millis() - start < 10000) { - bool cur = digitalRead(EMERGENCY_BUTTON_PIN); - if (cur != last) { - Serial.printf("Button state change: %s\n", cur ? "HIGH" : "LOW"); - last = cur; - } - delay(100); - } - Serial.println("Button test complete."); -} - -void testAllSensors() { - testDHT(); - testAllMQ(); - testLoRa(); -} - -// ---------------- System status and misc helpers ---------------- -void scanI2CDevices() { - Serial.println("\n=== I2C Device Scanner ==="); - Serial.println("Scanning I2C bus (0x00 to 0x7F)..."); - int devicesFound = 0; - for (byte address = 1; address < 127; address++) { - Wire.beginTransmission(address); - byte error = Wire.endTransmission(); - if (error == 0) { - Serial.printf("✓ Device found at 0x%02X\n", address); - devicesFound++; - if (address == 0x68 || address == 0x69) Serial.println(" → This is likely the MPU6050!"); - } - } - if (devicesFound == 0) { - Serial.println("\n❌ NO I2C devices found!"); - Serial.println("\nTroubleshooting:"); - Serial.println("1. Check VCC is connected to 3.3V (NOT 5V)"); - Serial.println("2. Check GND is connected"); - Serial.println("3. Verify SDA → GPIO21, SCL → GPIO22"); - Serial.println("4. Check all connections are firm"); - Serial.println("5. Add 4.7kΩ pull-up resistors to SDA & SCL"); - Serial.println("6. Try a different MPU6050 module (may be faulty)"); - } else { - Serial.printf("\n✓ Total devices found: %d\n", devicesFound); - } - Serial.println("=========================\n"); -} - -void sendCommand(byte cmd, byte param1, byte param2, bool feedback) { - byte packet[10]; - packet[0] = FRAME_START; - packet[1] = VERSION; - packet[2] = 0x06; - packet[3] = cmd; - packet[4] = feedback ? WITH_FEEDBACK : NO_FEEDBACK; - packet[5] = param1; - packet[6] = param2; - uint16_t sum = packet[1] + packet[2] + packet[3] + packet[4] + packet[5] + packet[6]; - uint16_t checksum = 0xFFFF - sum + 1; - packet[7] = (checksum >> 8) & 0xFF; - packet[8] = checksum & 0xFF; - packet[9] = FRAME_END; - fnM16pSerial.write(packet, 10); -} - -void setVolume(int volume) { - if (volume < 0) volume = 0; - if (volume > 30) volume = 30; - sendCommand(CMD_VOLUME, 0x00, volume, false); -} - -void playAudioFile(int fileNumber) { - if (fileNumber < 1 || fileNumber > 10) return; - sendCommand(CMD_PLAY_TRACK, 0x00, fileNumber, false); - delay(60); -} - -void stopAudio() { - sendCommand(CMD_STOP, 0x00, 0x00, false); -} - -void calibrateSensors() { - Serial.println("Starting sensor calibration..."); - delay(500); - calibrateSensor(MQ2_ANALOG_PIN, &mq2_cal, "MQ2", MQ2_DANGER_THRESHOLD); - calibrateSensor(MQ9_ANALOG_PIN, &mq9_cal, "MQ9", MQ9_DANGER_THRESHOLD); - calibrateSensor(MQ135_ANALOG_PIN, &mq135_cal, "MQ135", MQ135_DANGER_THRESHOLD); - Serial.println("Calibration completed."); -} - -void calibrateSensor(int pin, SensorCalibration* cal, String sensorName, int minThreshold) { - Serial.print("Calibrating " + sensorName + " ..."); - float sum = 0; - for (int i=0;ibaseline = sum / CALIBRATION_SAMPLES; - - // FIXED: For MQ9 specifically, if baseline is already high, use static threshold - if (sensorName == "MQ9" && cal->baseline > (minThreshold * 0.8)) { - Serial.printf("\n ⚠ MQ9 baseline (%.0f) is high - using static threshold\n", cal->baseline); - cal->dangerThreshold = minThreshold; - } else { - float calculatedThreshold = cal->baseline * DANGER_MULTIPLIER; - cal->dangerThreshold = (calculatedThreshold > minThreshold) ? calculatedThreshold : minThreshold; - } - - cal->calibrated = true; - Serial.println(" Done"); - Serial.printf(" Baseline: %.0f, Danger threshold: %.0f\n", cal->baseline, cal->dangerThreshold); -} - -bool checkSensorDanger(int currentValue, SensorCalibration* cal, int staticDangerThreshold) { - if (!cal->calibrated) return currentValue >= staticDangerThreshold; - return currentValue >= cal->dangerThreshold; -} - -SensorData readAllSensors() { - SensorData data; - data.timestamp = millis(); - if (millis() - lastDHTReading > DHT_READING_INTERVAL) { - float rt = dht.readTemperature(); - float rh = dht.readHumidity(); - if (!isnan(rt) && rt >= -40 && rt <= 80) { data.temperature = rt; lastValidTemperature = rt; } - else data.temperature = lastValidTemperature; - if (!isnan(rh) && rh >= 0 && rh <= 100) { - float corr = rh - 30.0f; - if (corr < 0.0f) corr = 0.0f; - if (corr > 100.0f) corr = 100.0f; - data.humidity = corr; - lastValidHumidity = corr; - } else data.humidity = lastValidHumidity; - lastDHTReading = millis(); - } else { - data.temperature = lastValidTemperature; - data.humidity = lastValidHumidity; - } - data.mq2_analog = analogRead(MQ2_ANALOG_PIN); - data.mq9_analog = analogRead(MQ9_ANALOG_PIN); - data.mq135_analog = analogRead(MQ135_ANALOG_PIN); - data.mq2_digital = digitalRead(MQ2_DIGITAL_PIN) == LOW; - data.mq9_digital = digitalRead(MQ9_DIGITAL_PIN) == LOW; - data.mq135_digital = digitalRead(MQ135_DIGITAL_PIN) == LOW; - data.emergency = digitalRead(EMERGENCY_BUTTON_PIN) == LOW; - data.motion = motionData; - return data; -} - -void displayReadings(SensorData data) { - Serial.println("\n--- SENSOR SNAPSHOT ---"); - Serial.printf("Timestamp: %lu\n", data.timestamp); - Serial.printf("Temp: %.1f C Hum: %.1f %%\n", data.temperature, data.humidity); - Serial.printf("MQ2: %d (%s) MQ9: %d (%s) MQ135: %d (%s)\n", - data.mq2_analog, data.mq2_digital ? "ALERT":"OK", - data.mq9_analog, data.mq9_digital ? "ALERT":"OK", - data.mq135_analog, data.mq135_digital ? "ALERT":"OK"); - Serial.printf("Motion: accel=%.2fm/s2 gyro=%.2f deg/s\n", data.motion.totalAccel, data.motion.totalGyro); - Serial.printf("Emergency Button: %s\n", data.emergency ? "PRESSED" : "RELEASED"); - if (wristbandStatus.connected) { - unsigned long age = (millis() - wristbandStatus.lastUpdate) / 1000; - Serial.printf("Wristband: CONNECTED (BPM=%u SpO2=%u finger=%s, %lus ago)\n", - wristbandStatus.bpm, wristbandStatus.spo2, wristbandStatus.fingerDetected?"YES":"NO", age); - } else { - Serial.println("Wristband: DISCONNECTED"); - } -} - -void checkAlerts(SensorData data) { - // Alert cooldown tracking (static variables persist between function calls) - static unsigned long lastTempHighAlert = 0; - static unsigned long lastTempLowAlert = 0; - static unsigned long lastHumidityHighAlert = 0; - static unsigned long lastHumidityLowAlert = 0; - static unsigned long lastMQ2Alert = 0; - static unsigned long lastMQ9Alert = 0; - static unsigned long lastMQ135Alert = 0; - - const unsigned long ALERT_COOLDOWN = 300000; // 5 minutes in milliseconds - unsigned long now = millis(); - - // Temperature Alerts - if (data.temperature >= 45.0) { - if (lastTempHighAlert == 0 || (now - lastTempHighAlert >= ALERT_COOLDOWN)) { - Serial.println("!!! HIGH TEMPERATURE ALERT"); - Serial.printf(" Temperature: %.1f°C (threshold: 45°C)\n", data.temperature); - if (audioReady) playAudioFile(HIGH_TEMP_ALERT); - lastTempHighAlert = now; - } - } - - if (data.temperature <= 5.0) { - if (lastTempLowAlert == 0 || (now - lastTempLowAlert >= ALERT_COOLDOWN)) { - Serial.println("!!! LOW TEMPERATURE ALERT"); - Serial.printf(" Temperature: %.1f°C (threshold: 5°C)\n", data.temperature); - if (audioReady) playAudioFile(LOW_TEMP_ALERT); - lastTempLowAlert = now; - } - } - - // Humidity Alerts - if (data.humidity >= 80.0) { - if (lastHumidityHighAlert == 0 || (now - lastHumidityHighAlert >= ALERT_COOLDOWN)) { - Serial.println("!!! HIGH HUMIDITY ALERT"); - Serial.printf(" Humidity: %.1f%% (threshold: 80%%)\n", data.humidity); - if (audioReady) playAudioFile(HIGH_HUMIDITY_ALERT); - lastHumidityHighAlert = now; - } - } - - if (data.humidity <= 20.0) { - if (lastHumidityLowAlert == 0 || (now - lastHumidityLowAlert >= ALERT_COOLDOWN)) { - Serial.println("!!! LOW HUMIDITY ALERT"); - Serial.printf(" Humidity: %.1f%% (threshold: 20%%)\n", data.humidity); - if (audioReady) playAudioFile(LOW_HUMIDITY_ALERT); - lastHumidityLowAlert = now; - } - } - - // MQ2 Alert - bool mq2_danger = data.mq2_analog >= MQ2_DANGER_THRESHOLD || - (mq2_cal.calibrated && data.mq2_analog >= mq2_cal.dangerThreshold); - - if (mq2_danger) { - if (lastMQ2Alert == 0 || (now - lastMQ2Alert >= ALERT_COOLDOWN)) { - Serial.println("!!! MQ2 Danger detected"); - Serial.printf(" MQ2 analog=%d (static threshold=%d, calibrated=%.0f)\n", - data.mq2_analog, MQ2_DANGER_THRESHOLD, mq2_cal.dangerThreshold); - if (audioReady) playAudioFile(SMOKE_ALERT); - lastMQ2Alert = now; - } - } - - // MQ9 Alert - bool mq9_danger = data.mq9_analog >= MQ9_DANGER_THRESHOLD || - (mq9_cal.calibrated && data.mq9_analog >= mq9_cal.dangerThreshold); - - if (mq9_danger) { - if (lastMQ9Alert == 0 || (now - lastMQ9Alert >= ALERT_COOLDOWN)) { - Serial.println("!!! MQ9 Danger detected"); - Serial.printf(" MQ9 analog=%d (static threshold=%d, calibrated=%.0f)\n", - data.mq9_analog, MQ9_DANGER_THRESHOLD, mq9_cal.dangerThreshold); - if (audioReady) { - playAudioFile(CO_ALERT); - Serial.println(" ✓ Playing CO alert audio (0003.mp3)"); - } - lastMQ9Alert = now; - } - } - - // MQ135 Alert - bool mq135_danger = data.mq135_analog >= MQ135_DANGER_THRESHOLD || - (mq135_cal.calibrated && data.mq135_analog >= mq135_cal.dangerThreshold); - - if (mq135_danger) { - if (lastMQ135Alert == 0 || (now - lastMQ135Alert >= ALERT_COOLDOWN)) { - Serial.println("!!! MQ135 Air quality degraded"); - Serial.printf(" MQ135 analog=%d (static threshold=%d, calibrated=%.0f, rating=%s)\n", - data.mq135_analog, MQ135_DANGER_THRESHOLD, mq135_cal.dangerThreshold, - getAirQualityRating(data.mq135_analog).c_str()); - if (audioReady) playAudioFile(AIR_QUALITY_WARNING); - lastMQ135Alert = now; - } - } -} -void sendLoRaData(SensorData data) { - StaticJsonDocument<600> doc; - doc["node"] = NODE_ID; - doc["timestamp"] = data.timestamp; - doc["temp"] = data.temperature; - doc["hum"] = data.humidity; - doc["mq2"] = data.mq2_analog; - doc["mq9"] = data.mq9_analog; - doc["mq135"] = data.mq135_analog; - - // CRITICAL: Explicitly set emergency field (not as 0/1 but as boolean) - doc["emergency"] = data.emergency; // This will be true or false - - doc["motion_accel"] = data.motion.totalAccel; - doc["motion_gyro"] = data.motion.totalGyro; - doc["bpm"] = wristbandStatus.connected ? wristbandStatus.bpm : 0; - doc["spo2"] = wristbandStatus.connected ? wristbandStatus.spo2 : 0; - doc["body_temp"] = wristbandStatus.connected ? wristbandStatus.bodyTemp : 0.0f; - doc["wristband_connected"] = wristbandStatus.connected ? 1 : 0; - - char payload[600]; - size_t n = serializeJson(doc, payload, sizeof(payload)); - - if (!loraReady) { - Serial.println("LoRa not ready - cannot send"); - return; - } - - LoRa.beginPacket(); - LoRa.print(payload); - LoRa.endPacket(); - - packetCount++; - - // Enhanced logging for emergency packets - if (data.emergency) { - Serial.println("\n╔═══════════════════════════════════════╗"); - Serial.println("║ EMERGENCY PACKET TRANSMITTED ║"); - Serial.println("╚═══════════════════════════════════════╝"); - } - - Serial.printf("LoRa packet #%d sent (%d bytes)%s\n", - packetCount, (int)n, data.emergency ? " [EMERGENCY]" : ""); - Serial.println("Payload: " + String(payload)); -} - -// ---------------- ESP-NOW Implementation ---------------- -void initESPNOW() { - // Ensure WiFi is properly initialized first - WiFi.mode(WIFI_STA); - WiFi.disconnect(false); - delay(100); - WiFi.begin(); - delay(100); - - int channel = 1; - esp_err_t cherr = esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE); - if (cherr == ESP_OK) { - Serial.printf("✓ WiFi channel set to %d for ESP-NOW\n", channel); - } else { - Serial.printf("⚠ Failed to set WiFi channel (%d) err=%d\n", channel, (int)cherr); - } - - if (esp_now_init() != ESP_OK) { - Serial.println("⚠ ESP-NOW init failed - retrying..."); - delay(500); - if (esp_now_init() != ESP_OK) { - Serial.println("✗ ESP-NOW init failed after retry"); - espnowReady = false; - return; - } - } - espnowReady = true; - Serial.println("✓ ESP-NOW initialized"); - - esp_now_register_send_cb(onDataSent); - esp_now_register_recv_cb(onDataRecv); - - delay(100); - - esp_now_peer_info_t peerInfo; - memset(&peerInfo, 0, sizeof(peerInfo)); - memcpy(peerInfo.peer_addr, wristbandMac, 6); - peerInfo.channel = channel; - #if defined(ESP_IF_WIFI_STA) - peerInfo.ifidx = ESP_IF_WIFI_STA; - #elif defined(WIFI_IF_STA) - peerInfo.ifidx = WIFI_IF_STA; - #else - peerInfo.ifidx = (wifi_interface_t)0; - #endif - peerInfo.encrypt = false; - - if (!esp_now_is_peer_exist(wristbandMac)) { - esp_err_t addStatus = esp_now_add_peer(&peerInfo); - if (addStatus != ESP_OK) { - Serial.printf("⚠ Failed to add ESP-NOW peer (wristband) - error: %d\n", addStatus); - } else { - Serial.println("✓ Wristband ESP-NOW peer added"); - Serial.printf(" Peer MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", - wristbandMac[0], wristbandMac[1], wristbandMac[2], - wristbandMac[3], wristbandMac[4], wristbandMac[5]); - } - } else { - Serial.println("✓ Wristband peer already exists"); - } - - delay(100); -} - - -void onDataRecv(const esp_now_recv_info_t *recv_info, const uint8_t *data, int len) { - if (data == NULL || len < 1) return; - - uint8_t msgType = data[0]; - - if (msgType == MSG_TYPE_VITALS) { - if (len < (int)sizeof(espnow_vitals_t)) { - Serial.println("⚠ Received VITALS unexpected length"); - return; - } - - espnow_vitals_t vitals; - memcpy(&vitals, data, sizeof(vitals)); - - wristbandStatus.bpm = vitals.bpm; - wristbandStatus.spo2 = vitals.spo2; - wristbandStatus.fingerDetected = (vitals.finger != 0); - wristbandStatus.bodyTemp = (float)vitals.temperature; - wristbandStatus.lastUpdate = millis(); - wristbandStatus.connected = true; - - Serial.printf("[ESP-NOW RX] VITALS -> BPM=%u SpO2=%u hand=%s bodyTemp=%.1f°C ts=%lu\n", - wristbandStatus.bpm, - wristbandStatus.spo2, - wristbandStatus.fingerDetected ? "YES" : "NO", - wristbandStatus.bodyTemp, - (unsigned long)vitals.timestamp); - - } else if (msgType == MSG_TYPE_ACK) { - if (len < (int)sizeof(espnow_ack_t)) { - Serial.println("⚠ Received ACK unexpected length"); - return; - } - - espnow_ack_t ack; - memcpy(&ack, data, sizeof(ack)); - - if (ack.messageId == wristbandStatus.lastMessageId) { - wristbandStatus.messageAcknowledged = (ack.success != 0); - - Serial.printf("[ESP-NOW RX] ACK for msgId=%lu success=%s\n", - (unsigned long)ack.messageId, - ack.success ? "YES" : "NO"); - - // NEW: Play audio when message is successfully acknowledged by wristband - if (ack.success && audioReady) { - delay(200); // Wait for ESP-NOW to finish - playAudioFile(MESSAGE_RECEIVED); - Serial.println("✓ Playing message received confirmation audio"); - delay(100); // Ensure audio command is sent -} - - } else { - Serial.printf("[ESP-NOW RX] ACK for unknown msgId=%lu\n", - (unsigned long)ack.messageId); - } - - } else { - Serial.printf("[ESP-NOW RX] Unknown msgType: 0x%02X\n", msgType); - } -} - -void onDataSent(const wifi_tx_info_t *tx_info, esp_now_send_status_t status) { - lastEspNowSendStatus = (status == ESP_NOW_SEND_SUCCESS) ? ESP_OK : ESP_FAIL; - char macStr[18]; - sprintf(macStr, "%02X:%02X:%02X:%02X:%02X:%02X", - wristbandMac[0], wristbandMac[1], wristbandMac[2], - wristbandMac[3], wristbandMac[4], wristbandMac[5]); - Serial.printf("[ESP-NOW TX] to %s status=%s\n", macStr, (status == ESP_NOW_SEND_SUCCESS) ? "OK":"FAIL"); -} - -bool sendTextToWristband(const String &message) { - if (!espnowReady) { Serial.println("ESP-NOW not ready - cannot send text"); return false; } - espnow_text_t pkt; memset(&pkt,0,sizeof(pkt)); - pkt.msgType = MSG_TYPE_TEXT; - pkt.messageId = outgoingMessageCounter++; - size_t len = message.length(); - if (len > MAX_TEXT_LEN - 1) len = MAX_TEXT_LEN - 1; - pkt.length = (uint8_t)len; - memcpy(pkt.text, message.c_str(), pkt.length); - pkt.text[pkt.length] = '\0'; - esp_err_t rc = esp_now_send(wristbandMac, (uint8_t *)&pkt, sizeof(pkt)); - if (rc == ESP_OK) { - wristbandStatus.lastMessageId = pkt.messageId; - wristbandStatus.messageAcknowledged = false; - Serial.printf("Sent TEXT msgId=%lu len=%u\n", (unsigned long)pkt.messageId, pkt.length); - return true; - } else { - Serial.printf("Failed to send TEXT (esp_now_send rc=%d)\n", rc); - return false; - } -} - -void checkWristbandConnection() { - unsigned long now = millis(); - if (wristbandStatus.connected && (now - wristbandStatus.lastUpdate > 35000UL)) { - wristbandStatus.connected = false; - Serial.println("Wristband connection timed out (35s) -> DISCONNECTED"); - - // ADD THESE LINES: - // Clear vitals data when disconnected - wristbandStatus.bpm = 0; - wristbandStatus.spo2 = 0; - wristbandStatus.bodyTemp = 0.0f; - wristbandStatus.fingerDetected = false; - } -} -void receiveLoRaMessages() { - int packetSize = LoRa.parsePacket(); - if (packetSize <= 0) return; - - String incoming = ""; - while (LoRa.available()) incoming += (char)LoRa.read(); - incoming.trim(); - if (incoming.length() == 0) return; - - // Get signal quality - int rssi = LoRa.packetRssi(); - float snr = LoRa.packetSnr(); - - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.printf("║ LORA MESSAGE RECEIVED (RSSI: %4d dBm) ║\n", rssi); - Serial.println("╚════════════════════════════════════════════╝"); - Serial.println("Packet size: " + String(incoming.length()) + " bytes"); - Serial.println("SNR: " + String(snr, 1) + " dB"); - Serial.println("Raw data: " + incoming); - - // Parse JSON - StaticJsonDocument<256> doc; - DeserializationError err = deserializeJson(doc, incoming); - - if (err) { - Serial.println("❌ ERROR: JSON parse failed - " + String(err.c_str())); - Serial.println("════════════════════════════════════════════\n"); - return; - } - - // Check if this is a message relay packet - if (doc.containsKey("message")) { - String message = doc["message"].as(); - String from = doc["from"] | "unknown"; - unsigned long timestamp = doc["timestamp"] | 0; - - Serial.println("\n┌────────────────────────────────────────────┐"); - Serial.println("│ 📨 MESSAGE RELAY REQUEST DETECTED │"); - Serial.println("├────────────────────────────────────────────┤"); - Serial.printf("│ From: %-37s│\n", from.c_str()); - Serial.printf("│ Timestamp: %-32lu│\n", timestamp); - Serial.printf("│ Message: %-34s│\n", message.substring(0, 34).c_str()); - if (message.length() > 34) { - Serial.printf("│ %-34s│\n", message.substring(34, 68).c_str()); - } - Serial.println("└────────────────────────────────────────────┘"); - - Serial.println("\n→ Forwarding to wristband via ESP-NOW..."); - - // Forward to wristband - bool success = sendTextToWristband(message); - - - if (success) { - messagesRelayedToWristband++; - Serial.println("✓ Message forwarded successfully!"); - Serial.println(" Total messages relayed: " + String(messagesRelayedToWristband)); - - // Play audio confirmation (if audio is ready) - if (audioReady) { - delay(100); // Small delay before audio - // Note: MESSAGE_RECEIVED audio (0009.mp3) will play automatically - // when wristband sends ACK in onDataRecv() - } - } else { - Serial.println("✗ Failed to forward message to wristband"); - Serial.println(" Check: ESP-NOW status, wristband connection"); - } - - Serial.println("════════════════════════════════════════════\n"); - } - else { - // Not a message packet - could be sensor data or other packet type - Serial.println("ℹ️ Packet received but no 'message' field found"); - Serial.println(" (This is normal for sensor data packets)"); - - // List available fields for debugging - Serial.print(" Available fields: "); - JsonObject obj = doc.as(); - for (JsonPair kv : obj) { - Serial.print(kv.key().c_str()); - Serial.print(" "); - } - Serial.println(); - Serial.println("════════════════════════════════════════════\n"); - } -} -// Enhanced system status with more details -void printSystemStatus() { - unsigned long uptime = millis() / 1000; - int hours = uptime / 3600; - int minutes = (uptime % 3600) / 60; - int seconds = uptime % 60; - - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.println("║ SYSTEM STATUS REPORT ║"); - Serial.println("╚════════════════════════════════════════════╝"); - - Serial.printf("Uptime: %02d:%02d:%02d\n", hours, minutes, seconds); - Serial.println(); - - Serial.println("Hardware Status:"); - Serial.printf(" LoRa: %s (Packets sent: %d)\n", - loraReady ? "✓ Active" : "✗ Offline", packetCount); - Serial.printf(" ESP-NOW: %s\n", - espnowReady ? "✓ Active" : "✗ Offline"); - Serial.printf(" MPU6050: %s\n", - mpuReady ? "✓ Active" : "✗ Offline"); - Serial.printf(" DHT11: %s\n", - dhtReady ? "✓ Active" : "⚠ Fallback"); - Serial.printf(" Audio: %s\n", - audioReady ? "✓ Active" : "✗ Offline"); - Serial.println(); - - // ========== ADD THIS BLOCK HERE ========== - Serial.println("Message Relay Status:"); - Serial.printf(" Messages relayed to wristband: %lu\n", messagesRelayedToWristband); - Serial.printf(" Last message ID sent: %lu\n", (unsigned long)(outgoingMessageCounter - 1)); - if (wristbandStatus.lastMessageId > 0) { - Serial.printf(" Last message acknowledged: %s\n", - wristbandStatus.messageAcknowledged ? "✓ Yes" : "✗ No"); - } - Serial.println(); - // ========== END OF NEW BLOCK ========== - - if (espnowReady) { - if (wristbandStatus.connected) { - unsigned long age = (millis() - wristbandStatus.lastUpdate) / 1000; - Serial.println("Wristband Status:"); - Serial.printf(" Connection: ✓ Active (%lu seconds ago)\n", age); - Serial.printf(" Heart Rate: %u BPM\n", wristbandStatus.bpm); - Serial.printf(" SpO2: %u%%\n", wristbandStatus.spo2); - Serial.printf(" Finger: %s\n", wristbandStatus.fingerDetected ? "Detected" : "None"); - } else { - Serial.println("Wristband Status:"); - Serial.println(" Connection: ✗ Disconnected"); - } - Serial.println(); - } - - Serial.println("Sensor Calibration:"); - Serial.printf(" MQ2: Baseline=%.0f Threshold=%.0f %s\n", - mq2_cal.baseline, mq2_cal.dangerThreshold, - mq2_cal.calibrated ? "✓" : "✗"); - Serial.printf(" MQ9: Baseline=%.0f Threshold=%.0f %s\n", - mq9_cal.baseline, mq9_cal.dangerThreshold, - mq9_cal.calibrated ? "✓" : "✗"); - Serial.printf(" MQ135: Baseline=%.0f Threshold=%.0f %s\n", - mq135_cal.baseline, mq135_cal.dangerThreshold, - mq135_cal.calibrated ? "✓" : "✗"); - - Serial.println("\n════════════════════════════════════════════\n"); -} diff --git a/WristWatch FIxed ALgo with Temp Push.ino b/WristWatch FIxed ALgo with Temp Push.ino deleted file mode 100644 index 1191d65..0000000 --- a/WristWatch FIxed ALgo with Temp Push.ino +++ /dev/null @@ -1,780 +0,0 @@ -// wristband_espnow.ino -// PRODUCTION READY - Wristband with MAX30102 + SSD1306 + ESP-NOW -// MANUFACTURER ALGORITHMS - Accurate BPM, SpO2, and Temperature -// FIXED: BPM Detection Issues - -#include -#include -#include -#include -#include -#include -#include -#include - -// OLED Display -#define SCREEN_WIDTH 128 -#define SCREEN_HEIGHT 64 -#define OLED_RESET -1 -Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); - -// I2C Pins -#define SDA_PIN 4 -#define SCL_PIN 5 - -MAX30105 particleSensor; - -// Heart rate calculation - IMPROVED PEAK DETECTION -const byte RATE_SIZE = 4; -byte rates[RATE_SIZE] = {0, 0, 0, 0}; -byte rateSpot = 0; -long lastBeat = 0; -float beatsPerMinute = 0; -int beatAvg = 0; - -// Improved beat detection -const int MIN_BEAT_INTERVAL = 300; // Minimum ms between beats (200 BPM max) -const int MAX_BEAT_INTERVAL = 2000; // Maximum ms between beats (30 BPM min) -long prevIR = 0; -long prev2IR = 0; -long prev3IR = 0; -int beatCount = 0; - -// Peak detection variables -const long IR_THRESHOLD = 50000; // Minimum IR value for finger detection -const long PEAK_THRESHOLD = 2000; // Minimum change to be considered a peak -long peakValue = 0; -unsigned long peakTime = 0; -bool lookingForPeak = true; -long valleyValue = 0; - -// SpO2 calculation - from manufacturer Example 3 -uint32_t irBuffer[100]; // infrared LED sensor data -uint32_t redBuffer[100]; // red LED sensor data -int32_t bufferLength = 100; -int32_t spo2 = 0; // SPO2 value -int8_t validSPO2 = 0; // indicator to show if the SPO2 calculation is valid -int32_t heartRate = 0; // heart rate value from SpO2 algorithm -int8_t validHeartRate = 0; // indicator to show if the heart rate calculation is valid - -byte bufferIndex = 0; -bool bufferFilled = false; -unsigned long lastSpO2Calc = 0; -const unsigned long SPO2_CALC_INTERVAL = 1000; // Calculate every 1 second - -// Temperature - from manufacturer Example 4 -float currentTemperature = 0.0; -unsigned long lastTempRead = 0; -const unsigned long TEMP_READ_INTERVAL = 5000; // Read every 5 seconds - -// Timing -unsigned long lastUpdate = 0; -const unsigned long UPDATE_INTERVAL = 100; - -// ESP-NOW message types -#define MSG_TYPE_VITALS 0x01 -#define MSG_TYPE_TEXT 0x02 -#define MSG_TYPE_ACK 0x03 -#define MAX_TEXT_LEN 128 - -typedef struct __attribute__((packed)) { - uint8_t msgType; - uint8_t bpm; - uint8_t spo2; - uint8_t finger; - int8_t temperature; // Temperature in Celsius (can be negative) - uint32_t timestamp; -} espnow_vitals_t; - -typedef struct __attribute__((packed)) { - uint8_t msgType; - uint32_t messageId; - uint8_t length; - char text[MAX_TEXT_LEN]; -} espnow_text_t; - -typedef struct __attribute__((packed)) { - uint8_t msgType; - uint32_t messageId; - uint8_t success; -} espnow_ack_t; - -// Peer MAC -uint8_t edgeNodeMac[6] = {0x28, 0x56, 0x2F, 0x49, 0x56, 0xAC}; - -// Display state -bool displayingMessage = false; -String currentMessage = ""; -unsigned long messageStartTime = 0; -uint32_t currentMessageId = 0; -int scrollOffset = 0; -unsigned long lastScrollUpdate = 0; -const unsigned long SCROLL_SPEED = 150; - -bool edgeNodeConnected = false; -unsigned long lastEdgeNodeContact = 0; -bool espnowReady = false; - -const unsigned long VITALS_INTERVAL = 25000UL; -unsigned long lastVitalsSent = 0; -const unsigned long MESSAGE_DISPLAY_MS = 15000UL; -const unsigned long EDGE_NODE_DISCONNECT_MS = 60000UL; - -// Forward declarations -void initESPNOW(); -void onDataRecv(const esp_now_recv_info_t *recv_info, const uint8_t *data, int len); -void onDataSent(const wifi_tx_info_t *tx_info, esp_now_send_status_t status); -void sendVitals(uint8_t bpm, uint8_t spo2, bool fingerDetected, float temp); -void sendAcknowledgment(uint32_t messageId, bool success); -void checkConnection(); -void displayMessageScreen(const String &msg, uint32_t messageId); -void displayVitalsScreen(uint8_t bpm, uint8_t spo2, bool finger, bool connected, float temp); -void scanBus(); -int calculateTextHeight(const String &text, int maxWidthChars, int lineHeight); - -void setup() { - Serial.begin(115200); - delay(200); - - Serial.println("\n\nMAX30102 + OLED + ESP-NOW (MANUFACTURER ALGORITHMS)"); - - // ================= I2C FIRST ================= - Wire.begin(SDA_PIN, SCL_PIN); - Wire.setClock(100000); - delay(200); - - Serial.println("\n=== I2C Device Scan (Pre-init) ==="); - scanBus(); - delay(200); - - // ================= OLED FIRST ================= - if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { - Serial.println("ERROR: OLED not found!"); - while (1); - } - - display.clearDisplay(); - display.setTextSize(2); - display.setTextColor(SSD1306_WHITE); - display.setCursor(0, 0); - display.println("Starting...."); - display.display(); - - delay(1500); - - // ================= MAX30102 SECOND - MANUFACTURER CONFIGURATION ================= - if (!particleSensor.begin(Wire, I2C_SPEED_STANDARD)) { - display.clearDisplay(); - display.setTextSize(1); - display.setCursor(0, 0); - display.println("MAX30102 ERROR"); - display.display(); - Serial.println("MAX30102 not found!"); - while (1); - } - - // Configuration from manufacturer Example 3 (SpO2) - byte ledBrightness = 60; // Options: 0=Off to 255=50mA - byte sampleAverage = 4; // Options: 1, 2, 4, 8, 16, 32 - byte ledMode = 2; // Options: 1 = Red only, 2 = Red + IR, 3 = Red + IR + Green - byte sampleRate = 100; // Options: 50, 100, 200, 400, 800, 1000, 1600, 3200 - int pulseWidth = 411; // Options: 69, 118, 215, 411 - int adcRange = 4096; // Options: 2048, 4096, 8192, 16384 - - particleSensor.setup(ledBrightness, sampleAverage, ledMode, sampleRate, pulseWidth, adcRange); - - // Additional configuration for temperature - particleSensor.enableDIETEMPRDY(); // Enable temperature ready interrupt - - // Now safe to increase I2C speed - Wire.setClock(400000); - - Serial.println("✓ MAX30102 configured with manufacturer settings"); - Serial.println("Place finger with steady pressure for 4 seconds..."); - - // ================= WIFI / ESP-NOW LAST ================= - WiFi.mode(WIFI_STA); - WiFi.disconnect(false); - delay(100); - - WiFi.begin(); - delay(100); - - esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE); - initESPNOW(); - delay(200); - - // ================= INITIAL BUFFER FILL ================= - display.clearDisplay(); - display.setTextSize(1); - display.setCursor(0, 0); - display.println("Initializing..."); - display.setCursor(0, 15); - display.println("Collecting samples"); - display.display(); - - // Collect initial 100 samples (4 seconds at 25sps) - Serial.println("Collecting initial 100 samples..."); - for (byte i = 0; i < bufferLength; i++) { - while (particleSensor.available() == false) - particleSensor.check(); - - redBuffer[i] = particleSensor.getRed(); - irBuffer[i] = particleSensor.getIR(); - particleSensor.nextSample(); - - if (i % 10 == 0) { - display.setCursor(0, 30); - display.fillRect(0, 30, 128, 20, SSD1306_BLACK); - display.printf("Progress: %d%%", (i * 100) / bufferLength); - display.display(); - } - } - - bufferFilled = true; - bufferIndex = 0; - - // Calculate initial SpO2 - maxim_heart_rate_and_oxygen_saturation(irBuffer, bufferLength, redBuffer, &spo2, &validSPO2, &heartRate, &validHeartRate); - lastSpO2Calc = millis(); - - // ================= FINAL UI ================= - display.clearDisplay(); - display.setTextSize(1); - display.setCursor(0, 0); - display.println("Heart Rate Monitor"); - display.setCursor(0, 15); - display.println("Ready!"); - display.display(); - - lastVitalsSent = millis() - (VITALS_INTERVAL - 5000); - Serial.println("\n✓ Wristband ready (MANUFACTURER ALGORITHMS)"); - Serial.println("System operational"); -} - -void loop() { - // Check sensor availability first - while (particleSensor.available() == false) - particleSensor.check(); - - long irValue = particleSensor.getIR(); - long redValue = particleSensor.getRed(); - - // === IMPROVED PEAK DETECTION === - // Only process if finger is detected - if (irValue > IR_THRESHOLD) { - - // Initialize valley on first reading - if (valleyValue == 0) { - valleyValue = irValue; - } - - // Looking for peak (going up) - if (lookingForPeak) { - if (irValue > peakValue) { - peakValue = irValue; - peakTime = millis(); - } - // If value drops significantly, we found a peak - else if (peakValue - irValue > PEAK_THRESHOLD) { - // We have a peak! Check if it's a valid beat - unsigned long currentTime = millis(); - unsigned long timeSinceLastBeat = currentTime - lastBeat; - - if (lastBeat == 0) { - // First beat - lastBeat = currentTime; - Serial.println(">>> First beat detected!"); - } - else if (timeSinceLastBeat >= MIN_BEAT_INTERVAL && timeSinceLastBeat <= MAX_BEAT_INTERVAL) { - // Valid beat! - beatsPerMinute = 60000.0 / timeSinceLastBeat; - - if (beatsPerMinute >= 40 && beatsPerMinute <= 200) { - rates[rateSpot++] = (byte)beatsPerMinute; - rateSpot %= RATE_SIZE; - - // Calculate average - beatAvg = 0; - int validCount = 0; - for (byte x = 0; x < RATE_SIZE; x++) { - if (rates[x] > 0) { - beatAvg += rates[x]; - validCount++; - } - } - if (validCount > 0) beatAvg /= validCount; - - beatCount++; - lastBeat = currentTime; - - Serial.println(); - Serial.print("♥♥♥ BEAT #"); - Serial.print(beatCount); - Serial.print("! BPM="); - Serial.print(beatsPerMinute, 1); - Serial.print(", Avg="); - Serial.print(beatAvg); - Serial.print(", Interval="); - Serial.print(timeSinceLastBeat); - Serial.print("ms, Peak="); - Serial.print(peakValue); - Serial.print(", Valley="); - Serial.print(valleyValue); - Serial.print(", Amplitude="); - Serial.print(peakValue - valleyValue); - Serial.println(" ♥♥♥"); - Serial.println(); - } - } - else if (timeSinceLastBeat > MAX_BEAT_INTERVAL) { - // Too long since last beat, reset - Serial.println(">>> Timeout - resetting beat detection"); - lastBeat = currentTime; - } - - // Switch to looking for valley - lookingForPeak = false; - valleyValue = irValue; - } - } - // Looking for valley (going down) - else { - if (irValue < valleyValue) { - valleyValue = irValue; - } - // If value rises significantly, we found a valley - else if (irValue - valleyValue > PEAK_THRESHOLD) { - // Switch to looking for peak - lookingForPeak = true; - peakValue = irValue; - } - } - } - else { - // No finger detected - reset - peakValue = 0; - valleyValue = 0; - lookingForPeak = true; - } - - // === SPO2 CONTINUOUS SAMPLING (Example 3) === - if (bufferFilled) { - // Shift buffer - dump first 25 samples, move last 75 to top - if (bufferIndex >= 25) { - for (byte i = 25; i < 100; i++) { - redBuffer[i - 25] = redBuffer[i]; - irBuffer[i - 25] = irBuffer[i]; - } - bufferIndex = 75; // Start filling from position 75 - } - - // Take new sample - redBuffer[bufferIndex] = redValue; - irBuffer[bufferIndex] = irValue; - particleSensor.nextSample(); - - bufferIndex++; - - // After collecting 25 new samples (reaching index 100), recalculate - if (bufferIndex >= 100) { - if (millis() - lastSpO2Calc >= SPO2_CALC_INTERVAL) { - maxim_heart_rate_and_oxygen_saturation(irBuffer, bufferLength, redBuffer, &spo2, &validSPO2, &heartRate, &validHeartRate); - lastSpO2Calc = millis(); - - Serial.print("SpO2 Algorithm: HR="); - Serial.print(heartRate); - Serial.print(", HRvalid="); - Serial.print(validHeartRate); - Serial.print(", SPO2="); - Serial.print(spo2); - Serial.print(", SPO2Valid="); - Serial.println(validSPO2); - } - bufferIndex = 0; // Reset for next cycle - } - } - - // === TEMPERATURE READING (Example 4) === - if (millis() - lastTempRead >= TEMP_READ_INTERVAL) { - currentTemperature = particleSensor.readTemperature(); - lastTempRead = millis(); - Serial.print("Temperature: "); - Serial.print(currentTemperature, 2); - Serial.println("°C"); - } - - // === DISPLAY UPDATE === - if (millis() - lastUpdate > UPDATE_INTERVAL) { - lastUpdate = millis(); - - if (displayingMessage) { - unsigned long elapsed = millis() - messageStartTime; - if (elapsed >= MESSAGE_DISPLAY_MS) { - displayingMessage = false; - currentMessage = ""; - scrollOffset = 0; - Serial.println("Message display timeout - returning to vitals"); - } else { - displayMessageScreen(currentMessage, currentMessageId); - } - } else { - bool fingerDetected = (irValue > IR_THRESHOLD); - - // Use simple beat detection BPM (more reliable for real-time display) - uint8_t displayBPM = beatAvg; - uint8_t displaySpO2 = 0; - - if (validSPO2 == 1 && spo2 > 0 && spo2 <= 100) { - displaySpO2 = (uint8_t)spo2; - } - - displayVitalsScreen(displayBPM, displaySpO2, fingerDetected, edgeNodeConnected, currentTemperature); - } - - // Debug output - Serial.print("IR="); - Serial.print(irValue); - Serial.print(", State="); - Serial.print(lookingForPeak ? "PEAK" : "VALLEY"); - Serial.print(", Peak="); - Serial.print(peakValue); - Serial.print(", Valley="); - Serial.print(valleyValue); - Serial.print(", BPM="); - Serial.print(beatsPerMinute, 1); - Serial.print(", Avg="); - Serial.print(beatAvg); - Serial.print(", SpO2="); - Serial.print(spo2); - Serial.print("%, Temp="); - Serial.print(currentTemperature, 1); - Serial.print("°C, Beats="); - Serial.println(beatCount); - } - - // === SEND VITALS VIA ESP-NOW === - if (millis() - lastVitalsSent >= VITALS_INTERVAL) { - long irVal = particleSensor.getIR(); - bool fingerDetected = (irVal > IR_THRESHOLD); - uint8_t bpmSend = 0; - uint8_t spo2Send = 0; - - if (fingerDetected) { - if (beatAvg > 0 && beatAvg < 255) { - bpmSend = (uint8_t)beatAvg; - } - - // Use validated SpO2 from algorithm - if (validSPO2 == 1 && spo2 > 0 && spo2 <= 100) { - spo2Send = (uint8_t)spo2; - } - } - - sendVitals(bpmSend, spo2Send, fingerDetected, currentTemperature); - lastVitalsSent = millis(); - } - - checkConnection(); - delay(20); -} - -void scanBus() { - int foundCount = 0; - for(byte i = 0; i < 128; i++) { - Wire.beginTransmission(i); - byte error = Wire.endTransmission(); - if (error == 0) { - Serial.print(" Found device at: 0x"); - if (i < 16) Serial.print("0"); - Serial.println(i, HEX); - foundCount++; - } - } - Serial.print(" Total: "); - Serial.print(foundCount); - Serial.println(" device(s)\n"); -} - -void initESPNOW() { - if (WiFi.status() == WL_NO_SHIELD) { - Serial.println("⚠ WiFi not ready"); - WiFi.mode(WIFI_STA); - delay(100); - } - - if (esp_now_init() != ESP_OK) { - Serial.println("⚠ ESP-NOW init failed - retrying..."); - delay(500); - if (esp_now_init() != ESP_OK) { - Serial.println("✗ ESP-NOW init failed"); - espnowReady = false; - return; - } - } - espnowReady = true; - Serial.println("✓ ESP-NOW initialized"); - - esp_now_register_send_cb(onDataSent); - esp_now_register_recv_cb(onDataRecv); - delay(100); - - esp_now_peer_info_t peerInfo; - memset(&peerInfo, 0, sizeof(peerInfo)); - memcpy(peerInfo.peer_addr, edgeNodeMac, 6); - peerInfo.channel = 1; - - #if defined(ESP_IF_WIFI_STA) - peerInfo.ifidx = ESP_IF_WIFI_STA; - #elif defined(WIFI_IF_STA) - peerInfo.ifidx = WIFI_IF_STA; - #else - peerInfo.ifidx = (wifi_interface_t)0; - #endif - - peerInfo.encrypt = false; - - if (!esp_now_is_peer_exist(edgeNodeMac)) { - esp_err_t addStatus = esp_now_add_peer(&peerInfo); - if (addStatus != ESP_OK) { - Serial.printf("⚠ Failed to add peer - error: %d\n", addStatus); - } else { - Serial.println("✓ Edge node peer added"); - } - } - delay(100); -} - -void onDataRecv(const esp_now_recv_info_t *recv_info, const uint8_t *data, int len) { - if (data == NULL || len < 1) return; - uint8_t msgType = data[0]; - - lastEdgeNodeContact = millis(); - edgeNodeConnected = true; - - if (msgType == MSG_TYPE_TEXT) { - if (len < 6) return; - uint32_t msgId = 0; - uint8_t lengthField = 0; - memcpy(&msgId, &data[1], sizeof(msgId)); - memcpy(&lengthField, &data[5], 1); - if (lengthField > MAX_TEXT_LEN - 1) lengthField = MAX_TEXT_LEN - 1; - char txtbuf[MAX_TEXT_LEN + 1]; - memset(txtbuf, 0, sizeof(txtbuf)); - int copyLen = min((int)lengthField, len - 6); - if (copyLen > 0) memcpy(txtbuf, &data[6], copyLen); - txtbuf[copyLen] = '\0'; - - String receivedText = String(txtbuf); - Serial.printf("[ESP-NOW RX] TEXT msgId=%lu text='%s'\n", (unsigned long)msgId, receivedText.c_str()); - - scrollOffset = 0; - lastScrollUpdate = millis(); - displayingMessage = true; - currentMessage = receivedText; - messageStartTime = millis(); - currentMessageId = msgId; - - displayMessageScreen(receivedText, msgId); - sendAcknowledgment(msgId, true); - } -} - -void onDataSent(const wifi_tx_info_t *tx_info, esp_now_send_status_t status) { - Serial.printf("[ESP-NOW TX] status=%s\n", (status == ESP_NOW_SEND_SUCCESS) ? "OK":"FAIL"); -} - -void sendVitals(uint8_t bpm, uint8_t spo2, bool fingerDetected, float temp) { - if (!espnowReady) return; - espnow_vitals_t pkt; - pkt.msgType = MSG_TYPE_VITALS; - pkt.bpm = bpm; - pkt.spo2 = spo2; - pkt.finger = fingerDetected ? 1 : 0; - pkt.temperature = (int8_t)temp; // Convert to integer Celsius - pkt.timestamp = (uint32_t)millis(); - - esp_err_t rc = esp_now_send(edgeNodeMac, (uint8_t *)&pkt, sizeof(pkt)); - if (rc == ESP_OK) { - Serial.printf("Sent VITALS bpm=%u spo2=%u finger=%s temp=%d°C\n", - bpm, spo2, fingerDetected?"YES":"NO", pkt.temperature); - } -} - -void sendAcknowledgment(uint32_t messageId, bool success) { - if (!espnowReady) return; - espnow_ack_t ack; - ack.msgType = MSG_TYPE_ACK; - ack.messageId = messageId; - ack.success = success ? 1 : 0; - esp_now_send(edgeNodeMac, (uint8_t *)&ack, sizeof(ack)); - Serial.printf("Sent ACK msgId=%lu\n", (unsigned long)messageId); -} - -void checkConnection() { - unsigned long now = millis(); - if (edgeNodeConnected && (now - lastEdgeNodeContact > EDGE_NODE_DISCONNECT_MS)) { - edgeNodeConnected = false; - Serial.println("Edge node timeout -> DISCONNECTED"); - display.clearDisplay(); - display.setTextSize(2); - display.setCursor(0, 10); - display.println("NO EDGE NODE"); - display.setTextSize(1); - display.setCursor(0, 40); - display.println("Reconnecting..."); - display.display(); - } -} - -int calculateTextHeight(const String &text, int maxWidthChars, int lineHeight) { - int start = 0; - int len = text.length(); - int lines = 0; - - while (start < len) { - int remaining = len - start; - int take = min(remaining, maxWidthChars); - - if (take == maxWidthChars) { - int lastSpace = -1; - for (int i = 0; i < take; i++) { - if (text.charAt(start + i) == ' ') lastSpace = i; - } - if (lastSpace > 0) take = lastSpace; - } - - lines++; - start += take; - while (start < len && text.charAt(start) == ' ') start++; - } - - return lines * lineHeight; -} - -void displayMessageScreen(const String &msg, uint32_t messageId) { - const int maxWidthChars = 21; - const int lineHeight = 10; - const int textStartY = 12; - const int displayHeight = 52; - - int totalTextHeight = calculateTextHeight(msg, maxWidthChars, lineHeight); - - if (totalTextHeight > displayHeight) { - if (millis() - lastScrollUpdate > SCROLL_SPEED) { - scrollOffset++; - int maxScroll = totalTextHeight - displayHeight + lineHeight; - if (scrollOffset > maxScroll) { - scrollOffset = -20; - } - lastScrollUpdate = millis(); - } - } else { - scrollOffset = 0; - } - - display.clearDisplay(); - display.setTextSize(1); - display.setTextColor(SSD1306_WHITE); - - display.setCursor(0, 0); - display.println("MSG:"); - display.drawLine(0, 10, 128, 10, SSD1306_WHITE); - - unsigned long elapsed = millis() - messageStartTime; - unsigned long remaining = 0; - if (elapsed < MESSAGE_DISPLAY_MS) { - remaining = (MESSAGE_DISPLAY_MS - elapsed) / 1000; - } - display.setCursor(90, 0); - display.printf("%2lus", remaining); - - int start = 0; - int len = msg.length(); - int line = 0; - int currentY = textStartY - scrollOffset; - - while (start < len) { - int remaining = len - start; - int take = min(remaining, maxWidthChars); - - if (take == maxWidthChars) { - int lastSpace = -1; - for (int i = 0; i < take; i++) { - if (msg.charAt(start + i) == ' ') lastSpace = i; - } - if (lastSpace > 0) take = lastSpace; - } - - String part = msg.substring(start, start + take); - - if (currentY >= textStartY - lineHeight && currentY < SCREEN_HEIGHT) { - display.setCursor(0, currentY); - display.println(part); - } - - line++; - currentY += lineHeight; - start += take; - while (start < len && msg.charAt(start) == ' ') start++; - } - - display.display(); -} - -void displayVitalsScreen(uint8_t bpm, uint8_t spo2, bool finger, bool connected, float temp) { - display.clearDisplay(); - display.setTextSize(1); - display.setTextColor(SSD1306_WHITE); - - // Connection status - display.setCursor(15, 0); - if (connected) display.print("[CONNECTED]"); - else display.print("[DISCONNECTED]"); - - display.drawLine(0, 10, 128, 10, SSD1306_WHITE); - - if (!finger) { - display.setTextSize(2); - display.setCursor(15, 20); - display.println("SIREN"); - display.setTextSize(1); - display.setCursor(5, 45); - display.println("Wear Device"); - } else { - // BPM - Large display - display.setTextSize(1); - display.setCursor(0, 14); - if (bpm > 0) display.print(bpm); else display.print("--"); - display.setTextSize(1); - display.setCursor(35, 18); - display.println("BPM"); - - // SpO2 - display.setTextSize(1); - display.setCursor(70, 14); - display.print("SpO2:"); - display.setCursor(70, 24); - if (spo2 > 0) { - display.print(spo2); - display.print("%"); - } else { - display.print("--"); - } - - // Temperature - Bottom row - display.setTextSize(1); - display.setCursor(0, 40); - display.print("Temp: "); - if (temp > 0) { - display.print(temp, 1); - display.print(" C"); - } else { - display.print("--.-"); - } - - // Status indicator REmoved - } - - display.display(); -} diff --git a/attempt for logic fix.cpp b/attempt for logic fix.cpp deleted file mode 100644 index f9dda7b..0000000 --- a/attempt for logic fix.cpp +++ /dev/null @@ -1,1040 +0,0 @@ -/* - ESP32 Multi-Sensor System with Emergency Button + MPU6050 (Clone WHO_AM_I fix) - - Preserves your pin layout and calibration logic (DANGER_MULTIPLIER = 2.0) - - Accepts MPU6050 WHO_AM_I values 0x68, 0x69, and 0x72 (common clones) - - If WHO_AM_I == 0x00 it will attempt bus recovery and a safe fallback: - * try re-read WHO_AM_I after recovery - * if still 0x00, attempt to write power-up and read accelerometer registers - — if accel data looks plausible proceed (sensor likely responding despite ID) - * otherwise disable MPU features and provide clear diagnostics - - Robust I2C read/write with retries and bus recovery to avoid "I2C software timeout" spam - - Keeps original JSON payload format for LoRa - - Keeps original calibration logic & DHT -30% manual humidity correction as requested - - Pin layout preserved exactly as you provided -*/ - -#include -#include -#include -#include -#include -#include -#include - -// ========================= MPU6050 I2C Register Definitions ========================= -#define MPU6050_ADDR 0x68 -#define PWR_MGMT_1 0x6B -#define ACCEL_XOUT_H 0x3B -#define GYRO_XOUT_H 0x43 -#define CONFIG 0x1A -#define GYRO_CONFIG 0x1B -#define ACCEL_CONFIG 0x1C -#define WHO_AM_I 0x75 - -// ========================= Pin definitions (preserved) ========================= -// MQ sensors -#define MQ2_DIGITAL_PIN 27 // ✔️ Safe -#define MQ2_ANALOG_PIN 32 // ✔️ ADC1_CH4 -#define MQ9_DIGITAL_PIN 14 // ✔️ Safe -#define MQ9_ANALOG_PIN 33 // ✔️ ADC1_CH5 -#define MQ135_DIGITAL_PIN 13 // ✔️ Safe -#define MQ135_ANALOG_PIN 35 // ✔️ ADC1_CH7 - -// FN-M16P Audio Module pins (UART2) -#define FN_M16P_RX 16 // ✔️ UART2 RX -#define FN_M16P_TX 17 // ✔️ UART2 TX - -// DHT11 pin -#define DHT11_PIN 25 // ✔️ GPIO25 (manual humidity correction applied) -#define DHT_TYPE DHT11 - -// MPU6050 pins (I2C) -#define MPU6050_SDA 21 // ✔️ I2C SDA -#define MPU6050_SCL 22 // ✔️ I2C SCL -#define MPU6050_INT 34 // ✔️ Input-only - -// LoRa Module pins (SPI) -#define LORA_SCK 18 // ✔️ SPI SCK -#define LORA_MISO 19 // ✔️ SPI MISO -#define LORA_MOSI 23 // ✔️ SPI MOSI -#define LORA_SS 5 // ⚠️ Needs pull-up resistor on some boards -#define LORA_RST 4 // ✔️ Safe -#define LORA_DIO0 26 // ✔️ Safe - -// EMERGENCY BUTTON PIN -#define EMERGENCY_BUTTON_PIN 15 // ✔️ GPIO15; use INPUT_PULLUP (pressed = LOW) - -// LoRa frequency -#define LORA_BAND 915E6 - -// ========================= System constants ========================= -#define NODE_ID "001" - -#define MQ2_DANGER_THRESHOLD 1600 -#define MQ9_DANGER_THRESHOLD 3800 -#define MQ135_DANGER_THRESHOLD 1800 - -#define FALLBACK_TEMPERATURE 27.0 -#define FALLBACK_HUMIDITY 47.0 - -#define CALIBRATION_SAMPLES 10 -#define DANGER_MULTIPLIER 2.0 -#define CALIBRATION_DELAY 2000 - -const int TAP_TIMEOUT = 600; -const int REQUIRED_TAPS = 3; - -// FN-M16P commands -const byte FRAME_START = 0x7E; -const byte FRAME_END = 0xEF; -const byte VERSION = 0xFF; -const byte NO_FEEDBACK = 0x00; -const byte WITH_FEEDBACK = 0x01; -const byte CMD_PLAY_TRACK = 0x03; -const byte CMD_VOLUME = 0x06; -const byte CMD_STOP = 0x16; - -// Audio files mapping -enum AudioFiles { - BOOT_AUDIO = 1, - SMOKE_ALERT = 2, - CO_ALERT = 3, - AIR_QUALITY_WARNING = 4, - HIGH_TEMP_ALERT = 5, - LOW_TEMP_ALERT = 6, - HIGH_HUMIDITY_ALERT = 7, - LOW_HUMIDITY_ALERT = 8, - FALL_DETECTED = 9, - MOTION_ALERT = 10 -}; - -// ========================= Types & Globals ========================= -struct SensorCalibration { - float baseline; - float dangerThreshold; - bool calibrated; -}; - -struct MotionData { - float totalAccel; // m/s^2 - float totalGyro; // deg/s - bool fallDetected; - bool impactDetected; - bool motionDetected; - unsigned long lastMotionTime; -}; - -struct SensorData { - float temperature; - float humidity; - int mq2_analog; - int mq9_analog; - int mq135_analog; - bool mq2_digital; - bool mq9_digital; - bool mq135_digital; - bool emergency; - unsigned long timestamp; - MotionData motion; -}; - -SensorCalibration mq2_cal = {0,0,false}; -SensorCalibration mq9_cal = {0,0,false}; -SensorCalibration mq135_cal = {0,0,false}; - -DHT dht(DHT11_PIN, DHT_TYPE); -HardwareSerial fnM16pSerial(2); - -bool audioReady = false; -bool loraReady = false; -bool dhtReady = false; -bool mpuReady = false; - -unsigned long lastLoRaSend = 0; -int loraInterval = 30000; -int packetCount = 0; - -unsigned long lastDHTReading = 0; -const unsigned long DHT_READING_INTERVAL = 2000; -float lastValidTemperature = FALLBACK_TEMPERATURE; -float lastValidHumidity = FALLBACK_HUMIDITY; - -// Emergency Button Variables -volatile int tapCount = 0; -volatile unsigned long lastTapTime = 0; -volatile bool emergencyTriggered = false; - -// Motion variables -MotionData motionData = {0}; - -// MPU detection / fall detector internals -const float G = 9.80665f; -const float FREE_FALL_G_THRESHOLD = 0.6f; // g -const unsigned long FREE_FALL_MIN_MS = 120; // ms -const float IMPACT_G_THRESHOLD = 3.5f; // g -const unsigned long IMPACT_WINDOW_MS = 1200; // ms -const unsigned long STATIONARY_CONFIRM_MS = 800; // ms -const float ROTATION_IMPACT_THRESHOLD = 400.0f; // deg/s - -bool inFreeFall = false; -bool fallInProgress = false; -bool impactSeen = false; -unsigned long freeFallStart = 0; -unsigned long fallStartTime = 0; -unsigned long impactTime = 0; -unsigned long stationarySince = 0; -float accelFiltered = G; -const float ALPHA = 0.85f; - -unsigned long lastI2CAttempt = 0; - -// ========================= Forward declarations ========================= -void printTestMenu(); -void handleTestCommand(String cmd); -void testDHT(); -void testMQ2(); -void testMQ9(); -void testMQ135(); -void testAllMQ(); -void testAudio(int fileNum); -void testLoRa(); -void testEmergency(); -void testButton(); -void testAllSensors(); -void setVolume(int volume); -void playAudioFile(int fileNumber); -void stopAudio(); -void calibrateSensors(); -void calibrateSensor(int pin, SensorCalibration* cal, String sensorName, int minThreshold); -bool checkSensorDanger(int currentValue, SensorCalibration* cal, int staticDangerThreshold); -SensorData readAllSensors(); -void displayReadings(SensorData data); -void checkAlerts(SensorData data); -void sendLoRaData(SensorData data); -String getAirQualityRating(int value); -void checkEmergencyButton(); -void handleEmergency(); -void sendCommand(byte cmd, byte param1, byte param2, bool feedback); - -// I2C helpers & MPU -bool i2cBusRecover(); -bool safeWireRequest(uint8_t addr, uint8_t reg, uint8_t *buf, size_t len, int retries=3); -bool safeWireWrite(uint8_t addr, uint8_t reg, uint8_t val, int retries=3); -bool initMPU6050(); -void readMPU6050Data(int16_t *ax, int16_t *ay, int16_t *az, int16_t *gx, int16_t *gy, int16_t *gz); -void monitorMotion(); -void detectFallAndHandle(); - -// ========================= I2C helpers & bus recovery ========================= -// Attempt to unstuck a bus by toggling SCL. Returns true if SDA released. -bool i2cBusRecover() { - Wire.end(); - pinMode(MPU6050_SCL, OUTPUT); - pinMode(MPU6050_SDA, INPUT_PULLUP); - // If SDA already high, bus free - if (digitalRead(MPU6050_SDA) == HIGH) { - Wire.begin(MPU6050_SDA, MPU6050_SCL); - Wire.setClock(400000); - return true; - } - // Pulse clock up to 9 times - for (int i=0;i<9;i++) { - digitalWrite(MPU6050_SCL, HIGH); - delayMicroseconds(300); - digitalWrite(MPU6050_SCL, LOW); - delayMicroseconds(300); - if (digitalRead(MPU6050_SDA) == HIGH) break; - } - // Generate STOP - pinMode(MPU6050_SDA, OUTPUT); - digitalWrite(MPU6050_SDA, LOW); - delayMicroseconds(100); - digitalWrite(MPU6050_SCL, HIGH); - delayMicroseconds(100); - digitalWrite(MPU6050_SDA, HIGH); - delayMicroseconds(100); - Wire.begin(MPU6050_SDA, MPU6050_SCL); - Wire.setClock(400000); - pinMode(MPU6050_SDA, INPUT_PULLUP); - return digitalRead(MPU6050_SDA) == HIGH; -} - -// robust read with retries + recovery -bool safeWireRequest(uint8_t addr, uint8_t reg, uint8_t *buf, size_t len, int retries) { - for (int attempt=0; attempt 4096 LSB/g and gyro ±500 dps -> 65.5 LSB/°/s - float ax = (axr / 4096.0f) * G; - float ay = (ayr / 4096.0f) * G; - float az = (azr / 4096.0f) * G; - float gx = (gxr / 65.5f); - float gy = (gyr / 65.5f); - float gz = (gzr / 65.5f); - - motionData.totalAccel = sqrt(ax*ax + ay*ay + az*az); - motionData.totalGyro = sqrt(gx*gx + gy*gy + gz*gz); - - // smoothing - accelFiltered = ALPHA * accelFiltered + (1.0f - ALPHA) * motionData.totalAccel; - - if (fabs(motionData.totalAccel - accelFiltered) > 0.2f * G || motionData.totalGyro > 25.0f) { - motionData.lastMotionTime = millis(); - motionData.motionDetected = true; - } else { - motionData.motionDetected = false; - } - - detectFallAndHandle(); -} - -void detectFallAndHandle() { - unsigned long now = millis(); - float totG = motionData.totalAccel / G; - float totGyro = motionData.totalGyro; - - // free-fall detection - if (totG < FREE_FALL_G_THRESHOLD) { - if (!inFreeFall) { inFreeFall = true; freeFallStart = now; } - else if ((now - freeFallStart) >= FREE_FALL_MIN_MS && !fallInProgress) { - fallInProgress = true; - fallStartTime = now; - impactSeen = false; - motionData.impactDetected = false; - } - } else { - if (inFreeFall) inFreeFall = false; - } - - // impact detection - if (fallInProgress && !impactSeen) { - if (totG >= IMPACT_G_THRESHOLD || totGyro >= ROTATION_IMPACT_THRESHOLD) { - impactSeen = true; - impactTime = now; - motionData.impactDetected = true; - } else if (now - fallStartTime > IMPACT_WINDOW_MS) { - // timed out without impact - fallInProgress = false; - impactSeen = false; - motionData.impactDetected = false; - } - } - - // confirm fall if impactSeen and post-impact stationary - if (impactSeen) { - float accelVariationG = fabs((motionData.totalAccel / G) - 1.0f); - if (accelVariationG < 0.35f && motionData.totalGyro < 50.0f) { - if (stationarySince == 0) stationarySince = now; - if (now - stationarySince >= STATIONARY_CONFIRM_MS) { - motionData.fallDetected = true; - emergencyTriggered = true; - if (audioReady) playAudioFile(FALL_DETECTED); - Serial.println("\n╔════════════════════════════════════╗"); - Serial.println("║ FALL CONFIRMED - IMPACT + STATIONARY ║"); - Serial.println("╚════════════════════════════════════╝"); - Serial.printf("Acceleration: %.2f g\n", motionData.totalAccel / G); - Serial.printf("Gyroscope: %.2f °/s\n", motionData.totalGyro); - // reset - fallInProgress = false; - impactSeen = false; - stationarySince = 0; - } - } else { - stationarySince = 0; - if (now - impactTime > IMPACT_WINDOW_MS) { - fallInProgress = false; impactSeen = false; stationarySince = 0; motionData.impactDetected = false; - } - } - } - - // immediate large spike detection (conservative) - static float lastTotalAccel = G; - static float lastTotalGyro = 0.0f; - float accelDeltaG = fabs((motionData.totalAccel - lastTotalAccel) / G); - float gyroDelta = fabs(motionData.totalGyro - lastTotalGyro); - - if (!motionData.fallDetected) { - if (accelDeltaG > 2.5f && (motionData.totalAccel / G) > 2.0f) { - // short confirmation window (non-blocking ideally, but keep short) - unsigned long t0 = millis(); - bool remainedStationary = true; - while (millis() - t0 < STATIONARY_CONFIRM_MS) { - // rely on monitorMotion() being called frequently in loop(); if motion remains low then confirm - if (motionData.motionDetected) { remainedStationary = false; break; } - delay(40); - } - if (remainedStationary) { - motionData.fallDetected = true; - emergencyTriggered = true; - if (audioReady) playAudioFile(FALL_DETECTED); - Serial.println("\n╔════════════════════════════════════╗"); - Serial.println("║ FALL CONFIRMED - SUDDEN IMPACT ║"); - Serial.println("╚════════════════════════════════════╝"); - } - } else if (gyroDelta > 300.0f && motionData.totalGyro > 400.0f) { - unsigned long t0 = millis(); - bool remainedStationary = true; - while (millis() - t0 < STATIONARY_CONFIRM_MS) { - if (motionData.motionDetected) { remainedStationary = false; break; } - delay(40); - } - if (remainedStationary) { - motionData.fallDetected = true; - emergencyTriggered = true; - if (audioReady) playAudioFile(FALL_DETECTED); - Serial.println("\n╔════════════════════════════════════╗"); - Serial.println("║ FALL CONFIRMED - ROTATION SPIKE ║"); - Serial.println("╚════════════════════════════════════╝"); - } - } - } - - lastTotalAccel = motionData.totalAccel; - lastTotalGyro = motionData.totalGyro; -} - -// ========================= Setup & Loop ========================= -void setup() { - Serial.begin(115200); - delay(800); - Serial.println("\n\n================================="); - Serial.println("ESP32 Multi-Sensor System - MPU6050 Clone WHO_AM_I Fix"); - Serial.println("================================="); - - // Initialize I2C (preserved pins) - Wire.begin(MPU6050_SDA, MPU6050_SCL); - Wire.setClock(400000); - - // MQ digital pins - pinMode(MQ2_DIGITAL_PIN, INPUT); - pinMode(MQ9_DIGITAL_PIN, INPUT); - pinMode(MQ135_DIGITAL_PIN, INPUT); - - // Emergency button -> use internal pull-up (pressed == LOW) - pinMode(EMERGENCY_BUTTON_PIN, INPUT_PULLUP); - Serial.printf("Emergency button: GPIO%d (INPUT_PULLUP). Press => LOW\n", EMERGENCY_BUTTON_PIN); - - // MPU INT pin - pinMode(MPU6050_INT, INPUT); - - Serial.println("Initializing MPU6050..."); - if (!initMPU6050()) { - Serial.println("⚠ MPU6050 init failed. Motion features will be disabled until I2C/wiring fixed."); - mpuReady = false; - } else { - Serial.println("✓ MPU6050 initialized (clone WHO_AM_I accepted if applicable)."); - mpuReady = true; - motionData.lastMotionTime = millis(); - accelFiltered = G; - } - - // DHT11 initialization (preserved pin 25) and manual humidity correction -30% - Serial.println("\nInitializing DHT11 sensor..."); - dht.begin(); - delay(1500); - bool dhtWorking = false; - for (int i=0;i<3;i++) { - float t = dht.readTemperature(); - float h = dht.readHumidity(); - if (!isnan(t) && !isnan(h)) { - dhtWorking = true; - dhtReady = true; - lastValidTemperature = t; - float corr = h - 30.0f; - if (corr < 0.0f) corr = 0.0f; - if (corr > 100.0f) corr = 100.0f; - lastValidHumidity = corr; - Serial.printf("✓ DHT11: Temp %.1fC Humidity (corrected) %.1f%%\n", lastValidTemperature, lastValidHumidity); - break; - } - delay(1000); - } - if (!dhtWorking) { - Serial.println("⚠ DHT11 ERROR - Check DATA wiring to GPIO25 and power."); - dhtReady = false; - } - - // Audio init - fnM16pSerial.begin(9600, SERIAL_8N1, FN_M16P_RX, FN_M16P_TX); - delay(200); - setVolume(30); - audioReady = true; - Serial.println("✓ Audio module initialized."); - - // LoRa init with SS high & RST pulse - Serial.println("Initializing LoRa..."); - pinMode(LORA_SS, OUTPUT); - digitalWrite(LORA_SS, HIGH); - pinMode(LORA_RST, OUTPUT); - digitalWrite(LORA_RST, HIGH); - SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_SS); - LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0); - digitalWrite(LORA_RST, LOW); delay(10); - digitalWrite(LORA_RST, HIGH); delay(10); - if (!LoRa.begin(LORA_BAND)) { - Serial.println("⚠ LoRa initialization failed. Check wiring/antenna."); - loraReady = false; - } else { - loraReady = true; - Serial.println("✓ LoRa initialized."); - } - - Serial.println("Warming gas sensors (15s)..."); - delay(15000); - - Serial.println("Calibrating MQ sensors..."); - calibrateSensors(); - - Serial.println("\nSYSTEM READY."); - printTestMenu(); - - if (audioReady) { playAudioFile(BOOT_AUDIO); delay(1000); } -} - -void loop() { - // serial test commands - if (Serial.available() > 0) { - String cmd = Serial.readStringUntil('\n'); - cmd.trim(); - cmd.toLowerCase(); - handleTestCommand(cmd); - } - - // emergency button - checkEmergencyButton(); - - // monitor motion - if (mpuReady) monitorMotion(); - - // emergency handling - if (emergencyTriggered) { - handleEmergency(); - emergencyTriggered = false; - } - - // periodic snapshot and LoRa send - static unsigned long lastNormal = 0; - if (millis() - lastNormal >= 10000) { - lastNormal = millis(); - SensorData data = readAllSensors(); - data.emergency = false; - data.motion = motionData; - displayReadings(data); - checkAlerts(data); - if (loraReady && (millis() - lastLoRaSend > loraInterval)) { - sendLoRaData(data); - lastLoRaSend = millis(); - } - Serial.println("------------------------"); - } - - delay(50); -} - -// ========================= Test commands & helpers ========================= - -void printTestMenu() { - Serial.println("\n╔═══════════════════════════════════════╗"); - Serial.println("║ TEST COMMANDS MENU ║"); - Serial.println("╠═══════════════════════════════════════╣"); - Serial.println("║ dht, mq2, mq9, mq135, mq, all ║"); - Serial.println("║ audio1..audio10, stop, volume+, volume-║"); - Serial.println("║ lora, button, emergency, calibrate ║"); - Serial.println("║ status, help/menu ║"); - Serial.println("╚═══════════════════════════════════════╝\n"); -} - -void handleTestCommand(String cmd) { - Serial.println("\n>>> EXECUTING TEST: " + cmd + " <<<\n"); - if (cmd == "help" || cmd == "menu") printTestMenu(); - else if (cmd == "dht") testDHT(); - else if (cmd == "mq2") testMQ2(); - else if (cmd == "mq9") testMQ9(); - else if (cmd == "mq135") testMQ135(); - else if (cmd == "mq") testAllMQ(); - else if (cmd.startsWith("audio")) { - int n = cmd.substring(5).toInt(); - if (n >= 1 && n <= 10) testAudio(n); - } - else if (cmd == "stop") { stopAudio(); Serial.println("Audio stopped."); } - else if (cmd == "volume+") { setVolume(25); Serial.println("Volume 25"); } - else if (cmd == "volume-") { setVolume(15); Serial.println("Volume 15"); } - else if (cmd == "lora") testLoRa(); - else if (cmd == "button") testButton(); - else if (cmd == "emergency") testEmergency(); - else if (cmd == "all") testAllSensors(); - else if (cmd == "calibrate") calibrateSensors(); - else if (cmd == "status") printSystemStatus(); - else Serial.println("❌ Unknown command: " + cmd); - Serial.println("\n>>> TEST COMPLETE <<<\n"); -} - -void testDHT() { - Serial.println("Testing DHT11 (5 samples) with manual -30% humidity correction:"); - for (int i=0;i<5;i++) { - float t = dht.readTemperature(); - float h = dht.readHumidity(); - if (!isnan(t) && !isnan(h)) { - float corr = h - 30.0f; - if (corr < 0) corr = 0; if (corr > 100) corr = 100; - Serial.printf(" #%d: Temp=%.2f C, Hum(corrected)=%.2f %%\n", i+1, t, corr); - } else { - Serial.printf(" #%d: FAILED (NaN)\n", i+1); - } - delay(2000); - } -} - -void testMQ2() { - Serial.println("Testing MQ2 (10 samples):"); - for (int i=0;i<10;i++) { - int a = analogRead(MQ2_ANALOG_PIN); - bool d = digitalRead(MQ2_DIGITAL_PIN) == LOW; - Serial.printf(" #%d: analog=%d digital=%s\n", i+1, a, d?"ACTIVE":"INACTIVE"); - delay(500); - } - Serial.printf("Calibration baseline=%.1f threshold=%.1f\n", mq2_cal.baseline, mq2_cal.dangerThreshold); -} - -void testMQ9() { - Serial.println("Testing MQ9 (10 samples):"); - for (int i=0;i<10;i++) { - int a = analogRead(MQ9_ANALOG_PIN); - bool d = digitalRead(MQ9_DIGITAL_PIN) == LOW; - Serial.printf(" #%d: analog=%d digital=%s\n", i+1, a, d?"ACTIVE":"INACTIVE"); - delay(500); - } - Serial.printf("Calibration baseline=%.1f threshold=%.1f\n", mq9_cal.baseline, mq9_cal.dangerThreshold); -} - -void testMQ135() { - Serial.println("Testing MQ135 (10 samples):"); - for (int i=0;i<10;i++) { - int a = analogRead(MQ135_ANALOG_PIN); - bool d = digitalRead(MQ135_DIGITAL_PIN) == LOW; - Serial.printf(" #%d: analog=%d digital=%s rating=%s\n", i+1, a, d?"POOR":"GOOD", getAirQualityRating(a).c_str()); - delay(500); - } - Serial.printf("Calibration baseline=%.1f threshold=%.1f\n", mq135_cal.baseline, mq135_cal.dangerThreshold); -} - -void testAllMQ() { testMQ2(); testMQ9(); testMQ135(); } - -void testAudio(int fileNum) { - if (!audioReady) { Serial.println("Audio not ready"); return; } - Serial.printf("Playing audio file #%d\n", fileNum); - playAudioFile(fileNum); -} - -void testLoRa() { - if (!loraReady) { Serial.println("LoRa not ready"); return; } - Serial.println("Sending test LoRa packet..."); - SensorData d = readAllSensors(); - d.emergency = false; - d.motion = motionData; - sendLoRaData(d); - Serial.println("Test LoRa sent."); -} - -void testEmergency() { - Serial.println("Triggering emergency now..."); - emergencyTriggered = true; -} - -void testButton() { - Serial.println("Testing emergency button for 10s..."); - unsigned long start = millis(); - bool last = digitalRead(EMERGENCY_BUTTON_PIN); - while (millis() - start < 10000) { - bool cur = digitalRead(EMERGENCY_BUTTON_PIN); - if (cur != last) { - Serial.printf("Button state change: %s\n", cur ? "HIGH" : "LOW"); - last = cur; - } - delay(100); - } - Serial.println("Button test complete."); -} - -void testAllSensors() { - testDHT(); - testAllMQ(); - testLoRa(); -} - -// ========================= Audio control ========================= - -void sendCommand(byte cmd, byte param1, byte param2, bool feedback) { - byte packet[10]; - packet[0] = FRAME_START; - packet[1] = VERSION; - packet[2] = 0x06; - packet[3] = cmd; - packet[4] = feedback ? WITH_FEEDBACK : NO_FEEDBACK; - packet[5] = param1; - packet[6] = param2; - uint16_t sum = packet[1] + packet[2] + packet[3] + packet[4] + packet[5] + packet[6]; - uint16_t checksum = 0xFFFF - sum + 1; - packet[7] = (checksum >> 8) & 0xFF; - packet[8] = checksum & 0xFF; - packet[9] = FRAME_END; - fnM16pSerial.write(packet, 10); -} - -void setVolume(int volume) { - if (volume < 0) volume = 0; - if (volume > 30) volume = 30; - sendCommand(CMD_VOLUME, 0x00, volume, false); -} - -void playAudioFile(int fileNumber) { - if (fileNumber < 1 || fileNumber > 10) return; - sendCommand(CMD_PLAY_TRACK, 0x00, fileNumber, false); - delay(60); -} - -void stopAudio() { - sendCommand(CMD_STOP, 0x00, 0x00, false); -} - -// ========================= Calibration ========================= - -void calibrateSensors() { - Serial.println("Starting sensor calibration..."); - delay(500); - calibrateSensor(MQ2_ANALOG_PIN, &mq2_cal, "MQ2", MQ2_DANGER_THRESHOLD); - calibrateSensor(MQ9_ANALOG_PIN, &mq9_cal, "MQ9", MQ9_DANGER_THRESHOLD); - calibrateSensor(MQ135_ANALOG_PIN, &mq135_cal, "MQ135", MQ135_DANGER_THRESHOLD); - Serial.println("Calibration completed."); -} - -void calibrateSensor(int pin, SensorCalibration* cal, String sensorName, int minThreshold) { - Serial.print("Calibrating " + sensorName + " ..."); - float sum = 0; - for (int i=0;ibaseline = sum / CALIBRATION_SAMPLES; - float calculatedThreshold = cal->baseline * DANGER_MULTIPLIER; - cal->dangerThreshold = (calculatedThreshold > minThreshold) ? calculatedThreshold : minThreshold; - cal->calibrated = true; - Serial.println(" Done"); -} - -// ========================= Sensors reading/display/alerts/LoRa ========================= - -SensorData readAllSensors() { - SensorData data; - data.timestamp = millis(); - - if (millis() - lastDHTReading > DHT_READING_INTERVAL) { - float rt = dht.readTemperature(); - float rh = dht.readHumidity(); - if (!isnan(rt) && rt >= -40 && rt <= 80) { data.temperature = rt; lastValidTemperature = rt; } - else data.temperature = lastValidTemperature; - - if (!isnan(rh) && rh >= 0 && rh <= 100) { - float corr = rh - 30.0f; - if (corr < 0.0f) corr = 0.0f; - if (corr > 100.0f) corr = 100.0f; - data.humidity = corr; - lastValidHumidity = corr; - } else data.humidity = lastValidHumidity; - - lastDHTReading = millis(); - } else { - data.temperature = lastValidTemperature; - data.humidity = lastValidHumidity; - } - - data.mq2_analog = analogRead(MQ2_ANALOG_PIN); - data.mq9_analog = analogRead(MQ9_ANALOG_PIN); - data.mq135_analog = analogRead(MQ135_ANALOG_PIN); - - data.mq2_digital = digitalRead(MQ2_DIGITAL_PIN) == LOW; - data.mq9_digital = digitalRead(MQ9_DIGITAL_PIN) == LOW; - data.mq135_digital = digitalRead(MQ135_DIGITAL_PIN) == LOW; - - data.emergency = false; - data.motion = motionData; - return data; -} - -void displayReadings(SensorData data) { - Serial.println("=== SENSOR READINGS ==="); - Serial.printf("Timestamp: %lu\n", data.timestamp); - Serial.println("DHT11:"); - Serial.printf(" Temperature: %.2f°C\n", data.temperature); - Serial.printf(" Humidity: %.2f%%\n", data.humidity); - Serial.println("MQ2 (Smoke/LPG/Gas):"); - Serial.printf(" Digital: %s | Analog: %d", data.mq2_digital ? "GAS DETECTED" : "No Gas", data.mq2_analog); - Serial.println(checkSensorDanger(data.mq2_analog, &mq2_cal, MQ2_DANGER_THRESHOLD) ? " [DANGER]" : " [Safe]"); - Serial.println("MQ9 (Carbon Monoxide):"); - Serial.printf(" Digital: %s | Analog: %d", data.mq9_digital ? "CO DETECTED" : "No CO", data.mq9_analog); - Serial.println(checkSensorDanger(data.mq9_analog, &mq9_cal, MQ9_DANGER_THRESHOLD) ? " [DANGER]" : " [Safe]"); - Serial.println("MQ135 (Air Quality):"); - Serial.printf(" Digital: %s | Analog: %d", data.mq135_digital ? "POOR AIR" : "Good Air", data.mq135_analog); - Serial.println(checkSensorDanger(data.mq135_analog, &mq135_cal, MQ135_DANGER_THRESHOLD) ? " [DANGER]" : " [Safe]"); - Serial.println("MOTION:"); - Serial.printf(" Accel: %.2f g\n", data.motion.totalAccel / G); - Serial.printf(" Gyro: %.2f °/s\n", data.motion.totalGyro); - Serial.printf(" Fall Detected: %s\n", data.motion.fallDetected ? "YES" : "NO"); -} - -void checkAlerts(SensorData data) { - static unsigned long lastAlert = 0; - unsigned long now = millis(); - if (now - lastAlert < 60000) return; - - if (data.motion.fallDetected) { - if (audioReady) playAudioFile(FALL_DETECTED); - lastAlert = now; - return; - } - if (checkSensorDanger(data.mq2_analog, &mq2_cal, MQ2_DANGER_THRESHOLD)) { - playAudioFile(SMOKE_ALERT); lastAlert = now; return; - } - if (checkSensorDanger(data.mq9_analog, &mq9_cal, MQ9_DANGER_THRESHOLD)) { - playAudioFile(CO_ALERT); lastAlert = now; return; - } - if (checkSensorDanger(data.mq135_analog, &mq135_cal, MQ135_DANGER_THRESHOLD)) { - playAudioFile(AIR_QUALITY_WARNING); lastAlert = now; return; - } - if (data.temperature > 45.0) { playAudioFile(HIGH_TEMP_ALERT); lastAlert = now; return; } - if (data.temperature < 0.0) { playAudioFile(LOW_TEMP_ALERT); lastAlert = now; return; } - if (data.humidity > 90.0) { playAudioFile(HIGH_HUMIDITY_ALERT); lastAlert = now; return; } - if (data.humidity < 10.0) { playAudioFile(LOW_HUMIDITY_ALERT); lastAlert = now; return; } -} - -void sendLoRaData(SensorData data) { - packetCount++; - StaticJsonDocument<512> doc; - doc["nodeId"] = NODE_ID; - doc["packetCount"] = packetCount; - doc["timestamp"] = data.timestamp; - doc["temperature"] = data.temperature; - doc["humidity"] = data.humidity; - doc["mq2_analog"] = data.mq2_analog; - doc["mq9_analog"] = data.mq9_analog; - doc["mq135_analog"] = data.mq135_analog; - doc["mq2_digital"] = data.mq2_digital; - doc["mq9_digital"] = data.mq9_digital; - doc["mq135_digital"] = data.mq135_digital; - doc["air_quality"] = getAirQualityRating(data.mq135_analog); - doc["emergency"] = data.emergency; - // motion - doc["fall_detected"] = data.motion.fallDetected; - doc["activity"] = data.motion.fallDetected ? String("FALLEN - HELP!") : String("Stationary"); - doc["total_accel"] = data.motion.totalAccel; - doc["total_gyro"] = data.motion.totalGyro; - doc["motion_active"] = data.motion.motionDetected; - - String jsonString; - serializeJson(doc, jsonString); - - if (data.emergency || data.motion.fallDetected) { - Serial.println("\n╔════════ EMERGENCY LoRa ═════════╗"); - if (data.motion.fallDetected) Serial.println("║ FALL DETECTED! ║"); - Serial.println("╚════════════════════════════════╝"); - } - - Serial.print("LoRa Packet #"); Serial.print(packetCount); Serial.println(data.emergency ? " [EMERGENCY]" : " [NORMAL]"); - Serial.print("Payload size: "); Serial.print(jsonString.length()); Serial.println(" bytes"); - Serial.println("Payload: " + jsonString); - - LoRa.beginPacket(); - LoRa.print(jsonString); - LoRa.endPacket(); - - Serial.println("✓ Packet sent"); -} - -String getAirQualityRating(int value) { - if (value < 800) return "Excellent"; - else if (value < 1200) return "Good"; - else if (value < 1800) return "Moderate"; - else if (value < 2400) return "Poor"; - else return "Very Poor"; -} - -// ========================= Emergency Button ========================= -void checkEmergencyButton() { - static bool lastState = HIGH; // using INPUT_PULLUP -> HIGH when released - static unsigned long lastChangeTime = 0; - bool currentState = digitalRead(EMERGENCY_BUTTON_PIN); - if (currentState != lastState && (millis() - lastChangeTime > 50)) { - lastChangeTime = millis(); - if (currentState == LOW) { // pressed - unsigned long now = millis(); - if (now - lastTapTime < TAP_TIMEOUT) tapCount++; - else tapCount = 1; - lastTapTime = now; - Serial.printf("BUTTON TAP #%d/%d\n", tapCount, REQUIRED_TAPS); - if (tapCount >= REQUIRED_TAPS) { - Serial.println("TRIPLE TAP - EMERGENCY TRIGGERED"); - emergencyTriggered = true; - tapCount = 0; - } - } - } - if (millis() - lastTapTime > TAP_TIMEOUT && tapCount > 0) tapCount = 0; - lastState = currentState; -} - -void handleEmergency() { - Serial.println("\n████████ EMERGENCY MODE █████████"); - SensorData s = readAllSensors(); - s.emergency = true; - s.motion = motionData; - Serial.println("EMERGENCY SNAPSHOT:"); - Serial.printf(" Temp: %.2f C Hum: %.2f %%\n", s.temperature, s.humidity); - Serial.printf(" MQ2:%d MQ9:%d MQ135:%d\n", s.mq2_analog, s.mq9_analog, s.mq135_analog); - Serial.printf(" Fall: %s\n", s.motion.fallDetected ? "YES":"NO"); - if (loraReady) sendLoRaData(s); else Serial.println("LoRa not ready."); - if (audioReady) playAudioFile(FALL_DETECTED); - delay(800); -} - -// ========================= Status ========================= -void printSystemStatus() { - Serial.println("\n=== SYSTEM STATUS ==="); - Serial.printf("Node ID: %s\n", NODE_ID); - Serial.printf("Uptime: %lu s\n", millis()/1000); - Serial.printf("Packets sent: %d\n", packetCount); - Serial.printf("DHT: %s MPU: %s LoRa: %s Audio: %s\n", - dhtReady ? "YES":"NO", - mpuReady ? "YES":"NO", - loraReady ? "YES":"NO", - audioReady ? "YES":"NO"); - Serial.printf("MQ2 cal: %s MQ9 cal: %s MQ135 cal: %s\n", - mq2_cal.calibrated?"YES":"NO", - mq9_cal.calibrated?"YES":"NO", - mq135_cal.calibrated?"YES":"NO"); - Serial.printf("Last Temp: %.2f C Last Hum (corrected): %.2f %%\n", lastValidTemperature, lastValidHumidity); - Serial.println("=====================\n"); -} diff --git a/audio_module.md b/audio_module.md deleted file mode 100644 index 10572a5..0000000 --- a/audio_module.md +++ /dev/null @@ -1,140 +0,0 @@ -Based on your working code, here's the complete reference for future LLM interactions: - -## **📋 FN-M16P Communication Reference** - -### **🔌 Hardware Configuration** -```cpp -// UART Configuration -HardwareSerial fnm16p(2); // Use UART2 on ESP32 -fnm16p.begin(9600, SERIAL_8N1, 16, 17); // RX=GPIO16, TX=GPIO17 - -// Pin Connections -ESP32 GPIO 16 (RX2) → FN-M16P TX -ESP32 GPIO 17 (TX2) → FN-M16P RX -ESP32 GND → FN-M16P GND -ESP32 5V/3.3V → FN-M16P VCC -``` - -### **📦 Packet Format** -``` -[0x7E] [0xFF] [0x06] [CMD] [Feedback] [Para_MSB] [Para_LSB] [Check_MSB] [Check_LSB] [0xEF] - -Byte Position: 0 1 2 3 4 5 6 7 8 9 -Total Length: 10 bytes -``` - -**Field Details:** -- `0x7E` - Frame Start (always) -- `0xFF` - Version (always) -- `0x06` - Data Length (always for standard commands) -- `CMD` - Command byte -- `Feedback` - `0x00` (no reply) or `0x01` (request reply) -- `Para_MSB` - Parameter high byte -- `Para_LSB` - Parameter low byte -- `Check_MSB` - Checksum high byte -- `Check_LSB` - Checksum low byte -- `0xEF` - Frame End (always) - -### **🧮 Checksum Calculation** -```cpp -// Formula: 0xFFFF - (Ver + Len + CMD + Feedback + Para_MSB + Para_LSB) + 1 -uint16_t sum = 0xFF + 0x06 + cmd + feedback + param1 + param2; -uint16_t checksum = 0xFFFF - sum + 1; -byte check_msb = (checksum >> 8) & 0xFF; -byte check_lsb = checksum & 0xFF; -``` - -### **🎵 Command Reference** -```cpp -// Common Commands -const byte CMD_PLAY_TRACK = 0x03; // Play specific track -const byte CMD_VOLUME = 0x06; // Set volume (0-30) -const byte CMD_STOP = 0x16; // Stop playback -const byte CMD_PAUSE = 0x0E; // Pause playback -const byte CMD_PLAY = 0x0D; // Resume/Play -const byte CMD_QUERY_STATUS = 0x42; // Query module status -const byte CMD_QUERY_FILES = 0x48; // Query total files -``` - -### **📂 File Naming Convention** -- Files must be named: `0001.mp3`, `0002.mp3`, `0003.mp3`, etc. -- SD card format: FAT32 -- Maximum 8 files supported in your current setup -- Files stored in root directory (not in folders) - -### **💻 Working sendCommand Function** -```cpp -void sendCommand(byte cmd, byte param1 = 0x00, byte param2 = 0x00, bool feedback = false) { - byte packet[10]; - - packet[0] = 0x7E; // Frame Start - packet[1] = 0xFF; // Version - packet[2] = 0x06; // Length - packet[3] = cmd; // Command - packet[4] = feedback ? 0x01 : 0x00; // Feedback flag - packet[5] = param1; // Para_MSB - packet[6] = param2; // Para_LSB - - // Calculate checksum - uint16_t sum = packet[1] + packet[2] + packet[3] + packet[4] + packet[5] + packet[6]; - uint16_t checksum = 0xFFFF - sum + 1; - - packet[7] = (checksum >> 8) & 0xFF; // Check_MSB - packet[8] = checksum & 0xFF; // Check_LSB - packet[9] = 0xEF; // Frame End - - fnm16p.write(packet, 10); -} -``` - -### **🎯 Example Packets** - -**Play Track 1:** -``` -Command: sendCommand(0x03, 0x00, 0x01, false); -Packet: 7E FF 06 03 00 00 01 FE F6 EF -``` - -**Set Volume to 25:** -``` -Command: sendCommand(0x06, 0x00, 0x19, false); -Packet: 7E FF 06 06 00 00 19 FE DB EF -``` - -**Query Status:** -``` -Command: sendCommand(0x42, 0x00, 0x00, true); -Packet: 7E FF 06 42 01 00 00 FE B8 EF -``` - -### **⚙️ System Parameters** -- **Baud Rate:** 9600 (confirmed working) -- **Data Bits:** 8 -- **Parity:** None -- **Stop Bits:** 1 -- **Volume Range:** 0-30 -- **Track Range:** 1-8 (for your setup) -- **Response Time:** Allow 100-500ms delay between commands - -### **🔍 Module Responses** -When feedback is enabled, expect responses starting with `0x7E` and ending with `0xEF`. Common response patterns: -- `0x7E FF 06 41 00 ...` - Acknowledgment -- `0x7E FF 06 3D 00 ...` - Track playing -- `0x7E FF 06 3C 00 ...` - Track finished - ---- - -## **🤖 For LLM Reference - Copy This:** - -``` -FN-M16P MP3 Module Communication Protocol: -- Baud Rate: 9600 -- Packet Format: [0x7E][0xFF][0x06][CMD][Feedback][Para_MSB][Para_LSB][Check_MSB][Check_LSB][0xEF] -- Checksum: 0xFFFF - (0xFF + 0x06 + CMD + Feedback + Para_MSB + Para_LSB) + 1 -- ESP32 Pins: GPIO16=RX, GPIO17=TX -- Files: 0001.mp3 to 0008.mp3 in SD card root -- Commands: Play=0x03, Volume=0x06, Stop=0x16 -- Working sendCommand function provided above -``` - -Save this reference - it contains everything needed for accurate code generation! diff --git a/before wristband integration final code b/before wristband integration final code deleted file mode 100644 index b4c1aa8..0000000 --- a/before wristband integration final code +++ /dev/null @@ -1,1166 +0,0 @@ -/* - ESP32 Multi-Sensor System with Emergency Button + MPU6050 (Clone WHO_AM_I fix) - - Preserves your pin layout and calibration logic (DANGER_MULTIPLIER = 2.0) - - Accepts MPU6050 WHO_AM_I values 0x68, 0x69, and 0x72 (common clones) - - If WHO_AM_I == 0x00 it will attempt bus recovery and a safe fallback: - * try re-read WHO_AM_I after recovery - * if still 0x00, attempt to write power-up and read accelerometer registers - — if accel data looks plausible proceed (sensor likely responding despite ID) - * otherwise disable MPU features and provide clear diagnostics - - Robust I2C read/write with retries and bus recovery to avoid "I2C software timeout" spam - - Keeps original JSON payload format for LoRa - - Keeps original calibration logic & DHT -30% manual humidity correction as requested - - Pin layout preserved exactly as you provided -*/ - -#include -#include -#include -#include -#include -#include -#include - -// ========================= MPU6050 I2C Register Definitions ========================= -#define MPU6050_ADDR 0x68 -#define PWR_MGMT_1 0x6B -#define ACCEL_XOUT_H 0x3B -#define GYRO_XOUT_H 0x43 -#define CONFIG 0x1A -#define GYRO_CONFIG 0x1B -#define ACCEL_CONFIG 0x1C -#define WHO_AM_I 0x75 - -// ========================= Pin definitions (preserved) ========================= -// MQ sensors -#define MQ2_DIGITAL_PIN 27 // ✔️ Safe -#define MQ2_ANALOG_PIN 32 // ✔️ ADC1_CH4 -#define MQ9_DIGITAL_PIN 14 // ✔️ Safe -#define MQ9_ANALOG_PIN 33 // ✔️ ADC1_CH5 -#define MQ135_DIGITAL_PIN 13 // ✔️ Safe -#define MQ135_ANALOG_PIN 35 // ✔️ ADC1_CH7 - -// FN-M16P Audio Module pins (UART2) -#define FN_M16P_RX 16 // ✔️ UART2 RX -#define FN_M16P_TX 17 // ✔️ UART2 TX - -// DHT11 pin -#define DHT11_PIN 25 // ✔️ GPIO25 (manual humidity correction applied) -#define DHT_TYPE DHT11 - -// MPU6050 pins (I2C) -#define MPU6050_SDA 21 // ✔️ I2C SDA -#define MPU6050_SCL 22 // ✔️ I2C SCL -#define MPU6050_INT 34 // ✔️ Input-only - -// LoRa Module pins (SPI) -#define LORA_SCK 18 // ✔️ SPI SCK -#define LORA_MISO 19 // ✔️ SPI MISO -#define LORA_MOSI 23 // ✔️ SPI MOSI -#define LORA_SS 5 // ⚠️ Needs pull-up resistor on some boards -#define LORA_RST 4 // ✔️ Safe -#define LORA_DIO0 26 // ✔️ Safe - -// EMERGENCY BUTTON PIN -#define EMERGENCY_BUTTON_PIN 15 // ✔️ GPIO15; use INPUT_PULLUP (pressed = LOW) - -// LoRa frequency -#define LORA_BAND 915E6 - -// ========================= System constants ========================= -#define NODE_ID "001" - -#define MQ2_DANGER_THRESHOLD 1600 -#define MQ9_DANGER_THRESHOLD 3800 -#define MQ135_DANGER_THRESHOLD 1800 - -#define FALLBACK_TEMPERATURE 27.0 -#define FALLBACK_HUMIDITY 47.0 - -#define CALIBRATION_SAMPLES 10 -#define DANGER_MULTIPLIER 2.0 -#define CALIBRATION_DELAY 2000 - -const int TAP_TIMEOUT = 600; -const int REQUIRED_TAPS = 3; - -// FN-M16P commands -const byte FRAME_START = 0x7E; -const byte FRAME_END = 0xEF; -const byte VERSION = 0xFF; -const byte NO_FEEDBACK = 0x00; -const byte WITH_FEEDBACK = 0x01; -const byte CMD_PLAY_TRACK = 0x03; -const byte CMD_VOLUME = 0x06; -const byte CMD_STOP = 0x16; - -// Audio files mapping -enum AudioFiles { - BOOT_AUDIO = 1, - SMOKE_ALERT = 2, - CO_ALERT = 3, - AIR_QUALITY_WARNING = 4, - HIGH_TEMP_ALERT = 5, - LOW_TEMP_ALERT = 6, - HIGH_HUMIDITY_ALERT = 7, - LOW_HUMIDITY_ALERT = 8, - FALL_DETECTED = 9, - MOTION_ALERT = 10 -}; - -// ========================= Types & Globals ========================= -struct SensorCalibration { - float baseline; - float dangerThreshold; - bool calibrated; -}; - -struct MotionData { - float totalAccel; // m/s^2 - float totalGyro; // deg/s - bool fallDetected; - bool impactDetected; - bool motionDetected; - unsigned long lastMotionTime; -}; - -struct SensorData { - float temperature; - float humidity; - int mq2_analog; - int mq9_analog; - int mq135_analog; - bool mq2_digital; - bool mq9_digital; - bool mq135_digital; - bool emergency; - unsigned long timestamp; - MotionData motion; -}; - -SensorCalibration mq2_cal = {0,0,false}; -SensorCalibration mq9_cal = {0,0,false}; -SensorCalibration mq135_cal = {0,0,false}; - -DHT dht(DHT11_PIN, DHT_TYPE); -HardwareSerial fnM16pSerial(2); - -bool audioReady = false; -bool loraReady = false; -bool dhtReady = false; -bool mpuReady = false; - -unsigned long lastLoRaSend = 0; -int loraInterval = 30000; -int packetCount = 0; - -unsigned long lastDHTReading = 0; -const unsigned long DHT_READING_INTERVAL = 2000; -float lastValidTemperature = FALLBACK_TEMPERATURE; -float lastValidHumidity = FALLBACK_HUMIDITY; - -// Emergency Button Variables -volatile int tapCount = 0; -volatile unsigned long lastTapTime = 0; -volatile bool emergencyTriggered = false; - -// Motion variables -MotionData motionData = {0}; - -// MPU detection / fall detector internals -const float G = 9.80665f; -const float FREE_FALL_G_THRESHOLD = 0.6f; // g -const unsigned long FREE_FALL_MIN_MS = 120; // ms -const float IMPACT_G_THRESHOLD = 3.5f; // g -const unsigned long IMPACT_WINDOW_MS = 1200; // ms -const unsigned long STATIONARY_CONFIRM_MS = 800; // ms -const float ROTATION_IMPACT_THRESHOLD = 400.0f; // deg/s - -bool inFreeFall = false; -bool fallInProgress = false; -bool impactSeen = false; -unsigned long freeFallStart = 0; -unsigned long fallStartTime = 0; -unsigned long impactTime = 0; -unsigned long stationarySince = 0; -float accelFiltered = G; -const float ALPHA = 0.85f; - -unsigned long lastI2CAttempt = 0; - -// ========================= Forward declarations ========================= -void printTestMenu(); -void handleTestCommand(String cmd); -void testDHT(); -void testMQ2(); -void testMQ9(); -void testMQ135(); -void testAllMQ(); -void testAudio(int fileNum); -void testLoRa(); -void testEmergency(); -void testButton(); -void testAllSensors(); -void printSystemStatus(); -void scanI2CDevices(); -void setVolume(int volume); -void playAudioFile(int fileNumber); -void stopAudio(); -void calibrateSensors(); -void calibrateSensor(int pin, SensorCalibration* cal, String sensorName, int minThreshold); -bool checkSensorDanger(int currentValue, SensorCalibration* cal, int staticDangerThreshold); -SensorData readAllSensors(); -void displayReadings(SensorData data); -void checkAlerts(SensorData data); -void sendLoRaData(SensorData data); -String getAirQualityRating(int value); -void checkEmergencyButton(); -void handleEmergency(); -void sendCommand(byte cmd, byte param1, byte param2, bool feedback); - -// I2C helpers & MPU -bool i2cBusRecover(); -bool safeWireRequest(uint8_t addr, uint8_t reg, uint8_t *buf, size_t len, int retries=3); -bool safeWireWrite(uint8_t addr, uint8_t reg, uint8_t val, int retries=3); -bool initMPU6050(); -void readMPU6050Data(int16_t *ax, int16_t *ay, int16_t *az, int16_t *gx, int16_t *gy, int16_t *gz); -void monitorMotion(); -void detectFallAndHandle(); - -// ========================= I2C helpers & bus recovery ========================= -// Attempt to unstuck a bus by toggling SCL. Returns true if SDA released. -bool i2cBusRecover() { - Wire.end(); - delay(10); - pinMode(MPU6050_SCL, OUTPUT); - pinMode(MPU6050_SDA, INPUT_PULLUP); - - // If SDA already high, bus free - if (digitalRead(MPU6050_SDA) == HIGH) { - Wire.begin(MPU6050_SDA, MPU6050_SCL); - Wire.setClock(100000); // Use 100kHz for clones - return true; - } - - // Pulse clock up to 9 times - for (int i=0;i<9;i++) { - digitalWrite(MPU6050_SCL, HIGH); - delayMicroseconds(500); - digitalWrite(MPU6050_SCL, LOW); - delayMicroseconds(500); - if (digitalRead(MPU6050_SDA) == HIGH) break; - } - - // Generate STOP condition - pinMode(MPU6050_SDA, OUTPUT); - digitalWrite(MPU6050_SDA, LOW); - delayMicroseconds(200); - digitalWrite(MPU6050_SCL, HIGH); - delayMicroseconds(200); - digitalWrite(MPU6050_SDA, HIGH); - delayMicroseconds(200); - - Wire.begin(MPU6050_SDA, MPU6050_SCL); - Wire.setClock(100000); // Use 100kHz for clones - pinMode(MPU6050_SDA, INPUT_PULLUP); - pinMode(MPU6050_SCL, INPUT_PULLUP); - - return digitalRead(MPU6050_SDA) == HIGH; -} - -// robust read with retries + recovery - optimized for clones -bool safeWireRequest(uint8_t addr, uint8_t reg, uint8_t *buf, size_t len, int retries) { - for (int attempt=0; attempt 4096 LSB/g and gyro ±500 dps -> 65.5 LSB/°/s - float ax = (axr / 4096.0f) * G; - float ay = (ayr / 4096.0f) * G; - float az = (azr / 4096.0f) * G; - float gx = (gxr / 65.5f); - float gy = (gyr / 65.5f); - float gz = (gzr / 65.5f); - - motionData.totalAccel = sqrt(ax*ax + ay*ay + az*az); - motionData.totalGyro = sqrt(gx*gx + gy*gy + gz*gz); - - // smoothing - accelFiltered = ALPHA * accelFiltered + (1.0f - ALPHA) * motionData.totalAccel; - - if (fabs(motionData.totalAccel - accelFiltered) > 0.2f * G || motionData.totalGyro > 25.0f) { - motionData.lastMotionTime = millis(); - motionData.motionDetected = true; - } else { - motionData.motionDetected = false; - } - - detectFallAndHandle(); -} - -void detectFallAndHandle() { - unsigned long now = millis(); - float totG = motionData.totalAccel / G; - float totGyro = motionData.totalGyro; - - // free-fall detection - if (totG < FREE_FALL_G_THRESHOLD) { - if (!inFreeFall) { inFreeFall = true; freeFallStart = now; } - else if ((now - freeFallStart) >= FREE_FALL_MIN_MS && !fallInProgress) { - fallInProgress = true; - fallStartTime = now; - impactSeen = false; - motionData.impactDetected = false; - } - } else { - if (inFreeFall) inFreeFall = false; - } - - // impact detection - if (fallInProgress && !impactSeen) { - if (totG >= IMPACT_G_THRESHOLD || totGyro >= ROTATION_IMPACT_THRESHOLD) { - impactSeen = true; - impactTime = now; - motionData.impactDetected = true; - } else if (now - fallStartTime > IMPACT_WINDOW_MS) { - // timed out without impact - fallInProgress = false; - impactSeen = false; - motionData.impactDetected = false; - } - } - - // confirm fall if impactSeen and post-impact stationary - if (impactSeen) { - float accelVariationG = fabs((motionData.totalAccel / G) - 1.0f); - if (accelVariationG < 0.35f && motionData.totalGyro < 50.0f) { - if (stationarySince == 0) stationarySince = now; - if (now - stationarySince >= STATIONARY_CONFIRM_MS) { - motionData.fallDetected = true; - emergencyTriggered = true; - if (audioReady) playAudioFile(FALL_DETECTED); - Serial.println("\n╔════════════════════════════════════╗"); - Serial.println("║ FALL CONFIRMED - IMPACT + STATIONARY ║"); - Serial.println("╚════════════════════════════════════╝"); - Serial.printf("Acceleration: %.2f g\n", motionData.totalAccel / G); - Serial.printf("Gyroscope: %.2f °/s\n", motionData.totalGyro); - // reset - fallInProgress = false; - impactSeen = false; - stationarySince = 0; - } - } else { - stationarySince = 0; - if (now - impactTime > IMPACT_WINDOW_MS) { - fallInProgress = false; impactSeen = false; stationarySince = 0; motionData.impactDetected = false; - } - } - } - - // immediate large spike detection (conservative) - static float lastTotalAccel = G; - static float lastTotalGyro = 0.0f; - float accelDeltaG = fabs((motionData.totalAccel - lastTotalAccel) / G); - float gyroDelta = fabs(motionData.totalGyro - lastTotalGyro); - - if (!motionData.fallDetected) { - if (accelDeltaG > 2.5f && (motionData.totalAccel / G) > 2.0f) { - // short confirmation window (non-blocking ideally, but keep short) - unsigned long t0 = millis(); - bool remainedStationary = true; - while (millis() - t0 < STATIONARY_CONFIRM_MS) { - // rely on monitorMotion() being called frequently in loop(); if motion remains low then confirm - if (motionData.motionDetected) { remainedStationary = false; break; } - delay(40); - } - if (remainedStationary) { - motionData.fallDetected = true; - emergencyTriggered = true; - if (audioReady) playAudioFile(FALL_DETECTED); - Serial.println("\n╔════════════════════════════════════╗"); - Serial.println("║ FALL CONFIRMED - SUDDEN IMPACT ║"); - Serial.println("╚════════════════════════════════════╝"); - } - } else if (gyroDelta > 300.0f && motionData.totalGyro > 400.0f) { - unsigned long t0 = millis(); - bool remainedStationary = true; - while (millis() - t0 < STATIONARY_CONFIRM_MS) { - if (motionData.motionDetected) { remainedStationary = false; break; } - delay(40); - } - if (remainedStationary) { - motionData.fallDetected = true; - emergencyTriggered = true; - if (audioReady) playAudioFile(FALL_DETECTED); - Serial.println("\n╔════════════════════════════════════╗"); - Serial.println("║ FALL CONFIRMED - ROTATION SPIKE ║"); - Serial.println("╚════════════════════════════════════╝"); - } - } - } - - lastTotalAccel = motionData.totalAccel; - lastTotalGyro = motionData.totalGyro; -} - -// ========================= Setup & Loop ========================= -void setup() { - Serial.begin(115200); - delay(800); - Serial.println("\n\n================================="); - Serial.println("ESP32 Multi-Sensor System - MPU6050 Clone WHO_AM_I Fix"); - Serial.println("================================="); - - // Initialize I2C (preserved pins) - Wire.begin(MPU6050_SDA, MPU6050_SCL); - Wire.setClock(100000); // Try slower speed first: 100kHz instead of 400kHz - - // Enable internal pull-ups - pinMode(MPU6050_SDA, INPUT_PULLUP); - pinMode(MPU6050_SCL, INPUT_PULLUP); - - // MQ digital pins - pinMode(MQ2_DIGITAL_PIN, INPUT); - pinMode(MQ9_DIGITAL_PIN, INPUT); - pinMode(MQ135_DIGITAL_PIN, INPUT); - - // Emergency button -> use internal pull-up (pressed == LOW) - pinMode(EMERGENCY_BUTTON_PIN, INPUT_PULLUP); - Serial.printf("Emergency button: GPIO%d (INPUT_PULLUP). Press => LOW\n", EMERGENCY_BUTTON_PIN); - - // MPU INT pin - pinMode(MPU6050_INT, INPUT); - - Serial.println("\n--- Initializing MPU6050 ---"); - if (!initMPU6050()) { - Serial.println("⚠ MPU6050 init failed. Motion features will be disabled."); - Serial.println(" Try running 'scan' command to check I2C devices."); - mpuReady = false; - } else { - Serial.println("✓ MPU6050 ready for motion detection"); - mpuReady = true; - motionData.lastMotionTime = millis(); - accelFiltered = G; - } - - // DHT11 initialization (preserved pin 25) and manual humidity correction -30% - Serial.println("\nInitializing DHT11 sensor..."); - dht.begin(); - delay(1500); - bool dhtWorking = false; - for (int i=0;i<3;i++) { - float t = dht.readTemperature(); - float h = dht.readHumidity(); - if (!isnan(t) && !isnan(h)) { - dhtWorking = true; - dhtReady = true; - lastValidTemperature = t; - float corr = h - 30.0f; - if (corr < 0.0f) corr = 0.0f; - if (corr > 100.0f) corr = 100.0f; - lastValidHumidity = corr; - Serial.printf("✓ DHT11: Temp %.1fC Humidity (corrected) %.1f%%\n", lastValidTemperature, lastValidHumidity); - break; - } - delay(1000); - } - if (!dhtWorking) { - Serial.println("⚠ DHT11 ERROR - Check DATA wiring to GPIO25 and power."); - dhtReady = false; - } - - // Audio init - fnM16pSerial.begin(9600, SERIAL_8N1, FN_M16P_RX, FN_M16P_TX); - delay(200); - setVolume(30); - audioReady = true; - Serial.println("✓ Audio module initialized."); - - // LoRa init with SS high & RST pulse - Serial.println("Initializing LoRa..."); - pinMode(LORA_SS, OUTPUT); - digitalWrite(LORA_SS, HIGH); - pinMode(LORA_RST, OUTPUT); - digitalWrite(LORA_RST, HIGH); - SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_SS); - LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0); - digitalWrite(LORA_RST, LOW); delay(10); - digitalWrite(LORA_RST, HIGH); delay(10); - if (!LoRa.begin(LORA_BAND)) { - Serial.println("⚠ LoRa initialization failed. Check wiring/antenna."); - loraReady = false; - } else { - loraReady = true; - Serial.println("✓ LoRa initialized."); - } - - Serial.println("Warming gas sensors (15s)..."); - delay(15000); - - Serial.println("Calibrating MQ sensors..."); - calibrateSensors(); - - Serial.println("\nSYSTEM READY."); - printTestMenu(); - - if (audioReady) { playAudioFile(BOOT_AUDIO); delay(1000); } -} - -void loop() { - // serial test commands - if (Serial.available() > 0) { - String cmd = Serial.readStringUntil('\n'); - cmd.trim(); - cmd.toLowerCase(); - handleTestCommand(cmd); - } - - // emergency button - checkEmergencyButton(); - - // monitor motion - if (mpuReady) monitorMotion(); - - // emergency handling - if (emergencyTriggered) { - handleEmergency(); - emergencyTriggered = false; - } - - // periodic snapshot and LoRa send - static unsigned long lastNormal = 0; - if (millis() - lastNormal >= 10000) { - lastNormal = millis(); - SensorData data = readAllSensors(); - data.emergency = false; - data.motion = motionData; - displayReadings(data); - checkAlerts(data); - if (loraReady && (millis() - lastLoRaSend > loraInterval)) { - sendLoRaData(data); - lastLoRaSend = millis(); - } - Serial.println("------------------------"); - } - - delay(50); -} - -// ========================= Test commands & helpers ========================= - -void printTestMenu() { - Serial.println("\n╔═══════════════════════════════════════╗"); - Serial.println("║ TEST COMMANDS MENU ║"); - Serial.println("╠═══════════════════════════════════════╣"); - Serial.println("║ dht, mq2, mq9, mq135, mq, all ║"); - Serial.println("║ audio1..audio10, stop, volume+, volume-║"); - Serial.println("║ lora, button, emergency, calibrate ║"); - Serial.println("║ status, scan/i2c, help/menu ║"); - Serial.println("╚═══════════════════════════════════════╝\n"); -} - -void handleTestCommand(String cmd) { - Serial.println("\n>>> EXECUTING TEST: " + cmd + " <<<\n"); - if (cmd == "help" || cmd == "menu") printTestMenu(); - else if (cmd == "dht") testDHT(); - else if (cmd == "mq2") testMQ2(); - else if (cmd == "mq9") testMQ9(); - else if (cmd == "mq135") testMQ135(); - else if (cmd == "mq") testAllMQ(); - else if (cmd.startsWith("audio")) { - int n = cmd.substring(5).toInt(); - if (n >= 1 && n <= 10) testAudio(n); - } - else if (cmd == "stop") { stopAudio(); Serial.println("Audio stopped."); } - else if (cmd == "volume+") { setVolume(25); Serial.println("Volume 25"); } - else if (cmd == "volume-") { setVolume(15); Serial.println("Volume 15"); } - else if (cmd == "lora") testLoRa(); - else if (cmd == "button") testButton(); - else if (cmd == "emergency") testEmergency(); - else if (cmd == "all") testAllSensors(); - else if (cmd == "calibrate") calibrateSensors(); - else if (cmd == "status") printSystemStatus(); - else if (cmd == "scan" || cmd == "i2c") scanI2CDevices(); - else Serial.println("❌ Unknown command: " + cmd); - Serial.println("\n>>> TEST COMPLETE <<<\n"); -} - -void testDHT() { - Serial.println("Testing DHT11 (5 samples) with manual -30% humidity correction:"); - for (int i=0;i<5;i++) { - float t = dht.readTemperature(); - float h = dht.readHumidity(); - if (!isnan(t) && !isnan(h)) { - float corr = h - 30.0f; - if (corr < 0) corr = 0; if (corr > 100) corr = 100; - Serial.printf(" #%d: Temp=%.2f C, Hum(corrected)=%.2f %%\n", i+1, t, corr); - } else { - Serial.printf(" #%d: FAILED (NaN)\n", i+1); - } - delay(2000); - } -} - -void testMQ2() { - Serial.println("Testing MQ2 (10 samples):"); - for (int i=0;i<10;i++) { - int a = analogRead(MQ2_ANALOG_PIN); - bool d = digitalRead(MQ2_DIGITAL_PIN) == LOW; - Serial.printf(" #%d: analog=%d digital=%s\n", i+1, a, d?"ACTIVE":"INACTIVE"); - delay(500); - } - Serial.printf("Calibration baseline=%.1f threshold=%.1f\n", mq2_cal.baseline, mq2_cal.dangerThreshold); -} - -void testMQ9() { - Serial.println("Testing MQ9 (10 samples):"); - for (int i=0;i<10;i++) { - int a = analogRead(MQ9_ANALOG_PIN); - bool d = digitalRead(MQ9_DIGITAL_PIN) == LOW; - Serial.printf(" #%d: analog=%d digital=%s\n", i+1, a, d?"ACTIVE":"INACTIVE"); - delay(500); - } - Serial.printf("Calibration baseline=%.1f threshold=%.1f\n", mq9_cal.baseline, mq9_cal.dangerThreshold); -} - -void testMQ135() { - Serial.println("Testing MQ135 (10 samples):"); - for (int i=0;i<10;i++) { - int a = analogRead(MQ135_ANALOG_PIN); - bool d = digitalRead(MQ135_DIGITAL_PIN) == LOW; - Serial.printf(" #%d: analog=%d digital=%s rating=%s\n", i+1, a, d?"POOR":"GOOD", getAirQualityRating(a).c_str()); - delay(500); - } - Serial.printf("Calibration baseline=%.1f threshold=%.1f\n", mq135_cal.baseline, mq135_cal.dangerThreshold); -} - -void testAllMQ() { testMQ2(); testMQ9(); testMQ135(); } - -void testAudio(int fileNum) { - if (!audioReady) { Serial.println("Audio not ready"); return; } - Serial.printf("Playing audio file #%d\n", fileNum); - playAudioFile(fileNum); -} - -void testLoRa() { - if (!loraReady) { Serial.println("LoRa not ready"); return; } - Serial.println("Sending test LoRa packet..."); - SensorData d = readAllSensors(); - d.emergency = false; - d.motion = motionData; - sendLoRaData(d); - Serial.println("Test LoRa sent."); -} - -void testEmergency() { - Serial.println("Triggering emergency now..."); - emergencyTriggered = true; -} - -void testButton() { - Serial.println("Testing emergency button for 10s..."); - unsigned long start = millis(); - bool last = digitalRead(EMERGENCY_BUTTON_PIN); - while (millis() - start < 10000) { - bool cur = digitalRead(EMERGENCY_BUTTON_PIN); - if (cur != last) { - Serial.printf("Button state change: %s\n", cur ? "HIGH" : "LOW"); - last = cur; - } - delay(100); - } - Serial.println("Button test complete."); -} - -void testAllSensors() { - testDHT(); - testAllMQ(); - testLoRa(); -} - -// ========================= I2C Scanner ========================= -void scanI2CDevices() { - Serial.println("\n=== I2C Device Scanner ==="); - Serial.println("Scanning I2C bus (0x00 to 0x7F)..."); - - int devicesFound = 0; - for (byte address = 1; address < 127; address++) { - Wire.beginTransmission(address); - byte error = Wire.endTransmission(); - - if (error == 0) { - Serial.printf("✓ Device found at 0x%02X\n", address); - devicesFound++; - - if (address == 0x68 || address == 0x69) { - Serial.println(" → This is likely the MPU6050!"); - } - } - } - - if (devicesFound == 0) { - Serial.println("\n❌ NO I2C devices found!"); - Serial.println("\nTroubleshooting:"); - Serial.println("1. Check VCC is connected to 3.3V (NOT 5V)"); - Serial.println("2. Check GND is connected"); - Serial.println("3. Verify SDA → GPIO21, SCL → GPIO22"); - Serial.println("4. Check all connections are firm"); - Serial.println("5. Add 4.7kΩ pull-up resistors to SDA & SCL"); - Serial.println("6. Try a different MPU6050 module (may be faulty)"); - } else { - Serial.printf("\n✓ Total devices found: %d\n", devicesFound); - } - Serial.println("=========================\n"); -} - -// ========================= Audio control ========================= - -void sendCommand(byte cmd, byte param1, byte param2, bool feedback) { - byte packet[10]; - packet[0] = FRAME_START; - packet[1] = VERSION; - packet[2] = 0x06; - packet[3] = cmd; - packet[4] = feedback ? WITH_FEEDBACK : NO_FEEDBACK; - packet[5] = param1; - packet[6] = param2; - uint16_t sum = packet[1] + packet[2] + packet[3] + packet[4] + packet[5] + packet[6]; - uint16_t checksum = 0xFFFF - sum + 1; - packet[7] = (checksum >> 8) & 0xFF; - packet[8] = checksum & 0xFF; - packet[9] = FRAME_END; - fnM16pSerial.write(packet, 10); -} - -void setVolume(int volume) { - if (volume < 0) volume = 0; - if (volume > 30) volume = 30; - sendCommand(CMD_VOLUME, 0x00, volume, false); -} - -void playAudioFile(int fileNumber) { - if (fileNumber < 1 || fileNumber > 10) return; - sendCommand(CMD_PLAY_TRACK, 0x00, fileNumber, false); - delay(60); -} - -void stopAudio() { - sendCommand(CMD_STOP, 0x00, 0x00, false); -} - -// ========================= Calibration ========================= - -void calibrateSensors() { - Serial.println("Starting sensor calibration..."); - delay(500); - calibrateSensor(MQ2_ANALOG_PIN, &mq2_cal, "MQ2", MQ2_DANGER_THRESHOLD); - calibrateSensor(MQ9_ANALOG_PIN, &mq9_cal, "MQ9", MQ9_DANGER_THRESHOLD); - calibrateSensor(MQ135_ANALOG_PIN, &mq135_cal, "MQ135", MQ135_DANGER_THRESHOLD); - Serial.println("Calibration completed."); -} - -void calibrateSensor(int pin, SensorCalibration* cal, String sensorName, int minThreshold) { - Serial.print("Calibrating " + sensorName + " ..."); - float sum = 0; - for (int i=0;ibaseline = sum / CALIBRATION_SAMPLES; - float calculatedThreshold = cal->baseline * DANGER_MULTIPLIER; - cal->dangerThreshold = (calculatedThreshold > minThreshold) ? calculatedThreshold : minThreshold; - cal->calibrated = true; - Serial.println(" Done"); -} - -// ========================= Sensor danger check - FIXED FUNCTION ========================= -bool checkSensorDanger(int currentValue, SensorCalibration* cal, int staticDangerThreshold) { - if (!cal->calibrated) { - // If not calibrated, use static threshold - return currentValue >= staticDangerThreshold; - } - // Use calibrated dynamic threshold - return currentValue >= cal->dangerThreshold; -} - -// ========================= Sensors reading/display/alerts/LoRa ========================= - -SensorData readAllSensors() { - SensorData data; - data.timestamp = millis(); - - if (millis() - lastDHTReading > DHT_READING_INTERVAL) { - float rt = dht.readTemperature(); - float rh = dht.readHumidity(); - if (!isnan(rt) && rt >= -40 && rt <= 80) { data.temperature = rt; lastValidTemperature = rt; } - else data.temperature = lastValidTemperature; - - if (!isnan(rh) && rh >= 0 && rh <= 100) { - float corr = rh - 30.0f; - if (corr < 0.0f) corr = 0.0f; - if (corr > 100.0f) corr = 100.0f; - data.humidity = corr; - lastValidHumidity = corr; - } else data.humidity = lastValidHumidity; - - lastDHTReading = millis(); - } else { - data.temperature = lastValidTemperature; - data.humidity = lastValidHumidity; - } - - data.mq2_analog = analogRead(MQ2_ANALOG_PIN); - data.mq9_analog = analogRead(MQ9_ANALOG_PIN); - data.mq135_analog = analogRead(MQ135_ANALOG_PIN); - - data.mq2_digital = digitalRead(MQ2_DIGITAL_PIN) == LOW; - data.mq9_digital = digitalRead(MQ9_DIGITAL_PIN) == LOW; - data.mq135_digital = digitalRead(MQ135_DIGITAL_PIN) == LOW; - - data.emergency = false; - data.motion = motionData; - return data; -} - -void displayReadings(SensorData data) { - Serial.println("=== SENSOR READINGS ==="); - Serial.printf("Timestamp: %lu\n", data.timestamp); - Serial.println("DHT11:"); - Serial.printf(" Temperature: %.2f°C\n", data.temperature); - Serial.printf(" Humidity: %.2f%%\n", data.humidity); - Serial.println("MQ2 (Smoke/LPG/Gas):"); - Serial.printf(" Digital: %s | Analog: %d", data.mq2_digital ? "GAS DETECTED" : "No Gas", data.mq2_analog); - Serial.println(checkSensorDanger(data.mq2_analog, &mq2_cal, MQ2_DANGER_THRESHOLD) ? " [DANGER]" : " [Safe]"); - Serial.println("MQ9 (Carbon Monoxide):"); - Serial.printf(" Digital: %s | Analog: %d", data.mq9_digital ? " NO CO" : "CO DETECTED", data.mq9_analog); - Serial.println(checkSensorDanger(data.mq9_analog, &mq9_cal, MQ9_DANGER_THRESHOLD) ? " [DANGER]" : " [Safe]"); - Serial.println("MQ135 (Air Quality):"); - Serial.printf(" Digital: %s | Analog: %d", data.mq135_digital ? "POOR AIR" : "Good Air", data.mq135_analog); - Serial.println(checkSensorDanger(data.mq135_analog, &mq135_cal, MQ135_DANGER_THRESHOLD) ? " [DANGER]" : " [Safe]"); - Serial.println("MOTION:"); - Serial.printf(" Accel: %.2f g\n", data.motion.totalAccel / G); - Serial.printf(" Gyro: %.2f °/s\n", data.motion.totalGyro); - Serial.printf(" Fall Detected: %s\n", data.motion.fallDetected ? "YES" : "NO"); -} - -void checkAlerts(SensorData data) { - static unsigned long lastAlert = 0; - unsigned long now = millis(); - if (now - lastAlert < 60000) return; - - if (data.motion.fallDetected) { - if (audioReady) playAudioFile(FALL_DETECTED); - lastAlert = now; - return; - } - if (checkSensorDanger(data.mq2_analog, &mq2_cal, MQ2_DANGER_THRESHOLD)) { - playAudioFile(SMOKE_ALERT); lastAlert = now; return; - } - if (checkSensorDanger(data.mq9_analog, &mq9_cal, MQ9_DANGER_THRESHOLD)) { - playAudioFile(CO_ALERT); lastAlert = now; return; - } - if (checkSensorDanger(data.mq135_analog, &mq135_cal, MQ135_DANGER_THRESHOLD)) { - playAudioFile(AIR_QUALITY_WARNING); lastAlert = now; return; - } - if (data.temperature > 45.0) { playAudioFile(HIGH_TEMP_ALERT); lastAlert = now; return; } - if (data.temperature < 0.0) { playAudioFile(LOW_TEMP_ALERT); lastAlert = now; return; } - if (data.humidity > 90.0) { playAudioFile(HIGH_HUMIDITY_ALERT); lastAlert = now; return; } - if (data.humidity < 10.0) { playAudioFile(LOW_HUMIDITY_ALERT); lastAlert = now; return; } -} - -void sendLoRaData(SensorData data) { - packetCount++; - StaticJsonDocument<512> doc; - doc["nodeId"] = NODE_ID; - doc["packetCount"] = packetCount; - doc["timestamp"] = data.timestamp; - doc["temperature"] = data.temperature; - doc["humidity"] = data.humidity; - doc["mq2_analog"] = data.mq2_analog; - doc["mq9_analog"] = data.mq9_analog; - doc["mq135_analog"] = data.mq135_analog; - doc["mq2_digital"] = data.mq2_digital; - doc["mq9_digital"] = data.mq9_digital; - doc["mq135_digital"] = data.mq135_digital; - doc["air_quality"] = getAirQualityRating(data.mq135_analog); - doc["emergency"] = data.emergency; - // motion - doc["fall_detected"] = data.motion.fallDetected; - doc["activity"] = data.motion.fallDetected ? String("FALLEN - HELP!") : String("Stationary"); - doc["total_accel"] = data.motion.totalAccel; - doc["total_gyro"] = data.motion.totalGyro; - doc["motion_active"] = data.motion.motionDetected; - - String jsonString; - serializeJson(doc, jsonString); - - if (data.emergency || data.motion.fallDetected) { - Serial.println("\n╔════════ EMERGENCY LoRa ═════════╗"); - if (data.motion.fallDetected) Serial.println("║ FALL DETECTED! ║"); - Serial.println("╚════════════════════════════════╝"); - } - - Serial.print("LoRa Packet #"); Serial.print(packetCount); Serial.println(data.emergency ? " [EMERGENCY]" : " [NORMAL]"); - Serial.print("Payload size: "); Serial.print(jsonString.length()); Serial.println(" bytes"); - Serial.println("Payload: " + jsonString); - - LoRa.beginPacket(); - LoRa.print(jsonString); - LoRa.endPacket(); - - Serial.println("✓ Packet sent"); -} - -String getAirQualityRating(int value) { - if (value < 800) return "Excellent"; - else if (value < 1200) return "Good"; - else if (value < 1800) return "Moderate"; - else if (value < 2400) return "Poor"; - else return "Very Poor"; -} - -// ========================= Emergency Button ========================= -void checkEmergencyButton() { - static bool lastState = HIGH; // using INPUT_PULLUP -> HIGH when released - static unsigned long lastChangeTime = 0; - bool currentState = digitalRead(EMERGENCY_BUTTON_PIN); - if (currentState != lastState && (millis() - lastChangeTime > 50)) { - lastChangeTime = millis(); - if (currentState == LOW) { // pressed - unsigned long now = millis(); - if (now - lastTapTime < TAP_TIMEOUT) tapCount++; - else tapCount = 1; - lastTapTime = now; - Serial.printf("BUTTON TAP #%d/%d\n", tapCount, REQUIRED_TAPS); - if (tapCount >= REQUIRED_TAPS) { - Serial.println("TRIPLE TAP - EMERGENCY TRIGGERED"); - emergencyTriggered = true; - tapCount = 0; - } - } - } - if (millis() - lastTapTime > TAP_TIMEOUT && tapCount > 0) tapCount = 0; - lastState = currentState; -} - -void handleEmergency() { - Serial.println("\n████████ EMERGENCY MODE █████████"); - SensorData s = readAllSensors(); - s.emergency = true; - s.motion = motionData; - Serial.println("EMERGENCY SNAPSHOT:"); - Serial.printf(" Temp: %.2f C Hum: %.2f %%\n", s.temperature, s.humidity); - Serial.printf(" MQ2:%d MQ9:%d MQ135:%d\n", s.mq2_analog, s.mq9_analog, s.mq135_analog); - Serial.printf(" Fall: %s\n", s.motion.fallDetected ? "YES":"NO"); - if (loraReady) sendLoRaData(s); else Serial.println("LoRa not ready."); - if (audioReady) playAudioFile(FALL_DETECTED); - delay(800); -} - -// ========================= Status ========================= -void printSystemStatus() { - Serial.println("\n=== SYSTEM STATUS ==="); - Serial.printf("Node ID: %s\n", NODE_ID); - Serial.printf("Uptime: %lu s\n", millis()/1000); - Serial.printf("Packets sent: %d\n", packetCount); - Serial.printf("DHT: %s MPU: %s LoRa: %s Audio: %s\n", - dhtReady ? "YES":"NO", - mpuReady ? "YES":"NO", - loraReady ? "YES":"NO", - audioReady ? "YES":"NO"); - Serial.printf("MQ2 cal: %s MQ9 cal: %s MQ135 cal: %s\n", - mq2_cal.calibrated?"YES":"NO", - mq9_cal.calibrated?"YES":"NO", - mq135_cal.calibrated?"YES":"NO"); - Serial.printf("Last Temp: %.2f C Last Hum (corrected): %.2f %%\n", lastValidTemperature, lastValidHumidity); - Serial.println("=====================\n"); -} diff --git a/central_Node.cpp b/central_Node.cpp deleted file mode 100644 index 640819d..0000000 --- a/central_Node.cpp +++ /dev/null @@ -1,548 +0,0 @@ -/* - ESP32 LoRa Central Node - Supabase Gateway - - Receives LoRa packets from sensor nodes - - Adds NTP timestamps - - Uploads to Supabase PostgreSQL database - - WiFi connectivity with auto-reconnect - - Enhanced JSON packet processing -*/ - -#include -#include -#include -#include -#include -#include -#include - -// WiFi Credentials -#define WIFI_SSID "419" -#define WIFI_PASSWORD "xyz@1234" - -// Supabase Configuration - YOUR ACTUAL VALUES -#define SUPABASE_URL "https://kfwngukvlsjjhwslktbn.supabase.co" -#define SUPABASE_ANON_KEY "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imtmd25ndWt2bHNqamh3c2xrdGJuIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTgzNzYwMzksImV4cCI6MjA3Mzk1MjAzOX0.qY_JlPE6g5ewfBodJZYDS6ABFySvEMLgqOhCeQg8U8I" - -// NTP Configuration -const char* ntpServer = "pool.ntp.org"; -const long gmtOffset_sec = 19800; // GMT+5:30 India -const int daylightOffset_sec = 0; - -// LoRa Module pins -#define LORA_SCK 5 -#define LORA_MISO 19 -#define LORA_MOSI 27 -#define LORA_SS 18 -#define LORA_RST 14 -#define LORA_DIO0 2 - -// LoRa frequency -#define LORA_BAND 915E6 - -// Central node identification -#define CENTRAL_NODE_ID "CENTRAL_GATEWAY_001" - -// Status flags -bool wifiConnected = false; -bool supabaseReady = false; -bool ntpSynced = false; -bool loraReady = false; - -// Statistics -unsigned long packetsReceived = 0; -unsigned long packetsUploaded = 0; -unsigned long packetsCorrupted = 0; -unsigned long lastStatsDisplay = 0; -unsigned long lastWiFiCheck = 0; -unsigned long lastNTPSync = 0; - -// HTTP client for Supabase API calls -HTTPClient http; -WiFiClientSecure client; - -void setup() { - Serial.begin(115200); - delay(2000); - - Serial.println("====================================="); - Serial.println("ESP32 LoRa Central Node - Supabase Gateway"); - Serial.println("Real-time sensor data to PostgreSQL"); - Serial.println("====================================="); - - // Initialize LoRa first - initializeLoRa(); - - // Initialize WiFi - initializeWiFi(); - - // Initialize NTP - if (wifiConnected) { - initializeNTP(); - } - - // Initialize Supabase - if (wifiConnected) { - initializeSupabase(); - } - - Serial.println("Central Node ready to receive LoRa packets!"); - Serial.println("Listening for sensor data..."); - Serial.println(); -} - -void loop() { - // Check WiFi connection every 30 seconds - if (millis() - lastWiFiCheck > 30000) { - checkWiFiConnection(); - lastWiFiCheck = millis(); - } - - // Try NTP sync every 5 minutes if not synced - if (!ntpSynced && wifiConnected && (millis() - lastNTPSync > 300000)) { - initializeNTP(); - lastNTPSync = millis(); - } - - // Check for LoRa packets - if (loraReady) { - handleLoRaPackets(); - } - - // Display statistics every 60 seconds - if (millis() - lastStatsDisplay > 60000) { - displayStatistics(); - lastStatsDisplay = millis(); - } - - delay(50); -} - -void initializeLoRa() { - Serial.println("Initializing LoRa receiver..."); - - SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_SS); - LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0); - - if (!LoRa.begin(LORA_BAND)) { - Serial.println("LoRa initialization failed. Check wiring."); - loraReady = false; - return; - } - - // LoRa configuration - LoRa.setTxPower(20); - LoRa.setSpreadingFactor(12); - LoRa.setSignalBandwidth(125E3); - LoRa.setCodingRate4(8); - LoRa.setPreambleLength(8); - LoRa.setSyncWord(0x34); - - Serial.println("LoRa initialized successfully!"); - Serial.println("Frequency: " + String(LORA_BAND/1E6) + " MHz"); - Serial.println("Spreading Factor: 12"); - Serial.println("Sync Word: 0x34"); - loraReady = true; -} - -void initializeWiFi() { - Serial.print("Connecting to WiFi"); - WiFi.mode(WIFI_STA); - WiFi.begin(WIFI_SSID, WIFI_PASSWORD); - - int attempts = 0; - while (WiFi.status() != WL_CONNECTED && attempts < 30) { - delay(500); - Serial.print("."); - attempts++; - } - - if (WiFi.status() == WL_CONNECTED) { - wifiConnected = true; - Serial.println(); - Serial.println("WiFi connected successfully!"); - Serial.print("IP address: "); - Serial.println(WiFi.localIP()); - Serial.print("Signal strength: "); - Serial.print(WiFi.RSSI()); - Serial.println(" dBm"); - } else { - wifiConnected = false; - Serial.println(); - Serial.println("WiFi connection failed!"); - Serial.println("Operating in offline mode"); - } -} - -void checkWiFiConnection() { - if (WiFi.status() != WL_CONNECTED && wifiConnected) { - Serial.println("WiFi connection lost. Attempting to reconnect..."); - wifiConnected = false; - supabaseReady = false; - initializeWiFi(); - - if (wifiConnected && !supabaseReady) { - initializeSupabase(); - } - } -} - -void initializeNTP() { - if (!wifiConnected) return; - - Serial.println("Synchronizing time with NTP server..."); - - const char* ntpServers[] = { - "pool.ntp.org", - "time.nist.gov", - "in.pool.ntp.org", - "0.in.pool.ntp.org" - }; - - for (int server = 0; server < 4 && !ntpSynced; server++) { - Serial.print("Trying NTP server: "); - Serial.println(ntpServers[server]); - - configTime(gmtOffset_sec, daylightOffset_sec, ntpServers[server]); - - struct tm timeinfo; - for (int attempts = 0; attempts < 10 && !ntpSynced; attempts++) { - delay(1000); - Serial.print("."); - if (getLocalTime(&timeinfo)) { - ntpSynced = true; - Serial.println(); - Serial.println("NTP time synchronized successfully!"); - Serial.printf("Current time: %s", asctime(&timeinfo)); - return; - } - } - Serial.println(); - } - - if (!ntpSynced) { - Serial.println("Failed to sync with any NTP server! Will retry later..."); - } -} - -void initializeSupabase() { - if (!wifiConnected) return; - - Serial.println("Initializing Supabase connection..."); - - // Configure SSL client (Supabase uses HTTPS) - client.setInsecure(); // For development - in production, use proper certificates - - // Test Supabase connection - String testUrl = String(SUPABASE_URL) + "/rest/v1/"; - http.begin(client, testUrl); - http.addHeader("Content-Type", "application/json"); - http.addHeader("apikey", SUPABASE_ANON_KEY); - http.addHeader("Authorization", "Bearer " + String(SUPABASE_ANON_KEY)); - - int httpResponseCode = http.GET(); - - if (httpResponseCode > 0) { - Serial.println("Supabase connection successful!"); - Serial.println("Response code: " + String(httpResponseCode)); - supabaseReady = true; - - // Create initial status record - uploadGatewayStatus("online"); - - } else { - Serial.println("Supabase connection failed!"); - Serial.println("Error code: " + String(httpResponseCode)); - Serial.println("Check your SUPABASE_URL and SUPABASE_ANON_KEY"); - supabaseReady = false; - } - - http.end(); -} - -void uploadGatewayStatus(String status) { - if (!supabaseReady) return; - - DynamicJsonDocument statusDoc(512); - statusDoc["central_node_id"] = CENTRAL_NODE_ID; - statusDoc["status"] = status; - statusDoc["startup_time"] = getCurrentDateTime(); - statusDoc["ip_address"] = WiFi.localIP().toString(); - statusDoc["rssi"] = WiFi.RSSI(); - statusDoc["free_heap"] = ESP.getFreeHeap(); - - String jsonString; - serializeJson(statusDoc, jsonString); - - String url = String(SUPABASE_URL) + "/rest/v1/gateway_status"; - - http.begin(client, url); - http.addHeader("Content-Type", "application/json"); - http.addHeader("apikey", SUPABASE_ANON_KEY); - http.addHeader("Authorization", "Bearer " + String(SUPABASE_ANON_KEY)); - http.addHeader("Prefer", "return=minimal"); - - int httpResponseCode = http.POST(jsonString); - - if (httpResponseCode > 0) { - Serial.println("Gateway status uploaded to Supabase"); - } else { - Serial.println("Failed to upload gateway status: " + String(httpResponseCode)); - } - - http.end(); -} - -void handleLoRaPackets() { - int packetSize = LoRa.parsePacket(); - if (packetSize == 0) return; - - // Read packet - String receivedPacket = ""; - while (LoRa.available()) { - char c = (char)LoRa.read(); - if (c >= 20 && c <= 126) { - receivedPacket += c; - } - } - - if (receivedPacket.length() < 5) { - Serial.println("Packet too short - discarded"); - return; - } - - int rssi = LoRa.packetRssi(); - float snr = LoRa.packetSnr(); - - packetsReceived++; - - Serial.println("========================"); - Serial.println("LoRa Packet Received #" + String(packetsReceived)); - Serial.println("RSSI: " + String(rssi) + " dBm"); - Serial.println("SNR: " + String(snr) + " dB"); - Serial.println("Size: " + String(packetSize) + " bytes"); - Serial.println("Clean Size: " + String(receivedPacket.length()) + " bytes"); - Serial.println("Raw Data: " + receivedPacket); - - // Clean and process packet - String cleanedPacket = cleanJsonPacket(receivedPacket); - if (cleanedPacket.length() > 0) { - processAndUploadPacket(cleanedPacket, rssi, snr); - } else { - packetsCorrupted++; - Serial.println("Packet could not be cleaned"); - } - - Serial.println("========================"); -} - -String cleanJsonPacket(String rawPacket) { - // First, try the original packet as-is - DynamicJsonDocument testDoc(512); - if (deserializeJson(testDoc, rawPacket) == DeserializationError::Ok) { - return rawPacket; - } - - // Perform minimal cleaning - String cleaned = rawPacket; - - // Fix truncated packets - if (cleaned.endsWith(",p")) { - cleaned = cleaned.substring(0, cleaned.length() - 2) + "od\"}"; - } else if (cleaned.endsWith("Go,p")) { - cleaned = cleaned.substring(0, cleaned.length() - 4) + "Good\"}"; - } - - // Fix incomplete JSON - add missing closing brace - int openBraces = 0; - int closeBraces = 0; - for (char c : cleaned) { - if (c == '{') openBraces++; - if (c == '}') closeBraces++; - } - - while (closeBraces < openBraces) { - cleaned += "}"; - closeBraces++; - } - - // Fix incomplete strings - add missing quotes - int quotes = 0; - for (char c : cleaned) { - if (c == '"') quotes++; - } - - if (quotes % 2 != 0) { - cleaned += "\""; - } - - return cleaned; -} - -void processAndUploadPacket(String cleanedPacket, int rssi, float snr) { - DynamicJsonDocument receivedDoc(512); - DeserializationError error = deserializeJson(receivedDoc, cleanedPacket); - - if (error) { - Serial.println("JSON parsing failed: " + String(error.c_str())); - Serial.println("Cleaned packet was: " + cleanedPacket); - packetsCorrupted++; - return; - } - - Serial.println("JSON parsed successfully!"); - - // Create enhanced JSON for Supabase - DynamicJsonDocument enhancedDoc(1024); - - // Sensor data - enhancedDoc["sensor_node_id"] = receivedDoc["nodeId"] | "UNKNOWN"; - enhancedDoc["sensor_packet_count"] = receivedDoc["packetCount"] | 0; - enhancedDoc["sensor_timestamp"] = receivedDoc["timestamp"] | 0; - enhancedDoc["temperature"] = receivedDoc["temperature"] | -999; - enhancedDoc["humidity"] = receivedDoc["humidity"] | -999; - enhancedDoc["mq2_analog"] = receivedDoc["mq2_analog"] | 0; - enhancedDoc["mq9_analog"] = receivedDoc["mq9_analog"] | 0; - enhancedDoc["mq135_analog"] = receivedDoc["mq135_analog"] | 0; - enhancedDoc["mq2_digital"] = receivedDoc["mq2_digital"] | false; - enhancedDoc["mq9_digital"] = receivedDoc["mq9_digital"] | false; - enhancedDoc["mq135_digital"] = receivedDoc["mq135_digital"] | false; - enhancedDoc["air_quality"] = receivedDoc["air_quality"] | "Unknown"; - - // Gateway info - enhancedDoc["central_node_id"] = CENTRAL_NODE_ID; - enhancedDoc["received_time"] = getCurrentDateTime(); - enhancedDoc["received_timestamp"] = millis(); - enhancedDoc["rssi"] = rssi; - enhancedDoc["snr"] = snr; - enhancedDoc["gateway_packet_count"] = packetsReceived; - - String enhancedJsonString; - serializeJson(enhancedDoc, enhancedJsonString); - Serial.println("Enhanced JSON: " + enhancedJsonString); - - // Upload to Supabase - if (supabaseReady) { - uploadToSupabase(enhancedJsonString); - } else { - Serial.println("Supabase not available - logged locally only"); - } -} - -void uploadToSupabase(String jsonData) { - if (!supabaseReady) { - Serial.println("Supabase not ready"); - return; - } - - Serial.println("Uploading to Supabase..."); - - String url = String(SUPABASE_URL) + "/rest/v1/sensor_data"; - - http.begin(client, url); - http.addHeader("Content-Type", "application/json"); - http.addHeader("apikey", SUPABASE_ANON_KEY); - http.addHeader("Authorization", "Bearer " + String(SUPABASE_ANON_KEY)); - http.addHeader("Prefer", "return=minimal"); - - int httpResponseCode = http.POST(jsonData); - - if (httpResponseCode == 201 || httpResponseCode == 200) { - packetsUploaded++; - Serial.println("SUCCESS: Data uploaded to Supabase!"); - Serial.println("HTTP Code: " + String(httpResponseCode)); - - // Also update latest reading table - updateLatestReading(jsonData); - - } else { - Serial.println("ERROR: Supabase upload failed"); - Serial.println("HTTP Code: " + String(httpResponseCode)); - String response = http.getString(); - Serial.println("Response: " + response); - - if (httpResponseCode == 401) { - Serial.println("AUTHENTICATION ERROR - Check your API key"); - } else if (httpResponseCode == 403) { - Serial.println("PERMISSION ERROR - Check RLS policies"); - } else if (httpResponseCode == 422) { - Serial.println("VALIDATION ERROR - Check table schema"); - } - } - - http.end(); -} - -void updateLatestReading(String jsonData) { - String url = String(SUPABASE_URL) + "/rest/v1/latest_sensor_data?central_node_id=eq." + String(CENTRAL_NODE_ID); - - http.begin(client, url); - http.addHeader("Content-Type", "application/json"); - http.addHeader("apikey", SUPABASE_ANON_KEY); - http.addHeader("Authorization", "Bearer " + String(SUPABASE_ANON_KEY)); - http.addHeader("Prefer", "return=minimal"); - - // First try to update existing record - int httpResponseCode = http.PATCH(jsonData); - - if (httpResponseCode == 200 || httpResponseCode == 204) { - Serial.println("Latest reading updated!"); - } else { - // If update fails, try insert - http.end(); - http.begin(client, String(SUPABASE_URL) + "/rest/v1/latest_sensor_data"); - http.addHeader("Content-Type", "application/json"); - http.addHeader("apikey", SUPABASE_ANON_KEY); - http.addHeader("Authorization", "Bearer " + String(SUPABASE_ANON_KEY)); - http.addHeader("Prefer", "return=minimal"); - - httpResponseCode = http.POST(jsonData); - if (httpResponseCode == 201) { - Serial.println("Latest reading inserted!"); - } - } - - http.end(); -} - -String getCurrentDateTime() { - if (!ntpSynced) { - return "Uptime: " + String(millis() / 1000) + "s"; - } - - struct tm timeinfo; - if (!getLocalTime(&timeinfo)) { - return "Failed to get time"; - } - - char timeString[64]; - strftime(timeString, sizeof(timeString), "%Y-%m-%d %H:%M:%S", &timeinfo); - return String(timeString); -} - -void displayStatistics() { - Serial.println(); - Serial.println("=== CENTRAL NODE STATISTICS ==="); - Serial.println("Uptime: " + String(millis() / 1000) + " seconds"); - Serial.println("WiFi: " + String(wifiConnected ? "Connected" : "Disconnected")); - if (wifiConnected) { - Serial.println("Signal: " + String(WiFi.RSSI()) + " dBm"); - } - Serial.println("Supabase: " + String(supabaseReady ? "Ready" : "Not Ready")); - Serial.println("NTP: " + String(ntpSynced ? "Synced" : "Not Synced")); - Serial.println("LoRa: " + String(loraReady ? "Ready" : "Not Ready")); - Serial.println("Packets Received: " + String(packetsReceived)); - Serial.println("Packets Corrupted: " + String(packetsCorrupted)); - Serial.println("Packets Uploaded: " + String(packetsUploaded)); - - if (packetsReceived > 0) { - float successRate = (float)packetsUploaded / packetsReceived * 100; - float corruptionRate = (float)packetsCorrupted / packetsReceived * 100; - Serial.println("Upload Success Rate: " + String(successRate, 1) + "%"); - Serial.println("Corruption Rate: " + String(corruptionRate, 1) + "%"); - } - - Serial.println("Current Time: " + getCurrentDateTime()); - Serial.println("Free Heap: " + String(ESP.getFreeHeap()) + " bytes"); - Serial.println("================================"); - Serial.println(); -} diff --git a/combined_test_1.cpp b/combined_test_1.cpp deleted file mode 100644 index 693baa3..0000000 --- a/combined_test_1.cpp +++ /dev/null @@ -1,546 +0,0 @@ -/* - ESP32 Multi-Sensor System - Local Operation Only - - MQ2, MQ9, MQ135 Gas Sensors - FIXED FOR HIGH THRESHOLD ONLY - - HTU21D Temperature & Humidity - - FN-M16P Audio Module with PAM8403 Amplifier - - LoRa Communication with Real Sensor Data - - Serial Output Only (No WiFi/Firebase/NTP) -*/ - -#include -#include -#include -#include -#include - -// HTU21D Library (install "Adafruit HTU21DF Library" from Library Manager) -#include - -// Pin definitions for MQ Sensors (NO CONFLICTS!) -#define MQ2_DIGITAL_PIN 12 // GPIO12 - Available -#define MQ2_ANALOG_PIN 32 // GPIO32 - ADC1_CH4 -#define MQ9_DIGITAL_PIN 13 // GPIO13 - Available -#define MQ9_ANALOG_PIN 33 // GPIO33 - ADC1_CH5 -#define MQ135_DIGITAL_PIN 15 // GPIO15 - Available -#define MQ135_ANALOG_PIN 35 // GPIO35 - ADC1_CH7 - -// FN-M16P Audio Module pins (SIMPLIFIED - Only UART) -#define FN_M16P_RX 16 // ESP32 GPIO16 → FN-M16P TX (pin 3) -#define FN_M16P_TX 17 // ESP32 GPIO17 → FN-M16P RX (pin 2) -// BUSY, I/O1, I/O2 not used as per your requirement - -// HTU21D I2C pins (NO CONFLICTS!) -#define HTU21D_SDA 21 // GPIO21 - Available (was conflicting with BUSY) -#define HTU21D_SCL 22 // GPIO22 - Available (was conflicting with MQ2) - -// LoRa Module pins (BOOT-SAFE VERSION - GPIO 34) -#define LORA_SCK 5 // GPIO5 -- SX1278's SCK -#define LORA_MISO 19 // GPIO19 -- SX1278's MISO -#define LORA_MOSI 27 // GPIO27 -- SX1278's MOSI -#define LORA_SS 18 // GPIO18 -- SX1278's CS (Your working config) -#define LORA_RST 14 // GPIO14 -- SX1278's RESET (Your working config) -#define LORA_DIO0 34 // GPIO34 -- SX1278's IRQ (INPUT-ONLY, PERFECT FOR INTERRUPT) - -// LoRa frequency -#define LORA_BAND 915E6 // Use 868E6 for Europe, 915E6 for North America - -// Node identification -#define NODE_ID "SENSOR_NODE_001" - -// FIXED: Much higher static thresholds for genuine danger only -#define MQ2_DANGER_THRESHOLD 1600 // Very high smoke/gas threshold -#define MQ9_DANGER_THRESHOLD 3800 // Very high CO threshold -#define MQ135_DANGER_THRESHOLD 1800 // Very high air quality threshold - -// HTU21D fallback values -#define FALLBACK_TEMPERATURE 27.0 // 27°C -#define FALLBACK_HUMIDITY 47.0 // 47% RH - -// Calibration parameters - FIXED -#define CALIBRATION_SAMPLES 10 // More samples for better baseline -#define DANGER_MULTIPLIER 2.0 // Must be 2x baseline to trigger (200% increase!) -#define CALIBRATION_DELAY 2000 // 2 seconds between samples - -// FN-M16P Command Structure -const byte FRAME_START = 0x7E; -const byte FRAME_END = 0xEF; -const byte VERSION = 0xFF; -const byte NO_FEEDBACK = 0x00; -const byte WITH_FEEDBACK = 0x01; - -// FN-M16P Commands -const byte CMD_PLAY_TRACK = 0x03; -const byte CMD_VOLUME = 0x06; -const byte CMD_STOP = 0x16; - -// Audio file mapping -enum AudioFiles { - BOOT_AUDIO = 1, // 0001.mp3 - System startup - SMOKE_ALERT = 2, // 0002.mp3 - Smoke/Gas alert - CO_ALERT = 3, // 0003.mp3 - Carbon monoxide alert - AIR_QUALITY_WARNING = 4, // 0004.mp3 - Air quality warning - HIGH_TEMP_ALERT = 5, // 0005.mp3 - High temperature - LOW_TEMP_ALERT = 6, // 0006.mp3 - Low temperature - HIGH_HUMIDITY_ALERT = 7, // 0007.mp3 - High humidity - LOW_HUMIDITY_ALERT = 8 // 0008.mp3 - Low humidity -}; - -// FIXED: New calibration structure - only care about high threshold -struct SensorCalibration { - float baseline; - float dangerThreshold; // Only trigger when MUCH higher than baseline - bool calibrated; -}; - -SensorCalibration mq2_cal = {0, 0, false}; -SensorCalibration mq9_cal = {0, 0, false}; -SensorCalibration mq135_cal = {0, 0, false}; - -// Initialize modules -Adafruit_HTU21DF htu = Adafruit_HTU21DF(); -HardwareSerial fnM16pSerial(2); // Use UART2 - -// Status flags -bool audioReady = false; -bool loraReady = false; - -// LoRa transmission variables -unsigned long lastLoRaSend = 0; -int loraInterval = 30000; // Send every 30 seconds -int packetCount = 0; - -// Data structure for sensor readings -struct SensorData { - float temperature; - float humidity; - int mq2_analog; - int mq9_analog; - int mq135_analog; - bool mq2_digital; - bool mq9_digital; - bool mq135_digital; - unsigned long timestamp; -}; - -void setup() { - Serial.begin(115200); - Serial.println("================================="); - Serial.println("ESP32 Multi-Sensor System v5.2"); - Serial.println("FIXED: High Gas Threshold Only"); - Serial.println("================================="); - - // Initialize MQ sensor pins - pinMode(MQ2_DIGITAL_PIN, INPUT); - pinMode(MQ9_DIGITAL_PIN, INPUT); - pinMode(MQ135_DIGITAL_PIN, INPUT); - - // Initialize I2C for HTU21D - Wire.begin(HTU21D_SDA, HTU21D_SCL); - if (!htu.begin()) { - Serial.println("HTU21D not detected. Check wiring."); - } else { - Serial.println("HTU21D initialized successfully!"); - } - - // Initialize FN-M16P Audio Module - fnM16pSerial.begin(9600, SERIAL_8N1, FN_M16P_RX, FN_M16P_TX); - delay(2000); // Give FN-M16P time to initialize - - Serial.println("Initializing FN-M16P Audio Module..."); - setVolume(30); // Set volume (0-30) - delay(500); - - audioReady = true; - Serial.println("FN-M16P initialized successfully!"); - - // Initialize LoRa - SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_SS); - LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0); - - if (!LoRa.begin(LORA_BAND)) { - Serial.println("LoRa initialization failed. Check wiring."); - loraReady = false; - } else { - // Set LoRa parameters for maximum reliability - LoRa.setTxPower(20); - LoRa.setSpreadingFactor(12); - LoRa.setSignalBandwidth(125E3); - LoRa.setCodingRate4(8); - LoRa.setPreambleLength(8); - LoRa.setSyncWord(0x34); - - Serial.println("LoRa initialized successfully!"); - loraReady = true; - } - - Serial.println("Warming up gas sensors..."); - delay(15000); // 15 second warm-up for better stability - - // Calibrate MQ sensors after warm-up - Serial.println("Calibrating MQ sensors for DANGER-ONLY thresholds..."); - calibrateSensors(); - - Serial.println("System ready!"); - Serial.println(); - - // Play startup sound - if (audioReady) { - playAudioFile(BOOT_AUDIO); - delay(2000); - } -} - -void loop() { - // Read all sensors - SensorData data = readAllSensors(); - - // Display readings - displayReadings(data); - - // Check for alerts - checkAlerts(data); - - // Send LoRa data (every 30 seconds) - if (loraReady && (millis() - lastLoRaSend > loraInterval)) { - sendLoRaData(data); - lastLoRaSend = millis(); - } - - Serial.println("------------------------"); - delay(10000); // Main loop every 10 seconds -} - -// FN-M16P Functions -void sendCommand(byte cmd, byte param1, byte param2, bool feedback) { - byte packet[10]; - - packet[0] = FRAME_START; - packet[1] = VERSION; - packet[2] = 0x06; - packet[3] = cmd; - packet[4] = feedback ? WITH_FEEDBACK : NO_FEEDBACK; - packet[5] = param1; - packet[6] = param2; - - uint16_t sum = packet[1] + packet[2] + packet[3] + packet[4] + packet[5] + packet[6]; - uint16_t checksum = 0xFFFF - sum + 1; - - packet[7] = (checksum >> 8) & 0xFF; - packet[8] = checksum & 0xFF; - packet[9] = FRAME_END; - - fnM16pSerial.write(packet, 10); -} - -void setVolume(int volume) { - if (volume < 0) volume = 0; - if (volume > 30) volume = 30; - sendCommand(CMD_VOLUME, 0x00, volume, false); - Serial.println("FN-M16P: Volume set to " + String(volume)); -} - -void playAudioFile(int fileNumber) { - if (fileNumber < 1 || fileNumber > 8) return; - - Serial.println("*** AUDIO TRIGGERED at " + String(millis()) + "ms ***"); - - String fileName = ""; - if(fileNumber < 10) fileName = "000" + String(fileNumber); - else fileName = "00" + String(fileNumber); - - Serial.println("Playing: " + getAudioDescription(fileNumber) + " (" + fileName + ".mp3)"); - sendCommand(CMD_PLAY_TRACK, 0x00, fileNumber, false); - delay(100); -} - -String getAudioDescription(int fileNumber) { - switch(fileNumber) { - case 1: return "System Boot"; - case 2: return "Smoke/Gas Alert"; - case 3: return "CO Alert"; - case 4: return "Air Quality Warning"; - case 5: return "High Temperature"; - case 6: return "Low Temperature"; - case 7: return "High Humidity"; - case 8: return "Low Humidity"; - default: return "Unknown"; - } -} - -void stopAudio() { - sendCommand(CMD_STOP, 0x00, 0x00, false); -} - -// FIXED: New calibration function - establishes DANGER thresholds only -void calibrateSensors() { - Serial.println("Starting sensor calibration for DANGER thresholds..."); - Serial.println("Ensure sensors are in clean air environment"); - delay(3000); - - // Calibrate MQ2 - Serial.print("Calibrating MQ2... "); - calibrateSensor(MQ2_ANALOG_PIN, &mq2_cal, "MQ2"); - - // Calibrate MQ9 - Serial.print("Calibrating MQ9... "); - calibrateSensor(MQ9_ANALOG_PIN, &mq9_cal, "MQ9"); - - // Calibrate MQ135 - Serial.print("Calibrating MQ135... "); - calibrateSensor(MQ135_ANALOG_PIN, &mq135_cal, "MQ135"); - - Serial.println("Sensor calibration completed!"); - Serial.println("DANGER-ONLY Baselines established:"); - Serial.printf(" MQ2: %.1f (DANGER threshold: %.1f - must be 4x baseline!)\n", mq2_cal.baseline, mq2_cal.dangerThreshold); - Serial.printf(" MQ9: %.1f (DANGER threshold: %.1f - must be 4x baseline!)\n", mq9_cal.baseline, mq9_cal.dangerThreshold); - Serial.printf(" MQ135: %.1f (DANGER threshold: %.1f - must be 4x baseline!)\n", mq135_cal.baseline, mq135_cal.dangerThreshold); - Serial.println(); - Serial.println("Audio alerts will ONLY trigger at extreme gas levels!"); -} - -// FIXED: New calibration function - only high thresholds -void calibrateSensor(int pin, SensorCalibration* cal, String sensorName) { - float sum = 0; - - for (int i = 0; i < CALIBRATION_SAMPLES; i++) { - int reading = analogRead(pin); - sum += reading; - Serial.print("."); - delay(CALIBRATION_DELAY); - } - - cal->baseline = sum / CALIBRATION_SAMPLES; - - // FIXED: Only create danger threshold (must be 4x baseline) - cal->dangerThreshold = cal->baseline * DANGER_MULTIPLIER; - - // Also ensure minimum static threshold for safety - if (sensorName == "MQ2" && cal->dangerThreshold < MQ2_DANGER_THRESHOLD) { - cal->dangerThreshold = MQ2_DANGER_THRESHOLD; - } - if (sensorName == "MQ9" && cal->dangerThreshold < MQ9_DANGER_THRESHOLD) { - cal->dangerThreshold = MQ9_DANGER_THRESHOLD; - } - if (sensorName == "MQ135" && cal->dangerThreshold < MQ135_DANGER_THRESHOLD) { - cal->dangerThreshold = MQ135_DANGER_THRESHOLD; - } - - cal->calibrated = true; - Serial.println(" Done"); -} - -// FIXED: Only trigger alerts for VERY HIGH readings (danger level) -bool checkSensorDanger(int currentValue, SensorCalibration* cal, int staticDangerThreshold) { - if (!cal->calibrated) { - // Fall back to static danger threshold if not calibrated - return currentValue > staticDangerThreshold; - } - - // ONLY trigger if current value exceeds danger threshold (4x baseline OR static minimum) - return currentValue > cal->dangerThreshold; -} - -void logSensorStatus(String sensorName, int currentValue, SensorCalibration* cal, bool isDanger) { - if (cal->calibrated) { - float ratio = currentValue / cal->baseline; - Serial.printf(" %s: %d (baseline: %.1f, ratio: %.1fx, danger: %.1f) - %s\n", - sensorName.c_str(), currentValue, cal->baseline, ratio, cal->dangerThreshold, - isDanger ? "DANGER!" : "Safe"); - } else { - Serial.printf(" %s: %d (uncalibrated) - %s\n", - sensorName.c_str(), currentValue, isDanger ? "DANGER!" : "Safe"); - } -} - -// Sensor reading functions with HTU21D fallback -SensorData readAllSensors() { - SensorData data; - data.timestamp = millis(); - - // Read HTU21D with fallback - float temp = htu.readTemperature(); - float hum = htu.readHumidity(); - - // Check for HTU21D sensor errors and use fallback values - if (isnan(temp) || temp < -50 || temp > 100) { - data.temperature = FALLBACK_TEMPERATURE; - Serial.println("HTU21D temperature error - using fallback: " + String(FALLBACK_TEMPERATURE) + "°C"); - } else { - data.temperature = temp; - } - - if (isnan(hum) || hum < 0 || hum > 100) { - data.humidity = FALLBACK_HUMIDITY; - Serial.println("HTU21D humidity error - using fallback: " + String(FALLBACK_HUMIDITY) + "%"); - } else { - data.humidity = hum; - } - - // Read MQ sensors - data.mq2_analog = analogRead(MQ2_ANALOG_PIN); - data.mq9_analog = analogRead(MQ9_ANALOG_PIN); - data.mq135_analog = analogRead(MQ135_ANALOG_PIN); - - data.mq2_digital = digitalRead(MQ2_DIGITAL_PIN) == LOW; - data.mq9_digital = digitalRead(MQ9_DIGITAL_PIN) == LOW; - data.mq135_digital = digitalRead(MQ135_DIGITAL_PIN) == LOW; - - return data; -} - -void displayReadings(SensorData data) { - Serial.println("=== SENSOR READINGS ==="); - Serial.println("Timestamp: " + String(data.timestamp)); - Serial.println("Audio: " + String(audioReady ? "Ready" : "Not Ready")); - Serial.println("LoRa: " + String(loraReady ? "Ready" : "Not Ready")); - Serial.println(); - - // Environmental data - Serial.println("HTU21D (Temperature & Humidity):"); - Serial.printf(" Temperature: %.2f°C\n", data.temperature); - Serial.printf(" Humidity: %.2f%%\n", data.humidity); - Serial.println(); - - // FIXED: Gas sensors with DANGER-only status display - Serial.println("MQ2 (Smoke/LPG/Gas) - DANGER-ONLY Alerts:"); - Serial.printf(" Digital: %s | Analog: %d", - data.mq2_digital ? "GAS DETECTED" : "No Gas", data.mq2_analog); - bool mq2_danger = checkSensorDanger(data.mq2_analog, &mq2_cal, MQ2_DANGER_THRESHOLD); - Serial.println(mq2_danger ? " - EXTREME DANGER!" : " - Safe"); - logSensorStatus("MQ2", data.mq2_analog, &mq2_cal, mq2_danger); - - Serial.println("MQ9 (Carbon Monoxide) - DANGER-ONLY Alerts:"); - Serial.printf(" Digital: %s | Analog: %d", - data.mq9_digital ? "CO DETECTED" : "No CO", data.mq9_analog); - bool mq9_danger = checkSensorDanger(data.mq9_analog, &mq9_cal, MQ9_DANGER_THRESHOLD); - Serial.println(mq9_danger ? " - EXTREME DANGER!" : " - Safe"); - logSensorStatus("MQ9", data.mq9_analog, &mq9_cal, mq9_danger); - - Serial.println("MQ135 (Air Quality/CO2) - DANGER-ONLY Alerts:"); - Serial.printf(" Digital: %s | Analog: %d", - data.mq135_digital ? "POOR AIR" : "Good Air", data.mq135_analog); - bool mq135_danger = checkSensorDanger(data.mq135_analog, &mq135_cal, MQ135_DANGER_THRESHOLD); - Serial.println(mq135_danger ? " - EXTREME DANGER!" : " - Safe"); - logSensorStatus("MQ135", data.mq135_analog, &mq135_cal, mq135_danger); -} - -// FIXED: Only trigger audio for EXTREME danger levels -void checkAlerts(SensorData data) { - static unsigned long lastAlert = 0; - unsigned long now = millis(); - - Serial.printf("checkAlerts() called at %lu ms (lastAlert: %lu ms)\n", now, lastAlert); - - // Avoid too frequent alerts (minimum 60 seconds between alerts for extreme conditions) - if (now - lastAlert < 60000) { - Serial.println("*** ALERT COOLDOWN ACTIVE - No alerts will play ***"); - return; - } - - // FIXED: Check for EXTREME DANGER conditions only - bool mq2_danger = checkSensorDanger(data.mq2_analog, &mq2_cal, MQ2_DANGER_THRESHOLD); - bool mq9_danger = checkSensorDanger(data.mq9_analog, &mq9_cal, MQ9_DANGER_THRESHOLD); - bool mq135_danger = checkSensorDanger(data.mq135_analog, &mq135_cal, MQ135_DANGER_THRESHOLD); - - // DEBUG: Print danger status - Serial.println("DEBUG Danger Status (4x baseline or static minimum):"); - Serial.printf(" MQ2 EXTREME DANGER: %s (threshold: %.1f)\n", mq2_danger ? "TRUE" : "FALSE", mq2_cal.dangerThreshold); - Serial.printf(" MQ9 EXTREME DANGER: %s (threshold: %.1f)\n", mq9_danger ? "TRUE" : "FALSE", mq9_cal.dangerThreshold); - Serial.printf(" MQ135 EXTREME DANGER: %s (threshold: %.1f)\n", mq135_danger ? "TRUE" : "FALSE", mq135_cal.dangerThreshold); - - // ONLY trigger audio for EXTREME readings - if (mq2_danger) { - Serial.println("*** EXTREME MQ2 DANGER - PLAYING SMOKE ALERT ***"); - Serial.println("EXTREME DANGER: Very high smoke/gas levels detected!"); - playAudioFile(SMOKE_ALERT); - lastAlert = now; - } - else if (mq9_danger) { - Serial.println("*** EXTREME MQ9 DANGER - PLAYING CO ALERT ***"); - Serial.println("EXTREME DANGER: Very high carbon monoxide levels detected!"); - playAudioFile(CO_ALERT); - lastAlert = now; - } - else if (mq135_danger) { - Serial.println("*** EXTREME MQ135 DANGER - PLAYING AIR QUALITY ALERT ***"); - Serial.println("EXTREME DANGER: Very poor air quality detected!"); - playAudioFile(AIR_QUALITY_WARNING); - lastAlert = now; - } - else { - Serial.println("*** NO EXTREME GAS DANGERS - CHECKING TEMPERATURE/HUMIDITY ***"); - } - - // Temperature alerts (slightly more conservative) - if (data.temperature > 45.0) { - Serial.println("*** EXTREME HIGH TEMPERATURE ALERT TRIGGERED ***"); - Serial.println("DANGER: Extreme high temperature!"); - playAudioFile(HIGH_TEMP_ALERT); - lastAlert = now; - } - else if (data.temperature < 0.0) { - Serial.println("*** EXTREME LOW TEMPERATURE ALERT TRIGGERED ***"); - Serial.println("DANGER: Freezing temperature!"); - playAudioFile(LOW_TEMP_ALERT); - lastAlert = now; - } - - // Humidity alerts (more conservative) - else if (data.humidity > 90.0) { - Serial.println("*** EXTREME HIGH HUMIDITY ALERT TRIGGERED ***"); - Serial.println("DANGER: Extreme high humidity!"); - playAudioFile(HIGH_HUMIDITY_ALERT); - lastAlert = now; - } - else if (data.humidity < 10.0) { - Serial.println("*** EXTREME LOW HUMIDITY ALERT TRIGGERED ***"); - Serial.println("DANGER: Extreme low humidity!"); - playAudioFile(LOW_HUMIDITY_ALERT); - lastAlert = now; - } - else { - Serial.println("*** NO EXTREME CONDITIONS - ALL VALUES SAFE ***"); - } -} - -void sendLoRaData(SensorData data) { - packetCount++; - - // Create JSON packet with real sensor data - StaticJsonDocument<280> doc; - doc["nodeId"] = "001"; - doc["packetCount"] = packetCount; - - // Real sensor data - doc["temperature"] = data.temperature; - doc["humidity"] = data.humidity; - doc["mq2_analog"] = data.mq2_analog; - doc["mq9_analog"] = data.mq9_analog; - doc["mq135_analog"] = data.mq135_analog; - doc["mq2_digital"] = data.mq2_digital; - doc["mq9_digital"] = data.mq9_digital; - doc["mq135_digital"] = data.mq135_digital; - doc["air_quality"] = getAirQualityRating(data.mq135_analog); - - String jsonString; - serializeJson(doc, jsonString); - - Serial.print("Sending LoRa packet #"); - Serial.print(packetCount); - Serial.print(" from Node 001: "); - Serial.println(jsonString); - - LoRa.beginPacket(); - LoRa.print(jsonString); - LoRa.endPacket(); - - Serial.println("LoRa packet sent!"); -} - -String getAirQualityRating(int value) { - if (value < 800) return "Excellent"; - else if (value < 1200) return "Good"; - else if (value < 1800) return "Moderate"; - else if (value < 2400) return "Poor"; - else return "Very Poor"; -} diff --git a/dht11.cpp b/dht11.cpp deleted file mode 100644 index 860e160..0000000 --- a/dht11.cpp +++ /dev/null @@ -1,585 +0,0 @@ -/* - ESP32 Multi-Sensor System - Local Operation Only - - MQ2, MQ9, MQ135 Gas Sensors - FIXED FOR HIGH THRESHOLD ONLY - - DHT11 Temperature & Humidity (replacing HTU21D) - - FN-M16P Audio Module with PAM8403 Amplifier - - LoRa Communication with Real Sensor Data - - Serial Output Only (No WiFi/Firebase/NTP) -*/ - -#include -#include -#include -#include -#include - -// DHT11 Library (install "DHT sensor library" by Adafruit from Library Manager) -#include - -// Pin definitions for MQ Sensors (NO CONFLICTS!) -#define MQ2_DIGITAL_PIN 12 // GPIO12 - Available -#define MQ2_ANALOG_PIN 32 // GPIO32 - ADC1_CH4 -#define MQ9_DIGITAL_PIN 13 // GPIO13 - Available -#define MQ9_ANALOG_PIN 33 // GPIO33 - ADC1_CH5 -#define MQ135_DIGITAL_PIN 15 // GPIO15 - Available -#define MQ135_ANALOG_PIN 35 // GPIO35 - ADC1_CH7 - -// FN-M16P Audio Module pins (SIMPLIFIED - Only UART) -#define FN_M16P_RX 16 // ESP32 GPIO16 → FN-M16P TX (pin 3) -#define FN_M16P_TX 17 // ESP32 GPIO17 → FN-M16P RX (pin 2) -// BUSY, I/O1, I/O2 not used as per your requirement - -// DHT11 pin (Much simpler - only needs one pin!) -#define DHT11_PIN 21 // GPIO21 - Available (was HTU21D_SDA) -#define DHT_TYPE DHT11 // DHT11 sensor type - -// LoRa Module pins (BOOT-SAFE VERSION - GPIO 34) -#define LORA_SCK 5 // GPIO5 -- SX1278's SCK -#define LORA_MISO 19 // GPIO19 -- SX1278's MISO -#define LORA_MOSI 27 // GPIO27 -- SX1278's MOSI -#define LORA_SS 18 // GPIO18 -- SX1278's CS (Your working config) -#define LORA_RST 14 // GPIO14 -- SX1278's RESET (Your working config) -#define LORA_DIO0 34 // GPIO34 -- SX1278's IRQ (INPUT-ONLY, PERFECT FOR INTERRUPT) - -// LoRa frequency -#define LORA_BAND 915E6 // Use 868E6 for Europe, 915E6 for North America - -// Node identification -#define NODE_ID "SENSOR_NODE_001" - -// FIXED: Much higher static thresholds for genuine danger only -#define MQ2_DANGER_THRESHOLD 1600 // Very high smoke/gas threshold -#define MQ9_DANGER_THRESHOLD 3800 // Very high CO threshold -#define MQ135_DANGER_THRESHOLD 1800 // Very high air quality threshold - -// DHT11 fallback values -#define FALLBACK_TEMPERATURE 27.0 // 27°C -#define FALLBACK_HUMIDITY 47.0 // 47% RH - -// Calibration parameters - FIXED -#define CALIBRATION_SAMPLES 10 // More samples for better baseline -#define DANGER_MULTIPLIER 2.0 // Must be 2x baseline to trigger (200% increase!) -#define CALIBRATION_DELAY 2000 // 2 seconds between samples - -// FN-M16P Command Structure -const byte FRAME_START = 0x7E; -const byte FRAME_END = 0xEF; -const byte VERSION = 0xFF; -const byte NO_FEEDBACK = 0x00; -const byte WITH_FEEDBACK = 0x01; - -// FN-M16P Commands -const byte CMD_PLAY_TRACK = 0x03; -const byte CMD_VOLUME = 0x06; -const byte CMD_STOP = 0x16; - -// Audio file mapping -enum AudioFiles { - BOOT_AUDIO = 1, // 0001.mp3 - System startup - SMOKE_ALERT = 2, // 0002.mp3 - Smoke/Gas alert - CO_ALERT = 3, // 0003.mp3 - Carbon monoxide alert - AIR_QUALITY_WARNING = 4, // 0004.mp3 - Air quality warning - HIGH_TEMP_ALERT = 5, // 0005.mp3 - High temperature - LOW_TEMP_ALERT = 6, // 0006.mp3 - Low temperature - HIGH_HUMIDITY_ALERT = 7, // 0007.mp3 - High humidity - LOW_HUMIDITY_ALERT = 8 // 0008.mp3 - Low humidity -}; - -// FIXED: New calibration structure - only care about high threshold -struct SensorCalibration { - float baseline; - float dangerThreshold; // Only trigger when MUCH higher than baseline - bool calibrated; -}; - -SensorCalibration mq2_cal = {0, 0, false}; -SensorCalibration mq9_cal = {0, 0, false}; -SensorCalibration mq135_cal = {0, 0, false}; - -// Initialize modules -DHT dht(DHT11_PIN, DHT_TYPE); // Initialize DHT11 -HardwareSerial fnM16pSerial(2); // Use UART2 - -// Status flags -bool audioReady = false; -bool loraReady = false; -bool dhtReady = false; - -// LoRa transmission variables -unsigned long lastLoRaSend = 0; -int loraInterval = 30000; // Send every 30 seconds -int packetCount = 0; - -// DHT11 reading variables -unsigned long lastDHTReading = 0; -const unsigned long DHT_READING_INTERVAL = 2000; // DHT11 needs 2 seconds between readings -float lastValidTemperature = FALLBACK_TEMPERATURE; -float lastValidHumidity = FALLBACK_HUMIDITY; - -// Data structure for sensor readings -struct SensorData { - float temperature; - float humidity; - int mq2_analog; - int mq9_analog; - int mq135_analog; - bool mq2_digital; - bool mq9_digital; - bool mq135_digital; - unsigned long timestamp; -}; - -void setup() { - Serial.begin(115200); - Serial.println("================================="); - Serial.println("ESP32 Multi-Sensor System v5.3"); - Serial.println("FIXED: High Gas Threshold Only"); - Serial.println("WITH DHT11 Sensor"); - Serial.println("================================="); - - // Initialize MQ sensor pins - pinMode(MQ2_DIGITAL_PIN, INPUT); - pinMode(MQ9_DIGITAL_PIN, INPUT); - pinMode(MQ135_DIGITAL_PIN, INPUT); - - // Initialize DHT11 - Serial.println("Initializing DHT11 sensor..."); - dht.begin(); - delay(2000); // DHT11 needs time to stabilize - - // Test DHT11 reading - float testTemp = dht.readTemperature(); - float testHum = dht.readHumidity(); - - if (isnan(testTemp) || isnan(testHum)) { - Serial.println("DHT11 not responding properly. Check wiring."); - dhtReady = false; - } else { - Serial.println("DHT11 initialized successfully!"); - Serial.printf("Initial reading - Temperature: %.1f°C, Humidity: %.1f%%\n", testTemp, testHum); - dhtReady = true; - lastValidTemperature = testTemp; - lastValidHumidity = testHum; - } - - // Initialize FN-M16P Audio Module - fnM16pSerial.begin(9600, SERIAL_8N1, FN_M16P_RX, FN_M16P_TX); - delay(2000); // Give FN-M16P time to initialize - - Serial.println("Initializing FN-M16P Audio Module..."); - setVolume(30); // Set volume (0-30) - delay(500); - - audioReady = true; - Serial.println("FN-M16P initialized successfully!"); - - // Initialize LoRa - SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_SS); - LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0); - - if (!LoRa.begin(LORA_BAND)) { - Serial.println("LoRa initialization failed. Check wiring."); - loraReady = false; - } else { - // Set LoRa parameters for maximum reliability - LoRa.setTxPower(20); - LoRa.setSpreadingFactor(12); - LoRa.setSignalBandwidth(125E3); - LoRa.setCodingRate4(8); - LoRa.setPreambleLength(8); - LoRa.setSyncWord(0x34); - - Serial.println("LoRa initialized successfully!"); - loraReady = true; - } - - Serial.println("Warming up gas sensors..."); - delay(15000); // 15 second warm-up for better stability - - // Calibrate MQ sensors after warm-up - Serial.println("Calibrating MQ sensors for DANGER-ONLY thresholds..."); - calibrateSensors(); - - Serial.println("System ready!"); - Serial.println(); - - // Play startup sound - if (audioReady) { - playAudioFile(BOOT_AUDIO); - delay(2000); - } -} - -void loop() { - // Read all sensors - SensorData data = readAllSensors(); - - // Display readings - displayReadings(data); - - // Check for alerts - checkAlerts(data); - - // Send LoRa data (every 30 seconds) - if (loraReady && (millis() - lastLoRaSend > loraInterval)) { - sendLoRaData(data); - lastLoRaSend = millis(); - } - - Serial.println("------------------------"); - delay(10000); // Main loop every 10 seconds -} - -// FN-M16P Functions -void sendCommand(byte cmd, byte param1, byte param2, bool feedback) { - byte packet[10]; - - packet[0] = FRAME_START; - packet[1] = VERSION; - packet[2] = 0x06; - packet[3] = cmd; - packet[4] = feedback ? WITH_FEEDBACK : NO_FEEDBACK; - packet[5] = param1; - packet[6] = param2; - - uint16_t sum = packet[1] + packet[2] + packet[3] + packet[4] + packet[5] + packet[6]; - uint16_t checksum = 0xFFFF - sum + 1; - - packet[7] = (checksum >> 8) & 0xFF; - packet[8] = checksum & 0xFF; - packet[9] = FRAME_END; - - fnM16pSerial.write(packet, 10); -} - -void setVolume(int volume) { - if (volume < 0) volume = 0; - if (volume > 30) volume = 30; - sendCommand(CMD_VOLUME, 0x00, volume, false); - Serial.println("FN-M16P: Volume set to " + String(volume)); -} - -void playAudioFile(int fileNumber) { - if (fileNumber < 1 || fileNumber > 8) return; - - Serial.println("*** AUDIO TRIGGERED at " + String(millis()) + "ms ***"); - - String fileName = ""; - if(fileNumber < 10) fileName = "000" + String(fileNumber); - else fileName = "00" + String(fileNumber); - - Serial.println("Playing: " + getAudioDescription(fileNumber) + " (" + fileName + ".mp3)"); - sendCommand(CMD_PLAY_TRACK, 0x00, fileNumber, false); - delay(100); -} - -String getAudioDescription(int fileNumber) { - switch(fileNumber) { - case 1: return "System Boot"; - case 2: return "Smoke/Gas Alert"; - case 3: return "CO Alert"; - case 4: return "Air Quality Warning"; - case 5: return "High Temperature"; - case 6: return "Low Temperature"; - case 7: return "High Humidity"; - case 8: return "Low Humidity"; - default: return "Unknown"; - } -} - -void stopAudio() { - sendCommand(CMD_STOP, 0x00, 0x00, false); -} - -// FIXED: New calibration function - establishes DANGER thresholds only -void calibrateSensors() { - Serial.println("Starting sensor calibration for DANGER thresholds..."); - Serial.println("Ensure sensors are in clean air environment"); - delay(3000); - - // Calibrate MQ2 - Serial.print("Calibrating MQ2... "); - calibrateSensor(MQ2_ANALOG_PIN, &mq2_cal, "MQ2"); - - // Calibrate MQ9 - Serial.print("Calibrating MQ9... "); - calibrateSensor(MQ9_ANALOG_PIN, &mq9_cal, "MQ9"); - - // Calibrate MQ135 - Serial.print("Calibrating MQ135... "); - calibrateSensor(MQ135_ANALOG_PIN, &mq135_cal, "MQ135"); - - Serial.println("Sensor calibration completed!"); - Serial.println("DANGER-ONLY Baselines established:"); - Serial.printf(" MQ2: %.1f (DANGER threshold: %.1f - must be 4x baseline!)\n", mq2_cal.baseline, mq2_cal.dangerThreshold); - Serial.printf(" MQ9: %.1f (DANGER threshold: %.1f - must be 4x baseline!)\n", mq9_cal.baseline, mq9_cal.dangerThreshold); - Serial.printf(" MQ135: %.1f (DANGER threshold: %.1f - must be 4x baseline!)\n", mq135_cal.baseline, mq135_cal.dangerThreshold); - Serial.println(); - Serial.println("Audio alerts will ONLY trigger at extreme gas levels!"); -} - -// FIXED: New calibration function - only high thresholds -void calibrateSensor(int pin, SensorCalibration* cal, String sensorName) { - float sum = 0; - - for (int i = 0; i < CALIBRATION_SAMPLES; i++) { - int reading = analogRead(pin); - sum += reading; - Serial.print("."); - delay(CALIBRATION_DELAY); - } - - cal->baseline = sum / CALIBRATION_SAMPLES; - - // FIXED: Only create danger threshold (must be 4x baseline) - cal->dangerThreshold = cal->baseline * DANGER_MULTIPLIER; - - // Also ensure minimum static threshold for safety - if (sensorName == "MQ2" && cal->dangerThreshold < MQ2_DANGER_THRESHOLD) { - cal->dangerThreshold = MQ2_DANGER_THRESHOLD; - } - if (sensorName == "MQ9" && cal->dangerThreshold < MQ9_DANGER_THRESHOLD) { - cal->dangerThreshold = MQ9_DANGER_THRESHOLD; - } - if (sensorName == "MQ135" && cal->dangerThreshold < MQ135_DANGER_THRESHOLD) { - cal->dangerThreshold = MQ135_DANGER_THRESHOLD; - } - - cal->calibrated = true; - Serial.println(" Done"); -} - -// FIXED: Only trigger alerts for VERY HIGH readings (danger level) -bool checkSensorDanger(int currentValue, SensorCalibration* cal, int staticDangerThreshold) { - if (!cal->calibrated) { - // Fall back to static danger threshold if not calibrated - return currentValue > staticDangerThreshold; - } - - // ONLY trigger if current value exceeds danger threshold (4x baseline OR static minimum) - return currentValue > cal->dangerThreshold; -} - -void logSensorStatus(String sensorName, int currentValue, SensorCalibration* cal, bool isDanger) { - if (cal->calibrated) { - float ratio = currentValue / cal->baseline; - Serial.printf(" %s: %d (baseline: %.1f, ratio: %.1fx, danger: %.1f) - %s\n", - sensorName.c_str(), currentValue, cal->baseline, ratio, cal->dangerThreshold, - isDanger ? "DANGER!" : "Safe"); - } else { - Serial.printf(" %s: %d (uncalibrated) - %s\n", - sensorName.c_str(), currentValue, isDanger ? "DANGER!" : "Safe"); - } -} - -// Sensor reading functions with DHT11 handling -SensorData readAllSensors() { - SensorData data; - data.timestamp = millis(); - - // Read DHT11 with proper timing and fallback - if (millis() - lastDHTReading > DHT_READING_INTERVAL) { - float temp = dht.readTemperature(); - float hum = dht.readHumidity(); - - // Check for DHT11 sensor errors and use fallback values - if (isnan(temp) || temp < -40 || temp > 80) { - data.temperature = lastValidTemperature; - if (!isnan(temp)) { - Serial.println("DHT11 temperature out of range - using last valid: " + String(lastValidTemperature) + "°C"); - } else { - Serial.println("DHT11 temperature error - using last valid: " + String(lastValidTemperature) + "°C"); - } - } else { - data.temperature = temp; - lastValidTemperature = temp; - } - - if (isnan(hum) || hum < 0 || hum > 100) { - data.humidity = lastValidHumidity; - if (!isnan(hum)) { - Serial.println("DHT11 humidity out of range - using last valid: " + String(lastValidHumidity) + "%"); - } else { - Serial.println("DHT11 humidity error - using last valid: " + String(lastValidHumidity) + "%"); - } - } else { - data.humidity = hum; - lastValidHumidity = hum; - } - - lastDHTReading = millis(); - } else { - // Use last valid readings if it's too soon to read DHT11 again - data.temperature = lastValidTemperature; - data.humidity = lastValidHumidity; - } - - // Read MQ sensors - data.mq2_analog = analogRead(MQ2_ANALOG_PIN); - data.mq9_analog = analogRead(MQ9_ANALOG_PIN); - data.mq135_analog = analogRead(MQ135_ANALOG_PIN); - - data.mq2_digital = digitalRead(MQ2_DIGITAL_PIN) == LOW; - data.mq9_digital = digitalRead(MQ9_DIGITAL_PIN) == LOW; - data.mq135_digital = digitalRead(MQ135_DIGITAL_PIN) == LOW; - - return data; -} - -void displayReadings(SensorData data) { - Serial.println("=== SENSOR READINGS ==="); - Serial.println("Timestamp: " + String(data.timestamp)); - Serial.println("Audio: " + String(audioReady ? "Ready" : "Not Ready")); - Serial.println("LoRa: " + String(loraReady ? "Ready" : "Not Ready")); - Serial.println("DHT11: " + String(dhtReady ? "Ready" : "Not Ready")); - Serial.println(); - - // Environmental data - Serial.println("DHT11 (Temperature & Humidity):"); - Serial.printf(" Temperature: %.2f°C\n", data.temperature); - Serial.printf(" Humidity: %.2f%%\n", data.humidity); - Serial.println(); - - // FIXED: Gas sensors with DANGER-only status display - Serial.println("MQ2 (Smoke/LPG/Gas) - DANGER-ONLY Alerts:"); - Serial.printf(" Digital: %s | Analog: %d", - data.mq2_digital ? "GAS DETECTED" : "No Gas", data.mq2_analog); - bool mq2_danger = checkSensorDanger(data.mq2_analog, &mq2_cal, MQ2_DANGER_THRESHOLD); - Serial.println(mq2_danger ? " - EXTREME DANGER!" : " - Safe"); - logSensorStatus("MQ2", data.mq2_analog, &mq2_cal, mq2_danger); - - Serial.println("MQ9 (Carbon Monoxide) - DANGER-ONLY Alerts:"); - Serial.printf(" Digital: %s | Analog: %d", - data.mq9_digital ? "CO DETECTED" : "No CO", data.mq9_analog); - bool mq9_danger = checkSensorDanger(data.mq9_analog, &mq9_cal, MQ9_DANGER_THRESHOLD); - Serial.println(mq9_danger ? " - EXTREME DANGER!" : " - Safe"); - logSensorStatus("MQ9", data.mq9_analog, &mq9_cal, mq9_danger); - - Serial.println("MQ135 (Air Quality/CO2) - DANGER-ONLY Alerts:"); - Serial.printf(" Digital: %s | Analog: %d", - data.mq135_digital ? "POOR AIR" : "Good Air", data.mq135_analog); - bool mq135_danger = checkSensorDanger(data.mq135_analog, &mq135_cal, MQ135_DANGER_THRESHOLD); - Serial.println(mq135_danger ? " - EXTREME DANGER!" : " - Safe"); - logSensorStatus("MQ135", data.mq135_analog, &mq135_cal, mq135_danger); -} - -// FIXED: Only trigger audio for EXTREME danger levels -void checkAlerts(SensorData data) { - static unsigned long lastAlert = 0; - unsigned long now = millis(); - - Serial.printf("checkAlerts() called at %lu ms (lastAlert: %lu ms)\n", now, lastAlert); - - // Avoid too frequent alerts (minimum 60 seconds between alerts for extreme conditions) - if (now - lastAlert < 60000) { - Serial.println("*** ALERT COOLDOWN ACTIVE - No alerts will play ***"); - return; - } - - // FIXED: Check for EXTREME DANGER conditions only - bool mq2_danger = checkSensorDanger(data.mq2_analog, &mq2_cal, MQ2_DANGER_THRESHOLD); - bool mq9_danger = checkSensorDanger(data.mq9_analog, &mq9_cal, MQ9_DANGER_THRESHOLD); - bool mq135_danger = checkSensorDanger(data.mq135_analog, &mq135_cal, MQ135_DANGER_THRESHOLD); - - // DEBUG: Print danger status - Serial.println("DEBUG Danger Status (4x baseline or static minimum):"); - Serial.printf(" MQ2 EXTREME DANGER: %s (threshold: %.1f)\n", mq2_danger ? "TRUE" : "FALSE", mq2_cal.dangerThreshold); - Serial.printf(" MQ9 EXTREME DANGER: %s (threshold: %.1f)\n", mq9_danger ? "TRUE" : "FALSE", mq9_cal.dangerThreshold); - Serial.printf(" MQ135 EXTREME DANGER: %s (threshold: %.1f)\n", mq135_danger ? "TRUE" : "FALSE", mq135_cal.dangerThreshold); - - // ONLY trigger audio for EXTREME readings - if (mq2_danger) { - Serial.println("*** EXTREME MQ2 DANGER - PLAYING SMOKE ALERT ***"); - Serial.println("EXTREME DANGER: Very high smoke/gas levels detected!"); - playAudioFile(SMOKE_ALERT); - lastAlert = now; - } - else if (mq9_danger) { - Serial.println("*** EXTREME MQ9 DANGER - PLAYING CO ALERT ***"); - Serial.println("EXTREME DANGER: Very high carbon monoxide levels detected!"); - playAudioFile(CO_ALERT); - lastAlert = now; - } - else if (mq135_danger) { - Serial.println("*** EXTREME MQ135 DANGER - PLAYING AIR QUALITY ALERT ***"); - Serial.println("EXTREME DANGER: Very poor air quality detected!"); - playAudioFile(AIR_QUALITY_WARNING); - lastAlert = now; - } - else { - Serial.println("*** NO EXTREME GAS DANGERS - CHECKING TEMPERATURE/HUMIDITY ***"); - } - - // Temperature alerts (slightly more conservative) - if (data.temperature > 45.0) { - Serial.println("*** EXTREME HIGH TEMPERATURE ALERT TRIGGERED ***"); - Serial.println("DANGER: Extreme high temperature!"); - playAudioFile(HIGH_TEMP_ALERT); - lastAlert = now; - } - else if (data.temperature < 0.0) { - Serial.println("*** EXTREME LOW TEMPERATURE ALERT TRIGGERED ***"); - Serial.println("DANGER: Freezing temperature!"); - playAudioFile(LOW_TEMP_ALERT); - lastAlert = now; - } - - // Humidity alerts (more conservative) - else if (data.humidity > 90.0) { - Serial.println("*** EXTREME HIGH HUMIDITY ALERT TRIGGERED ***"); - Serial.println("DANGER: Extreme high humidity!"); - playAudioFile(HIGH_HUMIDITY_ALERT); - lastAlert = now; - } - else if (data.humidity < 10.0) { - Serial.println("*** EXTREME LOW HUMIDITY ALERT TRIGGERED ***"); - Serial.println("DANGER: Extreme low humidity!"); - playAudioFile(LOW_HUMIDITY_ALERT); - lastAlert = now; - } - else { - Serial.println("*** NO EXTREME CONDITIONS - ALL VALUES SAFE ***"); - } -} - -void sendLoRaData(SensorData data) { - packetCount++; - - // Create JSON packet with real sensor data - StaticJsonDocument<280> doc; - doc["nodeId"] = "001"; - doc["packetCount"] = packetCount; - - // Real sensor data - doc["temperature"] = data.temperature; - doc["humidity"] = data.humidity; - doc["mq2_analog"] = data.mq2_analog; - doc["mq9_analog"] = data.mq9_analog; - doc["mq135_analog"] = data.mq135_analog; - doc["mq2_digital"] = data.mq2_digital; - doc["mq9_digital"] = data.mq9_digital; - doc["mq135_digital"] = data.mq135_digital; - doc["air_quality"] = getAirQualityRating(data.mq135_analog); - - String jsonString; - serializeJson(doc, jsonString); - - Serial.print("Sending LoRa packet #"); - Serial.print(packetCount); - Serial.print(" from Node 001: "); - Serial.println(jsonString); - - LoRa.beginPacket(); - LoRa.print(jsonString); - LoRa.endPacket(); - - Serial.println("LoRa packet sent!"); -} - -String getAirQualityRating(int value) { - if (value < 800) return "Excellent"; - else if (value < 1200) return "Good"; - else if (value < 1800) return "Moderate"; - else if (value < 2400) return "Poor"; - else return "Very Poor"; -} diff --git a/edge_node.ino b/edge_node.ino deleted file mode 100644 index c8f432a..0000000 --- a/edge_node.ino +++ /dev/null @@ -1,959 +0,0 @@ -/* - ESP32 Multi-Sensor System with Emergency Button - OPTIMIZED VERSION with Test Commands - - Added comprehensive test commands via Serial Monitor - - Cleaned up redundant code - - Updated LORA_DIO0 to GPIO26 - - Fixed potential bugs -*/ - -#include -#include -#include -#include -#include -#include - -// Pin definitions for MQ Sensors -#define MQ2_DIGITAL_PIN 12 -#define MQ2_ANALOG_PIN 32 -#define MQ9_DIGITAL_PIN 13 -#define MQ9_ANALOG_PIN 33 -#define MQ135_DIGITAL_PIN 15 -#define MQ135_ANALOG_PIN 35 - -// FN-M16P Audio Module pins -#define FN_M16P_RX 16 -#define FN_M16P_TX 17 - -// DHT11 pin -#define DHT11_PIN 21 -#define DHT_TYPE DHT11 - -// LoRa Module pins -#define LORA_SCK 5 -#define LORA_MISO 19 -#define LORA_MOSI 27 -#define LORA_SS 18 -#define LORA_RST 14 -#define LORA_DIO0 26 // UPDATED FROM GPIO34 TO GPIO26 - -// EMERGENCY BUTTON PIN (TTP223 Touch Sensor) -#define EMERGENCY_BUTTON_PIN 25 - -// LoRa frequency -#define LORA_BAND 915E6 - -// Node identification -#define NODE_ID "001" - -// Sensor thresholds -#define MQ2_DANGER_THRESHOLD 1600 -#define MQ9_DANGER_THRESHOLD 3800 -#define MQ135_DANGER_THRESHOLD 1800 - -#define FALLBACK_TEMPERATURE 27.0 -#define FALLBACK_HUMIDITY 47.0 - -#define CALIBRATION_SAMPLES 10 -#define DANGER_MULTIPLIER 2.0 -#define CALIBRATION_DELAY 2000 - -// Emergency Button Parameters -const int TAP_TIMEOUT = 600; -const int REQUIRED_TAPS = 3; - -// FN-M16P Commands -const byte FRAME_START = 0x7E; -const byte FRAME_END = 0xEF; -const byte VERSION = 0xFF; -const byte NO_FEEDBACK = 0x00; -const byte WITH_FEEDBACK = 0x01; -const byte CMD_PLAY_TRACK = 0x03; -const byte CMD_VOLUME = 0x06; -const byte CMD_STOP = 0x16; - -// Audio file mapping -enum AudioFiles { - BOOT_AUDIO = 1, - SMOKE_ALERT = 2, - CO_ALERT = 3, - AIR_QUALITY_WARNING = 4, - HIGH_TEMP_ALERT = 5, - LOW_TEMP_ALERT = 6, - HIGH_HUMIDITY_ALERT = 7, - LOW_HUMIDITY_ALERT = 8 -}; - -struct SensorCalibration { - float baseline; - float dangerThreshold; - bool calibrated; -}; - -SensorCalibration mq2_cal = {0, 0, false}; -SensorCalibration mq9_cal = {0, 0, false}; -SensorCalibration mq135_cal = {0, 0, false}; - -DHT dht(DHT11_PIN, DHT_TYPE); -HardwareSerial fnM16pSerial(2); - -bool audioReady = false; -bool loraReady = false; -bool dhtReady = false; - -unsigned long lastLoRaSend = 0; -int loraInterval = 30000; -int packetCount = 0; - -unsigned long lastDHTReading = 0; -const unsigned long DHT_READING_INTERVAL = 2000; -float lastValidTemperature = FALLBACK_TEMPERATURE; -float lastValidHumidity = FALLBACK_HUMIDITY; - -// Emergency Button Variables -volatile int tapCount = 0; -volatile unsigned long lastTapTime = 0; -volatile bool emergencyTriggered = false; - -struct SensorData { - float temperature; - float humidity; - int mq2_analog; - int mq9_analog; - int mq135_analog; - bool mq2_digital; - bool mq9_digital; - bool mq135_digital; - bool emergency; - unsigned long timestamp; -}; - -void setup() { - Serial.begin(115200); - delay(1000); - Serial.println("\n\n================================="); - Serial.println("ESP32 Multi-Sensor System v8.0"); - Serial.println("OPTIMIZED with TEST COMMANDS"); - Serial.println("================================="); - - // Initialize MQ sensor pins - pinMode(MQ2_DIGITAL_PIN, INPUT); - pinMode(MQ9_DIGITAL_PIN, INPUT); - pinMode(MQ135_DIGITAL_PIN, INPUT); - - // Initialize Emergency Button - pinMode(EMERGENCY_BUTTON_PIN, INPUT); - Serial.println("\n>>> EMERGENCY BUTTON SETUP <<<"); - Serial.println("Pin: GPIO25"); - Serial.println("Tap 3 times quickly to trigger emergency!"); - - int testRead = digitalRead(EMERGENCY_BUTTON_PIN); - Serial.print("Initial button state: "); - Serial.println(testRead == HIGH ? "HIGH" : "LOW"); - Serial.println(">>> Button ready <<<\n"); - - // Initialize DHT11 - Serial.println("Initializing DHT11 sensor..."); - dht.begin(); - delay(2000); - - // Test DHT11 - bool dhtWorking = false; - for (int i = 0; i < 3; i++) { - float testTemp = dht.readTemperature(); - float testHum = dht.readHumidity(); - - if (!isnan(testTemp) && !isnan(testHum)) { - dhtReady = true; - dhtWorking = true; - lastValidTemperature = testTemp; - lastValidHumidity = testHum; - Serial.println("✓ DHT11 initialized successfully!"); - Serial.printf(" Temperature: %.1f°C\n", lastValidTemperature); - Serial.printf(" Humidity: %.1f%%\n", lastValidHumidity); - break; - } - delay(2000); - } - - if (!dhtWorking) { - Serial.println("⚠ DHT11 ERROR - Check wiring!"); - Serial.println(" VCC -> 3.3V, GND -> GND, DATA -> GPIO21"); - dhtReady = false; - } - - // Initialize FN-M16P Audio Module - fnM16pSerial.begin(9600, SERIAL_8N1, FN_M16P_RX, FN_M16P_TX); - delay(2000); - - Serial.println("Initializing FN-M16P Audio Module..."); - setVolume(30); - delay(500); - - audioReady = true; - Serial.println("✓ FN-M16P initialized successfully!"); - - // Initialize LoRa - SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_SS); - LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0); - - if (!LoRa.begin(LORA_BAND)) { - Serial.println("⚠ LoRa initialization failed. Check wiring."); - loraReady = false; - } else { - LoRa.setTxPower(20); - LoRa.setSpreadingFactor(12); - LoRa.setSignalBandwidth(125E3); - LoRa.setCodingRate4(8); - LoRa.setPreambleLength(8); - LoRa.setSyncWord(0x34); - - Serial.println("✓ LoRa initialized successfully!"); - Serial.println(" DIO0 Pin: GPIO26"); - loraReady = true; - } - - Serial.println("Warming up gas sensors (15s)..."); - delay(15000); - - Serial.println("Calibrating MQ sensors..."); - calibrateSensors(); - - Serial.println("\n================================="); - Serial.println("SYSTEM READY!"); - Serial.println("================================="); - printTestMenu(); - - if (audioReady) { - playAudioFile(BOOT_AUDIO); - delay(2000); - } -} - -void loop() { - // Check for serial test commands - if (Serial.available() > 0) { - String command = Serial.readStringUntil('\n'); - command.trim(); - command.toLowerCase(); - handleTestCommand(command); - } - - // Check emergency button - checkEmergencyButton(); - - // Handle emergency if triggered - if (emergencyTriggered) { - handleEmergency(); - emergencyTriggered = false; - return; - } - - // Normal operation - static unsigned long lastNormalLoop = 0; - - if (millis() - lastNormalLoop >= 10000) { - lastNormalLoop = millis(); - - SensorData data = readAllSensors(); - data.emergency = false; - - displayReadings(data); - checkAlerts(data); - - if (loraReady && (millis() - lastLoRaSend > loraInterval)) { - sendLoRaData(data); - lastLoRaSend = millis(); - } - - Serial.println("------------------------"); - } - - delay(50); -} - -// ============ TEST COMMAND HANDLER ============ -void handleTestCommand(String cmd) { - Serial.println("\n>>> EXECUTING TEST: " + cmd + " <<<\n"); - - if (cmd == "help" || cmd == "menu") { - printTestMenu(); - } - else if (cmd == "dht") { - testDHT(); - } - else if (cmd == "mq2") { - testMQ2(); - } - else if (cmd == "mq9") { - testMQ9(); - } - else if (cmd == "mq135") { - testMQ135(); - } - else if (cmd == "mq") { - testAllMQ(); - } - else if (cmd == "audio1" || cmd == "a1") { - testAudio(1); - } - else if (cmd == "audio2" || cmd == "a2") { - testAudio(2); - } - else if (cmd == "audio3" || cmd == "a3") { - testAudio(3); - } - else if (cmd == "audio4" || cmd == "a4") { - testAudio(4); - } - else if (cmd == "audio5" || cmd == "a5") { - testAudio(5); - } - else if (cmd == "audio6" || cmd == "a6") { - testAudio(6); - } - else if (cmd == "audio7" || cmd == "a7") { - testAudio(7); - } - else if (cmd == "audio8" || cmd == "a8") { - testAudio(8); - } - else if (cmd == "stop") { - stopAudio(); - Serial.println("Audio stopped."); - } - else if (cmd == "volume+") { - setVolume(25); - Serial.println("Volume set to 25"); - } - else if (cmd == "volume-") { - setVolume(15); - Serial.println("Volume set to 15"); - } - else if (cmd == "lora") { - testLoRa(); - } - else if (cmd == "emergency") { - testEmergency(); - } - else if (cmd == "button") { - testButton(); - } - else if (cmd == "all") { - testAllSensors(); - } - else if (cmd == "calibrate") { - calibrateSensors(); - } - else if (cmd == "status") { - printSystemStatus(); - } - else { - Serial.println("❌ Unknown command: " + cmd); - Serial.println("Type 'help' for available commands."); - } - - Serial.println("\n>>> TEST COMPLETE <<<\n"); -} - -void printTestMenu() { - Serial.println("\n╔═══════════════════════════════════════╗"); - Serial.println("║ TEST COMMANDS MENU ║"); - Serial.println("╠═══════════════════════════════════════╣"); - Serial.println("║ SENSORS: ║"); - Serial.println("║ dht - Test DHT11 sensor ║"); - Serial.println("║ mq2 - Test MQ2 sensor ║"); - Serial.println("║ mq9 - Test MQ9 sensor ║"); - Serial.println("║ mq135 - Test MQ135 sensor ║"); - Serial.println("║ mq - Test all MQ sensors ║"); - Serial.println("║ all - Test all sensors ║"); - Serial.println("║ ║"); - Serial.println("║ AUDIO: ║"); - Serial.println("║ audio1-8 - Play audio file 1-8 ║"); - Serial.println("║ a1-a8 - Short form (e.g. a1) ║"); - Serial.println("║ stop - Stop audio playback ║"); - Serial.println("║ volume+ - Increase volume ║"); - Serial.println("║ volume- - Decrease volume ║"); - Serial.println("║ ║"); - Serial.println("║ COMMUNICATION: ║"); - Serial.println("║ lora - Test LoRa transmission ║"); - Serial.println("║ ║"); - Serial.println("║ EMERGENCY: ║"); - Serial.println("║ button - Test emergency button ║"); - Serial.println("║ emergency - Trigger emergency mode ║"); - Serial.println("║ ║"); - Serial.println("║ SYSTEM: ║"); - Serial.println("║ calibrate - Re-calibrate sensors ║"); - Serial.println("║ status - Show system status ║"); - Serial.println("║ help - Show this menu ║"); - Serial.println("╚═══════════════════════════════════════╝\n"); -} - -void testDHT() { - Serial.println("Testing DHT11 Sensor..."); - Serial.println("Reading 5 samples with 2s interval:\n"); - - for (int i = 1; i <= 5; i++) { - float temp = dht.readTemperature(); - float hum = dht.readHumidity(); - - Serial.print("Sample #"); - Serial.print(i); - Serial.print(": "); - - if (!isnan(temp) && !isnan(hum)) { - Serial.printf("Temp: %.2f°C, Humidity: %.2f%% ✓\n", temp, hum); - } else { - Serial.println("FAILED - NaN values ✗"); - } - - if (i < 5) delay(2000); - } - - Serial.println("\nDHT11 Status: " + String(dhtReady ? "READY" : "ERROR")); -} - -void testMQ2() { - Serial.println("Testing MQ2 Sensor (Smoke/LPG/Gas)..."); - Serial.println("Reading 10 samples with 500ms interval:\n"); - - for (int i = 1; i <= 10; i++) { - int analog = analogRead(MQ2_ANALOG_PIN); - bool digital = digitalRead(MQ2_DIGITAL_PIN) == LOW; - - Serial.printf("Sample #%d: Analog=%d, Digital=%s", - i, analog, digital ? "DETECTED" : "Clear"); - - if (mq2_cal.calibrated) { - bool danger = analog > mq2_cal.dangerThreshold; - Serial.print(danger ? " [DANGER]" : " [Safe]"); - } - Serial.println(); - - if (i < 10) delay(500); - } - - Serial.printf("\nCalibration: Baseline=%.1f, Threshold=%.1f\n", - mq2_cal.baseline, mq2_cal.dangerThreshold); -} - -void testMQ9() { - Serial.println("Testing MQ9 Sensor (Carbon Monoxide)..."); - Serial.println("Reading 10 samples with 500ms interval:\n"); - - for (int i = 1; i <= 10; i++) { - int analog = analogRead(MQ9_ANALOG_PIN); - bool digital = digitalRead(MQ9_DIGITAL_PIN) == LOW; - - Serial.printf("Sample #%d: Analog=%d, Digital=%s", - i, analog, digital ? "DETECTED" : "Clear"); - - if (mq9_cal.calibrated) { - bool danger = analog > mq9_cal.dangerThreshold; - Serial.print(danger ? " [DANGER]" : " [Safe]"); - } - Serial.println(); - - if (i < 10) delay(500); - } - - Serial.printf("\nCalibration: Baseline=%.1f, Threshold=%.1f\n", - mq9_cal.baseline, mq9_cal.dangerThreshold); -} - -void testMQ135() { - Serial.println("Testing MQ135 Sensor (Air Quality)..."); - Serial.println("Reading 10 samples with 500ms interval:\n"); - - for (int i = 1; i <= 10; i++) { - int analog = analogRead(MQ135_ANALOG_PIN); - bool digital = digitalRead(MQ135_DIGITAL_PIN) == LOW; - String quality = getAirQualityRating(analog); - - Serial.printf("Sample #%d: Analog=%d, Digital=%s, Quality=%s", - i, analog, digital ? "POOR" : "Good", quality.c_str()); - - if (mq135_cal.calibrated) { - bool danger = analog > mq135_cal.dangerThreshold; - Serial.print(danger ? " [DANGER]" : " [Safe]"); - } - Serial.println(); - - if (i < 10) delay(500); - } - - Serial.printf("\nCalibration: Baseline=%.1f, Threshold=%.1f\n", - mq135_cal.baseline, mq135_cal.dangerThreshold); -} - -void testAllMQ() { - Serial.println("Testing All MQ Sensors...\n"); - - Serial.println("MQ2 (Smoke/LPG/Gas):"); - int mq2 = analogRead(MQ2_ANALOG_PIN); - Serial.printf(" Analog: %d\n", mq2); - Serial.printf(" Digital: %s\n", digitalRead(MQ2_DIGITAL_PIN) == LOW ? "DETECTED" : "Clear"); - - Serial.println("\nMQ9 (Carbon Monoxide):"); - int mq9 = analogRead(MQ9_ANALOG_PIN); - Serial.printf(" Analog: %d\n", mq9); - Serial.printf(" Digital: %s\n", digitalRead(MQ9_DIGITAL_PIN) == LOW ? "DETECTED" : "Clear"); - - Serial.println("\nMQ135 (Air Quality):"); - int mq135 = analogRead(MQ135_ANALOG_PIN); - Serial.printf(" Analog: %d\n", mq135); - Serial.printf(" Digital: %s\n", digitalRead(MQ135_DIGITAL_PIN) == LOW ? "POOR" : "Good"); - Serial.printf(" Rating: %s\n", getAirQualityRating(mq135).c_str()); -} - -void testAudio(int fileNum) { - if (!audioReady) { - Serial.println("❌ Audio module not ready!"); - return; - } - - Serial.print("Playing audio file #"); - Serial.println(fileNum); - - const char* audioNames[] = { - "", "Boot", "Smoke Alert", "CO Alert", "Air Quality Warning", - "High Temp Alert", "Low Temp Alert", "High Humidity", "Low Humidity" - }; - - if (fileNum >= 1 && fileNum <= 8) { - Serial.print("File: "); - Serial.println(audioNames[fileNum]); - playAudioFile(fileNum); - } else { - Serial.println("❌ Invalid file number (1-8)"); - } -} - -void testLoRa() { - if (!loraReady) { - Serial.println("❌ LoRa not ready!"); - return; - } - - Serial.println("Testing LoRa transmission..."); - - SensorData data = readAllSensors(); - data.emergency = false; - - Serial.println("\nTest packet contents:"); - Serial.printf(" Node ID: %s\n", NODE_ID); - Serial.printf(" Temperature: %.2f°C\n", data.temperature); - Serial.printf(" Humidity: %.2f%%\n", data.humidity); - Serial.printf(" MQ2: %d\n", data.mq2_analog); - Serial.printf(" MQ9: %d\n", data.mq9_analog); - Serial.printf(" MQ135: %d\n", data.mq135_analog); - - Serial.println("\nSending test packet..."); - sendLoRaData(data); - Serial.println("✓ Test packet sent!"); -} - -void testEmergency() { - Serial.println("⚠ Triggering EMERGENCY MODE manually...\n"); - emergencyTriggered = true; -} - -void testButton() { - Serial.println("Testing Emergency Button..."); - Serial.println("Button Pin: GPIO25"); - Serial.println("Monitoring for 10 seconds...\n"); - - unsigned long startTime = millis(); - int changeCount = 0; - bool lastState = digitalRead(EMERGENCY_BUTTON_PIN); - - Serial.print("Initial state: "); - Serial.println(lastState == HIGH ? "HIGH" : "LOW"); - Serial.println("\nPress the button now...\n"); - - while (millis() - startTime < 10000) { - bool currentState = digitalRead(EMERGENCY_BUTTON_PIN); - - if (currentState != lastState) { - changeCount++; - Serial.print("State change #"); - Serial.print(changeCount); - Serial.print(": "); - Serial.println(currentState == HIGH ? "HIGH (Pressed)" : "LOW (Released)"); - lastState = currentState; - delay(50); - } - delay(10); - } - - Serial.print("\nTotal state changes: "); - Serial.println(changeCount); - Serial.println(changeCount > 0 ? "✓ Button working!" : "❌ No input detected - check wiring"); -} - -void testAllSensors() { - Serial.println("=== COMPLETE SYSTEM TEST ===\n"); - - Serial.println("1. DHT11 Sensor:"); - float temp = dht.readTemperature(); - float hum = dht.readHumidity(); - if (!isnan(temp) && !isnan(hum)) { - Serial.printf(" ✓ Temp: %.2f°C, Humidity: %.2f%%\n", temp, hum); - } else { - Serial.println(" ✗ DHT11 reading failed"); - } - - Serial.println("\n2. MQ2 Sensor:"); - Serial.printf(" Analog: %d\n", analogRead(MQ2_ANALOG_PIN)); - - Serial.println("\n3. MQ9 Sensor:"); - Serial.printf(" Analog: %d\n", analogRead(MQ9_ANALOG_PIN)); - - Serial.println("\n4. MQ135 Sensor:"); - Serial.printf(" Analog: %d\n", analogRead(MQ135_ANALOG_PIN)); - - Serial.println("\n5. Emergency Button:"); - Serial.printf(" State: %s\n", digitalRead(EMERGENCY_BUTTON_PIN) == HIGH ? "HIGH" : "LOW"); - - Serial.println("\n6. Audio Module:"); - Serial.printf(" Status: %s\n", audioReady ? "READY" : "NOT READY"); - - Serial.println("\n7. LoRa Module:"); - Serial.printf(" Status: %s\n", loraReady ? "READY" : "NOT READY"); - - Serial.println("\n=== TEST COMPLETE ==="); -} - -void printSystemStatus() { - Serial.println("\n╔═══════════════════════════════════════╗"); - Serial.println("║ SYSTEM STATUS ║"); - Serial.println("╠═══════════════════════════════════════╣"); - Serial.printf("║ Node ID: %-28s ║\n", NODE_ID); - Serial.printf("║ Uptime: %-29lu ║\n", millis() / 1000); - Serial.printf("║ Packets Sent: %-22d ║\n", packetCount); - Serial.println("╠═══════════════════════════════════════╣"); - Serial.printf("║ DHT11: %-27s ║\n", dhtReady ? "✓ READY" : "✗ ERROR"); - Serial.printf("║ Audio: %-27s ║\n", audioReady ? "✓ READY" : "✗ ERROR"); - Serial.printf("║ LoRa: %-27s ║\n", loraReady ? "✓ READY" : "✗ ERROR"); - Serial.println("╠═══════════════════════════════════════╣"); - Serial.printf("║ MQ2 Calibrated: %-18s ║\n", mq2_cal.calibrated ? "Yes" : "No"); - Serial.printf("║ MQ9 Calibrated: %-18s ║\n", mq9_cal.calibrated ? "Yes" : "No"); - Serial.printf("║ MQ135 Calibrated: %-18s ║\n", mq135_cal.calibrated ? "Yes" : "No"); - Serial.println("╠═══════════════════════════════════════╣"); - Serial.printf("║ Last Temp: %.2f°C%-18s ║\n", lastValidTemperature, ""); - Serial.printf("║ Last Humidity: %.2f%%%-15s ║\n", lastValidHumidity, ""); - Serial.println("╚═══════════════════════════════════════╝\n"); -} - -// ============ ORIGINAL FUNCTIONS ============ - -void checkEmergencyButton() { - static bool lastState = LOW; - static unsigned long lastChangeTime = 0; - - bool currentState = digitalRead(EMERGENCY_BUTTON_PIN); - - if (currentState != lastState && (millis() - lastChangeTime > 50)) { - lastChangeTime = millis(); - - if (currentState == HIGH) { - unsigned long now = millis(); - - if (now - lastTapTime < TAP_TIMEOUT) { - tapCount++; - } else { - tapCount = 1; - } - - lastTapTime = now; - - Serial.print("│ BUTTON TAP #"); - Serial.print(tapCount); - Serial.print(" of "); - Serial.print(REQUIRED_TAPS); - Serial.println(" │"); - - if (tapCount >= REQUIRED_TAPS) { - Serial.println("\n╔═══════════════════════════════╗"); - Serial.println("║ TRIPLE TAP DETECTED! ║"); - Serial.println("║ EMERGENCY TRIGGERED! ║"); - Serial.println("╚═══════════════════════════════╝\n"); - - emergencyTriggered = true; - tapCount = 0; - } - } - } - - if (millis() - lastTapTime > TAP_TIMEOUT && tapCount > 0 && tapCount < REQUIRED_TAPS) { - tapCount = 0; - } - - lastState = currentState; -} - -void handleEmergency() { - Serial.println("\n████████████████████████████████████████"); - Serial.println("████ EMERGENCY MODE ACTIVATED ████"); - Serial.println("████████████████████████████████████████\n"); - - SensorData data = readAllSensors(); - data.emergency = true; - - Serial.println("=== EMERGENCY SENSOR SNAPSHOT ==="); - Serial.printf("Temperature: %.2f°C\n", data.temperature); - Serial.printf("Humidity: %.2f%%\n", data.humidity); - Serial.printf("MQ2: %d\n", data.mq2_analog); - Serial.printf("MQ9: %d\n", data.mq9_analog); - Serial.printf("MQ135: %d\n", data.mq135_analog); - Serial.println("=================================\n"); - - if (loraReady) { - Serial.println(">>> SENDING EMERGENCY LORA PACKET <<<"); - sendLoRaData(data); - Serial.println(">>> EMERGENCY PACKET SENT <<<\n"); - } else { - Serial.println("ERROR: LoRa not ready!"); - } - - Serial.println("████████████████████████████████████████"); - Serial.println("████ EMERGENCY HANDLING COMPLETE ████"); - Serial.println("████████████████████████████████████████\n"); - - delay(1000); -} - -void sendCommand(byte cmd, byte param1, byte param2, bool feedback) { - byte packet[10]; - - packet[0] = FRAME_START; - packet[1] = VERSION; - packet[2] = 0x06; - packet[3] = cmd; - packet[4] = feedback ? WITH_FEEDBACK : NO_FEEDBACK; - packet[5] = param1; - packet[6] = param2; - - uint16_t sum = packet[1] + packet[2] + packet[3] + packet[4] + packet[5] + packet[6]; - uint16_t checksum = 0xFFFF - sum + 1; - - packet[7] = (checksum >> 8) & 0xFF; - packet[8] = checksum & 0xFF; - packet[9] = FRAME_END; - - fnM16pSerial.write(packet, 10); -} - -void setVolume(int volume) { - if (volume < 0) volume = 0; - if (volume > 30) volume = 30; - sendCommand(CMD_VOLUME, 0x00, volume, false); -} - -void playAudioFile(int fileNumber) { - if (fileNumber < 1 || fileNumber > 8) return; - sendCommand(CMD_PLAY_TRACK, 0x00, fileNumber, false); - delay(100); -} - -void stopAudio() { - sendCommand(CMD_STOP, 0x00, 0x00, false); -} - -void calibrateSensors() { - Serial.println("Starting sensor calibration..."); - delay(2000); - - calibrateSensor(MQ2_ANALOG_PIN, &mq2_cal, "MQ2", MQ2_DANGER_THRESHOLD); - calibrateSensor(MQ9_ANALOG_PIN, &mq9_cal, "MQ9", MQ9_DANGER_THRESHOLD); - calibrateSensor(MQ135_ANALOG_PIN, &mq135_cal, "MQ135", MQ135_DANGER_THRESHOLD); - - Serial.println("✓ Calibration completed!"); - Serial.printf(" MQ2: Baseline=%.1f, Danger=%.1f\n", mq2_cal.baseline, mq2_cal.dangerThreshold); - Serial.printf(" MQ9: Baseline=%.1f, Danger=%.1f\n", mq9_cal.baseline, mq9_cal.dangerThreshold); - Serial.printf(" MQ135: Baseline=%.1f, Danger=%.1f\n", mq135_cal.baseline, mq135_cal.dangerThreshold); -} - -void calibrateSensor(int pin, SensorCalibration* cal, String sensorName, int minThreshold) { - Serial.print("Calibrating " + sensorName + "..."); - float sum = 0; - - for (int i = 0; i < CALIBRATION_SAMPLES; i++) { - sum += analogRead(pin); - Serial.print("."); - delay(CALIBRATION_DELAY); - } - - cal->baseline = sum / CALIBRATION_SAMPLES; - float calculatedThreshold = cal->baseline * DANGER_MULTIPLIER; - cal->dangerThreshold = (calculatedThreshold > minThreshold) ? calculatedThreshold : minThreshold; - cal->calibrated = true; - Serial.println(" Done"); -} - -bool checkSensorDanger(int currentValue, SensorCalibration* cal, int staticDangerThreshold) { - if (!cal->calibrated) { - return currentValue > staticDangerThreshold; - } - return currentValue > cal->dangerThreshold; -} - -SensorData readAllSensors() { - SensorData data; - data.timestamp = millis(); - - // Read DHT11 with interval check - if (millis() - lastDHTReading > DHT_READING_INTERVAL) { - float temp = dht.readTemperature(); - float hum = dht.readHumidity(); - - if (!isnan(temp) && temp >= -40 && temp <= 80) { - data.temperature = temp; - lastValidTemperature = temp; - } else { - data.temperature = lastValidTemperature; - } - - if (!isnan(hum) && hum >= 0 && hum <= 100) { - data.humidity = hum; - lastValidHumidity = hum; - } else { - data.humidity = lastValidHumidity; - } - - lastDHTReading = millis(); - } else { - data.temperature = lastValidTemperature; - data.humidity = lastValidHumidity; - } - - // Read MQ sensors - data.mq2_analog = analogRead(MQ2_ANALOG_PIN); - data.mq9_analog = analogRead(MQ9_ANALOG_PIN); - data.mq135_analog = analogRead(MQ135_ANALOG_PIN); - - data.mq2_digital = digitalRead(MQ2_DIGITAL_PIN) == LOW; - data.mq9_digital = digitalRead(MQ9_DIGITAL_PIN) == LOW; - data.mq135_digital = digitalRead(MQ135_DIGITAL_PIN) == LOW; - - return data; -} - -void displayReadings(SensorData data) { - Serial.println("=== SENSOR READINGS ==="); - Serial.println("Timestamp: " + String(data.timestamp)); - - Serial.println("DHT11:"); - Serial.printf(" Temperature: %.2f°C\n", data.temperature); - Serial.printf(" Humidity: %.2f%%\n", data.humidity); - - Serial.println("MQ2 (Smoke/LPG/Gas):"); - Serial.printf(" Digital: %s | Analog: %d", - data.mq2_digital ? "GAS DETECTED" : "No Gas", data.mq2_analog); - Serial.println(checkSensorDanger(data.mq2_analog, &mq2_cal, MQ2_DANGER_THRESHOLD) ? " [DANGER]" : " [Safe]"); - - Serial.println("MQ9 (Carbon Monoxide):"); - Serial.printf(" Digital: %s | Analog: %d", - data.mq9_digital ? "CO DETECTED" : "No CO", data.mq9_analog); - Serial.println(checkSensorDanger(data.mq9_analog, &mq9_cal, MQ9_DANGER_THRESHOLD) ? " [DANGER]" : " [Safe]"); - - Serial.println("MQ135 (Air Quality):"); - Serial.printf(" Digital: %s | Analog: %d", - data.mq135_digital ? "POOR AIR" : "Good Air", data.mq135_analog); - Serial.println(checkSensorDanger(data.mq135_analog, &mq135_cal, MQ135_DANGER_THRESHOLD) ? " [DANGER]" : " [Safe]"); -} - -void checkAlerts(SensorData data) { - static unsigned long lastAlert = 0; - unsigned long now = millis(); - - if (now - lastAlert < 60000) return; - - if (checkSensorDanger(data.mq2_analog, &mq2_cal, MQ2_DANGER_THRESHOLD)) { - playAudioFile(SMOKE_ALERT); - lastAlert = now; - } - else if (checkSensorDanger(data.mq9_analog, &mq9_cal, MQ9_DANGER_THRESHOLD)) { - playAudioFile(CO_ALERT); - lastAlert = now; - } - else if (checkSensorDanger(data.mq135_analog, &mq135_cal, MQ135_DANGER_THRESHOLD)) { - playAudioFile(AIR_QUALITY_WARNING); - lastAlert = now; - } - else if (data.temperature > 45.0) { - playAudioFile(HIGH_TEMP_ALERT); - lastAlert = now; - } - else if (data.temperature < 0.0) { - playAudioFile(LOW_TEMP_ALERT); - lastAlert = now; - } - else if (data.humidity > 90.0) { - playAudioFile(HIGH_HUMIDITY_ALERT); - lastAlert = now; - } - else if (data.humidity < 10.0) { - playAudioFile(LOW_HUMIDITY_ALERT); - lastAlert = now; - } -} - -void sendLoRaData(SensorData data) { - packetCount++; - - StaticJsonDocument<300> doc; - - doc["nodeId"] = NODE_ID; - doc["packetCount"] = packetCount; - doc["timestamp"] = data.timestamp; - - doc["temperature"] = data.temperature; - doc["humidity"] = data.humidity; - doc["mq2_analog"] = data.mq2_analog; - doc["mq9_analog"] = data.mq9_analog; - doc["mq135_analog"] = data.mq135_analog; - doc["mq2_digital"] = data.mq2_digital; - doc["mq9_digital"] = data.mq9_digital; - doc["mq135_digital"] = data.mq135_digital; - doc["air_quality"] = getAirQualityRating(data.mq135_analog); - doc["emergency"] = data.emergency; - - String jsonString; - serializeJson(doc, jsonString); - - if (data.emergency) { - Serial.println("\n╔════════════════════════════════════╗"); - Serial.println("║ EMERGENCY LoRa TRANSMISSION ║"); - Serial.println("╚════════════════════════════════════╝"); - } - - Serial.print("LoRa Packet #"); - Serial.print(packetCount); - Serial.println(data.emergency ? " [EMERGENCY]" : " [NORMAL]"); - Serial.print("Payload size: "); - Serial.print(jsonString.length()); - Serial.println(" bytes"); - Serial.println("Payload: " + jsonString); - - LoRa.beginPacket(); - LoRa.print(jsonString); - LoRa.endPacket(); - - if (data.emergency) { - Serial.println("╚════════════════════════════════════╝"); - Serial.println("║ EMERGENCY PACKET SENT ║"); - Serial.println("╚════════════════════════════════════╝\n"); - } else { - Serial.println("✓ Packet sent!\n"); - } -} - -String getAirQualityRating(int value) { - if (value < 800) return "Excellent"; - else if (value < 1200) return "Good"; - else if (value < 1800) return "Moderate"; - else if (value < 2400) return "Poor"; - else return "Very Poor"; -} \ No newline at end of file diff --git a/fxiWristband.ino b/fxiWristband.ino deleted file mode 100644 index 12027e3..0000000 --- a/fxiWristband.ino +++ /dev/null @@ -1,159 +0,0 @@ -// ========================= REPLACE YOUR setup() FUNCTION WITH THIS ========================= -void setup() { - Serial.begin(115200); - while(!Serial) { delay(10); } - delay(200); - - Serial.println("\n\nMAX30102 + OLED Heart Rate Monitor + ESP-NOW (fixed peer.ifidx)"); - - // CRITICAL FIX: Initialize WiFi BEFORE setting channel - WiFi.mode(WIFI_STA); - WiFi.disconnect(false); // Changed from true to false - delay(100); // Give WiFi time to initialize - - // Start WiFi explicitly - WiFi.begin(); - delay(100); - - // Set WiFi channel explicitly for ESP-NOW to match edge node (both must use same channel) - int channel = 1; - esp_err_t cherr = esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE); - if (cherr == ESP_OK) { - Serial.printf("✓ WiFi channel set to %d for ESP-NOW\n", channel); - } else { - Serial.printf("⚠ Failed to set WiFi channel (%d) err=%d\n", channel, (int)cherr); - // Continue anyway - ESP-NOW will use default channel - } - - // Initialize ESP-NOW AFTER WiFi is ready - initESPNOW(); - - // Give ESP-NOW time to fully initialize - delay(200); - - Wire.begin(SDA_PIN, SCL_PIN); - Wire.setClock(400000); - delay(500); - - Serial.println("\n=== I2C Device Scan ==="); - scanBus(); - Serial.println("Note: 0x3C=OLED, 0x57=MAX30102 (both correct)"); - delay(500); - - // Initialize OLED - if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { - Serial.println("ERROR: OLED not found!"); - while(1) { Serial.println("OLED Error - Check wiring"); delay(2000); } - } - - display.clearDisplay(); - display.setTextSize(1); - display.setTextColor(SSD1306_WHITE); - display.setCursor(0, 0); - display.println("Initializing MAX..."); - display.display(); - delay(500); - - // Initialize MAX30102 - if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) { - Serial.println("ERROR: MAX30102 not found!"); - display.clearDisplay(); - display.setTextSize(1); - display.setCursor(0, 0); - display.println("MAX30102 Error!"); - display.setCursor(0, 10); - display.println("Check wiring"); - display.display(); - while(1) { - Serial.println("MAX30102 Error - Check wiring"); - delay(2000); - } - } - - particleSensor.setup(); - particleSensor.setPulseAmplitudeRed(0x1F); - particleSensor.setPulseAmplitudeGreen(0); - particleSensor.setPulseAmplitudeIR(0x33); - - display.clearDisplay(); - display.setTextSize(1); - display.setCursor(0, 0); - display.println("Heart Rate Monitor"); - display.setCursor(0, 15); - display.println("Place finger on sensor"); - display.setCursor(0, 30); - display.println("and keep it steady..."); - display.display(); - - delay(3000); - - lastVitalsSent = millis() - (VITALS_INTERVAL - 5000); // send first vitals soon, 5s before LoRa typical - - Serial.println("\n✓ Wristband ready - waiting for edge node connection..."); -} - - -// ========================= REPLACE YOUR initESPNOW() FUNCTION WITH THIS ========================= -void initESPNOW() { - // Ensure WiFi is started first - if (WiFi.status() == WL_NO_SHIELD) { - Serial.println("⚠ WiFi not ready, attempting to start..."); - WiFi.mode(WIFI_STA); - delay(100); - } - - if (esp_now_init() != ESP_OK) { - Serial.println("⚠ ESP-NOW init failed - retrying..."); - delay(500); - // Try once more - if (esp_now_init() != ESP_OK) { - Serial.println("✗ ESP-NOW init failed after retry"); - espnowReady = false; - return; - } - } - espnowReady = true; - Serial.println("✓ ESP-NOW initialized (wristband)"); - - // Register callbacks - use signatures for ESP32 Arduino Core 3.3.2 - esp_now_register_send_cb(onDataSent); - esp_now_register_recv_cb(onDataRecv); - - // Small delay before adding peer - delay(100); - - // Register peer (edge node) and bind to channel 1 and STA interface - esp_now_peer_info_t peerInfo; - memset(&peerInfo, 0, sizeof(peerInfo)); - memcpy(peerInfo.peer_addr, edgeNodeMac, 6); - peerInfo.channel = 1; // Must match edge node - - // Set interface index robustly; use macros when available otherwise cast - #if defined(ESP_IF_WIFI_STA) - peerInfo.ifidx = ESP_IF_WIFI_STA; - #elif defined(WIFI_IF_STA) - peerInfo.ifidx = WIFI_IF_STA; - #else - peerInfo.ifidx = (wifi_interface_t)0; // cast fallback - #endif - - peerInfo.encrypt = false; - - if (!esp_now_is_peer_exist(edgeNodeMac)) { - esp_err_t addStatus = esp_now_add_peer(&peerInfo); - if (addStatus != ESP_OK) { - Serial.printf("⚠ Failed to add ESP-NOW peer (edge node) - error: %d\n", addStatus); - } else { - Serial.println("✓ Edge node ESP-NOW peer added"); - // Print the MAC address we added - Serial.printf(" Peer MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", - edgeNodeMac[0], edgeNodeMac[1], edgeNodeMac[2], - edgeNodeMac[3], edgeNodeMac[4], edgeNodeMac[5]); - } - } else { - Serial.println("✓ Edge node peer already exists"); - } - - // Final delay to ensure everything is ready - delay(100); -} diff --git a/IshitaKaGhr/LoRaAttempt/centralbitch.ino b/hardware/central_node.ino similarity index 100% rename from IshitaKaGhr/LoRaAttempt/centralbitch.ino rename to hardware/central_node.ino diff --git a/IshitaKaGhr/LoRaAttempt/EdgeNodeKiAmma.ino b/hardware/edge_node.ino similarity index 100% rename from IshitaKaGhr/LoRaAttempt/EdgeNodeKiAmma.ino rename to hardware/edge_node.ino diff --git a/IshitaKaGhr/watch.ino b/hardware/watch.ino similarity index 100% rename from IshitaKaGhr/watch.ino rename to hardware/watch.ino diff --git a/hardware_specs.txt b/hardware_specs.txt deleted file mode 100644 index 61e5ec0..0000000 --- a/hardware_specs.txt +++ /dev/null @@ -1,14 +0,0 @@ -ESP32 Arduino Core Version: 3.3.2 - Board: ESP32 DEV Module - Target Platform: Arduino IDE 2.x - Libraries Used: - - Wire (I2C) - - SPI - - LoRa 0.8.0 - - ArduinoJson 7.4.2 - - DHT sensor library 1.4.6 - - WiFi (ESP32 built-in) - - ESP-NOW (ESP32 built-in) - - Adafruit_SSD1306 - - MAX30105 - - SparkFun heartRate library diff --git a/issue #3 Fix.cpp b/issue #3 Fix.cpp deleted file mode 100644 index f9dda7b..0000000 --- a/issue #3 Fix.cpp +++ /dev/null @@ -1,1040 +0,0 @@ -/* - ESP32 Multi-Sensor System with Emergency Button + MPU6050 (Clone WHO_AM_I fix) - - Preserves your pin layout and calibration logic (DANGER_MULTIPLIER = 2.0) - - Accepts MPU6050 WHO_AM_I values 0x68, 0x69, and 0x72 (common clones) - - If WHO_AM_I == 0x00 it will attempt bus recovery and a safe fallback: - * try re-read WHO_AM_I after recovery - * if still 0x00, attempt to write power-up and read accelerometer registers - — if accel data looks plausible proceed (sensor likely responding despite ID) - * otherwise disable MPU features and provide clear diagnostics - - Robust I2C read/write with retries and bus recovery to avoid "I2C software timeout" spam - - Keeps original JSON payload format for LoRa - - Keeps original calibration logic & DHT -30% manual humidity correction as requested - - Pin layout preserved exactly as you provided -*/ - -#include -#include -#include -#include -#include -#include -#include - -// ========================= MPU6050 I2C Register Definitions ========================= -#define MPU6050_ADDR 0x68 -#define PWR_MGMT_1 0x6B -#define ACCEL_XOUT_H 0x3B -#define GYRO_XOUT_H 0x43 -#define CONFIG 0x1A -#define GYRO_CONFIG 0x1B -#define ACCEL_CONFIG 0x1C -#define WHO_AM_I 0x75 - -// ========================= Pin definitions (preserved) ========================= -// MQ sensors -#define MQ2_DIGITAL_PIN 27 // ✔️ Safe -#define MQ2_ANALOG_PIN 32 // ✔️ ADC1_CH4 -#define MQ9_DIGITAL_PIN 14 // ✔️ Safe -#define MQ9_ANALOG_PIN 33 // ✔️ ADC1_CH5 -#define MQ135_DIGITAL_PIN 13 // ✔️ Safe -#define MQ135_ANALOG_PIN 35 // ✔️ ADC1_CH7 - -// FN-M16P Audio Module pins (UART2) -#define FN_M16P_RX 16 // ✔️ UART2 RX -#define FN_M16P_TX 17 // ✔️ UART2 TX - -// DHT11 pin -#define DHT11_PIN 25 // ✔️ GPIO25 (manual humidity correction applied) -#define DHT_TYPE DHT11 - -// MPU6050 pins (I2C) -#define MPU6050_SDA 21 // ✔️ I2C SDA -#define MPU6050_SCL 22 // ✔️ I2C SCL -#define MPU6050_INT 34 // ✔️ Input-only - -// LoRa Module pins (SPI) -#define LORA_SCK 18 // ✔️ SPI SCK -#define LORA_MISO 19 // ✔️ SPI MISO -#define LORA_MOSI 23 // ✔️ SPI MOSI -#define LORA_SS 5 // ⚠️ Needs pull-up resistor on some boards -#define LORA_RST 4 // ✔️ Safe -#define LORA_DIO0 26 // ✔️ Safe - -// EMERGENCY BUTTON PIN -#define EMERGENCY_BUTTON_PIN 15 // ✔️ GPIO15; use INPUT_PULLUP (pressed = LOW) - -// LoRa frequency -#define LORA_BAND 915E6 - -// ========================= System constants ========================= -#define NODE_ID "001" - -#define MQ2_DANGER_THRESHOLD 1600 -#define MQ9_DANGER_THRESHOLD 3800 -#define MQ135_DANGER_THRESHOLD 1800 - -#define FALLBACK_TEMPERATURE 27.0 -#define FALLBACK_HUMIDITY 47.0 - -#define CALIBRATION_SAMPLES 10 -#define DANGER_MULTIPLIER 2.0 -#define CALIBRATION_DELAY 2000 - -const int TAP_TIMEOUT = 600; -const int REQUIRED_TAPS = 3; - -// FN-M16P commands -const byte FRAME_START = 0x7E; -const byte FRAME_END = 0xEF; -const byte VERSION = 0xFF; -const byte NO_FEEDBACK = 0x00; -const byte WITH_FEEDBACK = 0x01; -const byte CMD_PLAY_TRACK = 0x03; -const byte CMD_VOLUME = 0x06; -const byte CMD_STOP = 0x16; - -// Audio files mapping -enum AudioFiles { - BOOT_AUDIO = 1, - SMOKE_ALERT = 2, - CO_ALERT = 3, - AIR_QUALITY_WARNING = 4, - HIGH_TEMP_ALERT = 5, - LOW_TEMP_ALERT = 6, - HIGH_HUMIDITY_ALERT = 7, - LOW_HUMIDITY_ALERT = 8, - FALL_DETECTED = 9, - MOTION_ALERT = 10 -}; - -// ========================= Types & Globals ========================= -struct SensorCalibration { - float baseline; - float dangerThreshold; - bool calibrated; -}; - -struct MotionData { - float totalAccel; // m/s^2 - float totalGyro; // deg/s - bool fallDetected; - bool impactDetected; - bool motionDetected; - unsigned long lastMotionTime; -}; - -struct SensorData { - float temperature; - float humidity; - int mq2_analog; - int mq9_analog; - int mq135_analog; - bool mq2_digital; - bool mq9_digital; - bool mq135_digital; - bool emergency; - unsigned long timestamp; - MotionData motion; -}; - -SensorCalibration mq2_cal = {0,0,false}; -SensorCalibration mq9_cal = {0,0,false}; -SensorCalibration mq135_cal = {0,0,false}; - -DHT dht(DHT11_PIN, DHT_TYPE); -HardwareSerial fnM16pSerial(2); - -bool audioReady = false; -bool loraReady = false; -bool dhtReady = false; -bool mpuReady = false; - -unsigned long lastLoRaSend = 0; -int loraInterval = 30000; -int packetCount = 0; - -unsigned long lastDHTReading = 0; -const unsigned long DHT_READING_INTERVAL = 2000; -float lastValidTemperature = FALLBACK_TEMPERATURE; -float lastValidHumidity = FALLBACK_HUMIDITY; - -// Emergency Button Variables -volatile int tapCount = 0; -volatile unsigned long lastTapTime = 0; -volatile bool emergencyTriggered = false; - -// Motion variables -MotionData motionData = {0}; - -// MPU detection / fall detector internals -const float G = 9.80665f; -const float FREE_FALL_G_THRESHOLD = 0.6f; // g -const unsigned long FREE_FALL_MIN_MS = 120; // ms -const float IMPACT_G_THRESHOLD = 3.5f; // g -const unsigned long IMPACT_WINDOW_MS = 1200; // ms -const unsigned long STATIONARY_CONFIRM_MS = 800; // ms -const float ROTATION_IMPACT_THRESHOLD = 400.0f; // deg/s - -bool inFreeFall = false; -bool fallInProgress = false; -bool impactSeen = false; -unsigned long freeFallStart = 0; -unsigned long fallStartTime = 0; -unsigned long impactTime = 0; -unsigned long stationarySince = 0; -float accelFiltered = G; -const float ALPHA = 0.85f; - -unsigned long lastI2CAttempt = 0; - -// ========================= Forward declarations ========================= -void printTestMenu(); -void handleTestCommand(String cmd); -void testDHT(); -void testMQ2(); -void testMQ9(); -void testMQ135(); -void testAllMQ(); -void testAudio(int fileNum); -void testLoRa(); -void testEmergency(); -void testButton(); -void testAllSensors(); -void setVolume(int volume); -void playAudioFile(int fileNumber); -void stopAudio(); -void calibrateSensors(); -void calibrateSensor(int pin, SensorCalibration* cal, String sensorName, int minThreshold); -bool checkSensorDanger(int currentValue, SensorCalibration* cal, int staticDangerThreshold); -SensorData readAllSensors(); -void displayReadings(SensorData data); -void checkAlerts(SensorData data); -void sendLoRaData(SensorData data); -String getAirQualityRating(int value); -void checkEmergencyButton(); -void handleEmergency(); -void sendCommand(byte cmd, byte param1, byte param2, bool feedback); - -// I2C helpers & MPU -bool i2cBusRecover(); -bool safeWireRequest(uint8_t addr, uint8_t reg, uint8_t *buf, size_t len, int retries=3); -bool safeWireWrite(uint8_t addr, uint8_t reg, uint8_t val, int retries=3); -bool initMPU6050(); -void readMPU6050Data(int16_t *ax, int16_t *ay, int16_t *az, int16_t *gx, int16_t *gy, int16_t *gz); -void monitorMotion(); -void detectFallAndHandle(); - -// ========================= I2C helpers & bus recovery ========================= -// Attempt to unstuck a bus by toggling SCL. Returns true if SDA released. -bool i2cBusRecover() { - Wire.end(); - pinMode(MPU6050_SCL, OUTPUT); - pinMode(MPU6050_SDA, INPUT_PULLUP); - // If SDA already high, bus free - if (digitalRead(MPU6050_SDA) == HIGH) { - Wire.begin(MPU6050_SDA, MPU6050_SCL); - Wire.setClock(400000); - return true; - } - // Pulse clock up to 9 times - for (int i=0;i<9;i++) { - digitalWrite(MPU6050_SCL, HIGH); - delayMicroseconds(300); - digitalWrite(MPU6050_SCL, LOW); - delayMicroseconds(300); - if (digitalRead(MPU6050_SDA) == HIGH) break; - } - // Generate STOP - pinMode(MPU6050_SDA, OUTPUT); - digitalWrite(MPU6050_SDA, LOW); - delayMicroseconds(100); - digitalWrite(MPU6050_SCL, HIGH); - delayMicroseconds(100); - digitalWrite(MPU6050_SDA, HIGH); - delayMicroseconds(100); - Wire.begin(MPU6050_SDA, MPU6050_SCL); - Wire.setClock(400000); - pinMode(MPU6050_SDA, INPUT_PULLUP); - return digitalRead(MPU6050_SDA) == HIGH; -} - -// robust read with retries + recovery -bool safeWireRequest(uint8_t addr, uint8_t reg, uint8_t *buf, size_t len, int retries) { - for (int attempt=0; attempt 4096 LSB/g and gyro ±500 dps -> 65.5 LSB/°/s - float ax = (axr / 4096.0f) * G; - float ay = (ayr / 4096.0f) * G; - float az = (azr / 4096.0f) * G; - float gx = (gxr / 65.5f); - float gy = (gyr / 65.5f); - float gz = (gzr / 65.5f); - - motionData.totalAccel = sqrt(ax*ax + ay*ay + az*az); - motionData.totalGyro = sqrt(gx*gx + gy*gy + gz*gz); - - // smoothing - accelFiltered = ALPHA * accelFiltered + (1.0f - ALPHA) * motionData.totalAccel; - - if (fabs(motionData.totalAccel - accelFiltered) > 0.2f * G || motionData.totalGyro > 25.0f) { - motionData.lastMotionTime = millis(); - motionData.motionDetected = true; - } else { - motionData.motionDetected = false; - } - - detectFallAndHandle(); -} - -void detectFallAndHandle() { - unsigned long now = millis(); - float totG = motionData.totalAccel / G; - float totGyro = motionData.totalGyro; - - // free-fall detection - if (totG < FREE_FALL_G_THRESHOLD) { - if (!inFreeFall) { inFreeFall = true; freeFallStart = now; } - else if ((now - freeFallStart) >= FREE_FALL_MIN_MS && !fallInProgress) { - fallInProgress = true; - fallStartTime = now; - impactSeen = false; - motionData.impactDetected = false; - } - } else { - if (inFreeFall) inFreeFall = false; - } - - // impact detection - if (fallInProgress && !impactSeen) { - if (totG >= IMPACT_G_THRESHOLD || totGyro >= ROTATION_IMPACT_THRESHOLD) { - impactSeen = true; - impactTime = now; - motionData.impactDetected = true; - } else if (now - fallStartTime > IMPACT_WINDOW_MS) { - // timed out without impact - fallInProgress = false; - impactSeen = false; - motionData.impactDetected = false; - } - } - - // confirm fall if impactSeen and post-impact stationary - if (impactSeen) { - float accelVariationG = fabs((motionData.totalAccel / G) - 1.0f); - if (accelVariationG < 0.35f && motionData.totalGyro < 50.0f) { - if (stationarySince == 0) stationarySince = now; - if (now - stationarySince >= STATIONARY_CONFIRM_MS) { - motionData.fallDetected = true; - emergencyTriggered = true; - if (audioReady) playAudioFile(FALL_DETECTED); - Serial.println("\n╔════════════════════════════════════╗"); - Serial.println("║ FALL CONFIRMED - IMPACT + STATIONARY ║"); - Serial.println("╚════════════════════════════════════╝"); - Serial.printf("Acceleration: %.2f g\n", motionData.totalAccel / G); - Serial.printf("Gyroscope: %.2f °/s\n", motionData.totalGyro); - // reset - fallInProgress = false; - impactSeen = false; - stationarySince = 0; - } - } else { - stationarySince = 0; - if (now - impactTime > IMPACT_WINDOW_MS) { - fallInProgress = false; impactSeen = false; stationarySince = 0; motionData.impactDetected = false; - } - } - } - - // immediate large spike detection (conservative) - static float lastTotalAccel = G; - static float lastTotalGyro = 0.0f; - float accelDeltaG = fabs((motionData.totalAccel - lastTotalAccel) / G); - float gyroDelta = fabs(motionData.totalGyro - lastTotalGyro); - - if (!motionData.fallDetected) { - if (accelDeltaG > 2.5f && (motionData.totalAccel / G) > 2.0f) { - // short confirmation window (non-blocking ideally, but keep short) - unsigned long t0 = millis(); - bool remainedStationary = true; - while (millis() - t0 < STATIONARY_CONFIRM_MS) { - // rely on monitorMotion() being called frequently in loop(); if motion remains low then confirm - if (motionData.motionDetected) { remainedStationary = false; break; } - delay(40); - } - if (remainedStationary) { - motionData.fallDetected = true; - emergencyTriggered = true; - if (audioReady) playAudioFile(FALL_DETECTED); - Serial.println("\n╔════════════════════════════════════╗"); - Serial.println("║ FALL CONFIRMED - SUDDEN IMPACT ║"); - Serial.println("╚════════════════════════════════════╝"); - } - } else if (gyroDelta > 300.0f && motionData.totalGyro > 400.0f) { - unsigned long t0 = millis(); - bool remainedStationary = true; - while (millis() - t0 < STATIONARY_CONFIRM_MS) { - if (motionData.motionDetected) { remainedStationary = false; break; } - delay(40); - } - if (remainedStationary) { - motionData.fallDetected = true; - emergencyTriggered = true; - if (audioReady) playAudioFile(FALL_DETECTED); - Serial.println("\n╔════════════════════════════════════╗"); - Serial.println("║ FALL CONFIRMED - ROTATION SPIKE ║"); - Serial.println("╚════════════════════════════════════╝"); - } - } - } - - lastTotalAccel = motionData.totalAccel; - lastTotalGyro = motionData.totalGyro; -} - -// ========================= Setup & Loop ========================= -void setup() { - Serial.begin(115200); - delay(800); - Serial.println("\n\n================================="); - Serial.println("ESP32 Multi-Sensor System - MPU6050 Clone WHO_AM_I Fix"); - Serial.println("================================="); - - // Initialize I2C (preserved pins) - Wire.begin(MPU6050_SDA, MPU6050_SCL); - Wire.setClock(400000); - - // MQ digital pins - pinMode(MQ2_DIGITAL_PIN, INPUT); - pinMode(MQ9_DIGITAL_PIN, INPUT); - pinMode(MQ135_DIGITAL_PIN, INPUT); - - // Emergency button -> use internal pull-up (pressed == LOW) - pinMode(EMERGENCY_BUTTON_PIN, INPUT_PULLUP); - Serial.printf("Emergency button: GPIO%d (INPUT_PULLUP). Press => LOW\n", EMERGENCY_BUTTON_PIN); - - // MPU INT pin - pinMode(MPU6050_INT, INPUT); - - Serial.println("Initializing MPU6050..."); - if (!initMPU6050()) { - Serial.println("⚠ MPU6050 init failed. Motion features will be disabled until I2C/wiring fixed."); - mpuReady = false; - } else { - Serial.println("✓ MPU6050 initialized (clone WHO_AM_I accepted if applicable)."); - mpuReady = true; - motionData.lastMotionTime = millis(); - accelFiltered = G; - } - - // DHT11 initialization (preserved pin 25) and manual humidity correction -30% - Serial.println("\nInitializing DHT11 sensor..."); - dht.begin(); - delay(1500); - bool dhtWorking = false; - for (int i=0;i<3;i++) { - float t = dht.readTemperature(); - float h = dht.readHumidity(); - if (!isnan(t) && !isnan(h)) { - dhtWorking = true; - dhtReady = true; - lastValidTemperature = t; - float corr = h - 30.0f; - if (corr < 0.0f) corr = 0.0f; - if (corr > 100.0f) corr = 100.0f; - lastValidHumidity = corr; - Serial.printf("✓ DHT11: Temp %.1fC Humidity (corrected) %.1f%%\n", lastValidTemperature, lastValidHumidity); - break; - } - delay(1000); - } - if (!dhtWorking) { - Serial.println("⚠ DHT11 ERROR - Check DATA wiring to GPIO25 and power."); - dhtReady = false; - } - - // Audio init - fnM16pSerial.begin(9600, SERIAL_8N1, FN_M16P_RX, FN_M16P_TX); - delay(200); - setVolume(30); - audioReady = true; - Serial.println("✓ Audio module initialized."); - - // LoRa init with SS high & RST pulse - Serial.println("Initializing LoRa..."); - pinMode(LORA_SS, OUTPUT); - digitalWrite(LORA_SS, HIGH); - pinMode(LORA_RST, OUTPUT); - digitalWrite(LORA_RST, HIGH); - SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_SS); - LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0); - digitalWrite(LORA_RST, LOW); delay(10); - digitalWrite(LORA_RST, HIGH); delay(10); - if (!LoRa.begin(LORA_BAND)) { - Serial.println("⚠ LoRa initialization failed. Check wiring/antenna."); - loraReady = false; - } else { - loraReady = true; - Serial.println("✓ LoRa initialized."); - } - - Serial.println("Warming gas sensors (15s)..."); - delay(15000); - - Serial.println("Calibrating MQ sensors..."); - calibrateSensors(); - - Serial.println("\nSYSTEM READY."); - printTestMenu(); - - if (audioReady) { playAudioFile(BOOT_AUDIO); delay(1000); } -} - -void loop() { - // serial test commands - if (Serial.available() > 0) { - String cmd = Serial.readStringUntil('\n'); - cmd.trim(); - cmd.toLowerCase(); - handleTestCommand(cmd); - } - - // emergency button - checkEmergencyButton(); - - // monitor motion - if (mpuReady) monitorMotion(); - - // emergency handling - if (emergencyTriggered) { - handleEmergency(); - emergencyTriggered = false; - } - - // periodic snapshot and LoRa send - static unsigned long lastNormal = 0; - if (millis() - lastNormal >= 10000) { - lastNormal = millis(); - SensorData data = readAllSensors(); - data.emergency = false; - data.motion = motionData; - displayReadings(data); - checkAlerts(data); - if (loraReady && (millis() - lastLoRaSend > loraInterval)) { - sendLoRaData(data); - lastLoRaSend = millis(); - } - Serial.println("------------------------"); - } - - delay(50); -} - -// ========================= Test commands & helpers ========================= - -void printTestMenu() { - Serial.println("\n╔═══════════════════════════════════════╗"); - Serial.println("║ TEST COMMANDS MENU ║"); - Serial.println("╠═══════════════════════════════════════╣"); - Serial.println("║ dht, mq2, mq9, mq135, mq, all ║"); - Serial.println("║ audio1..audio10, stop, volume+, volume-║"); - Serial.println("║ lora, button, emergency, calibrate ║"); - Serial.println("║ status, help/menu ║"); - Serial.println("╚═══════════════════════════════════════╝\n"); -} - -void handleTestCommand(String cmd) { - Serial.println("\n>>> EXECUTING TEST: " + cmd + " <<<\n"); - if (cmd == "help" || cmd == "menu") printTestMenu(); - else if (cmd == "dht") testDHT(); - else if (cmd == "mq2") testMQ2(); - else if (cmd == "mq9") testMQ9(); - else if (cmd == "mq135") testMQ135(); - else if (cmd == "mq") testAllMQ(); - else if (cmd.startsWith("audio")) { - int n = cmd.substring(5).toInt(); - if (n >= 1 && n <= 10) testAudio(n); - } - else if (cmd == "stop") { stopAudio(); Serial.println("Audio stopped."); } - else if (cmd == "volume+") { setVolume(25); Serial.println("Volume 25"); } - else if (cmd == "volume-") { setVolume(15); Serial.println("Volume 15"); } - else if (cmd == "lora") testLoRa(); - else if (cmd == "button") testButton(); - else if (cmd == "emergency") testEmergency(); - else if (cmd == "all") testAllSensors(); - else if (cmd == "calibrate") calibrateSensors(); - else if (cmd == "status") printSystemStatus(); - else Serial.println("❌ Unknown command: " + cmd); - Serial.println("\n>>> TEST COMPLETE <<<\n"); -} - -void testDHT() { - Serial.println("Testing DHT11 (5 samples) with manual -30% humidity correction:"); - for (int i=0;i<5;i++) { - float t = dht.readTemperature(); - float h = dht.readHumidity(); - if (!isnan(t) && !isnan(h)) { - float corr = h - 30.0f; - if (corr < 0) corr = 0; if (corr > 100) corr = 100; - Serial.printf(" #%d: Temp=%.2f C, Hum(corrected)=%.2f %%\n", i+1, t, corr); - } else { - Serial.printf(" #%d: FAILED (NaN)\n", i+1); - } - delay(2000); - } -} - -void testMQ2() { - Serial.println("Testing MQ2 (10 samples):"); - for (int i=0;i<10;i++) { - int a = analogRead(MQ2_ANALOG_PIN); - bool d = digitalRead(MQ2_DIGITAL_PIN) == LOW; - Serial.printf(" #%d: analog=%d digital=%s\n", i+1, a, d?"ACTIVE":"INACTIVE"); - delay(500); - } - Serial.printf("Calibration baseline=%.1f threshold=%.1f\n", mq2_cal.baseline, mq2_cal.dangerThreshold); -} - -void testMQ9() { - Serial.println("Testing MQ9 (10 samples):"); - for (int i=0;i<10;i++) { - int a = analogRead(MQ9_ANALOG_PIN); - bool d = digitalRead(MQ9_DIGITAL_PIN) == LOW; - Serial.printf(" #%d: analog=%d digital=%s\n", i+1, a, d?"ACTIVE":"INACTIVE"); - delay(500); - } - Serial.printf("Calibration baseline=%.1f threshold=%.1f\n", mq9_cal.baseline, mq9_cal.dangerThreshold); -} - -void testMQ135() { - Serial.println("Testing MQ135 (10 samples):"); - for (int i=0;i<10;i++) { - int a = analogRead(MQ135_ANALOG_PIN); - bool d = digitalRead(MQ135_DIGITAL_PIN) == LOW; - Serial.printf(" #%d: analog=%d digital=%s rating=%s\n", i+1, a, d?"POOR":"GOOD", getAirQualityRating(a).c_str()); - delay(500); - } - Serial.printf("Calibration baseline=%.1f threshold=%.1f\n", mq135_cal.baseline, mq135_cal.dangerThreshold); -} - -void testAllMQ() { testMQ2(); testMQ9(); testMQ135(); } - -void testAudio(int fileNum) { - if (!audioReady) { Serial.println("Audio not ready"); return; } - Serial.printf("Playing audio file #%d\n", fileNum); - playAudioFile(fileNum); -} - -void testLoRa() { - if (!loraReady) { Serial.println("LoRa not ready"); return; } - Serial.println("Sending test LoRa packet..."); - SensorData d = readAllSensors(); - d.emergency = false; - d.motion = motionData; - sendLoRaData(d); - Serial.println("Test LoRa sent."); -} - -void testEmergency() { - Serial.println("Triggering emergency now..."); - emergencyTriggered = true; -} - -void testButton() { - Serial.println("Testing emergency button for 10s..."); - unsigned long start = millis(); - bool last = digitalRead(EMERGENCY_BUTTON_PIN); - while (millis() - start < 10000) { - bool cur = digitalRead(EMERGENCY_BUTTON_PIN); - if (cur != last) { - Serial.printf("Button state change: %s\n", cur ? "HIGH" : "LOW"); - last = cur; - } - delay(100); - } - Serial.println("Button test complete."); -} - -void testAllSensors() { - testDHT(); - testAllMQ(); - testLoRa(); -} - -// ========================= Audio control ========================= - -void sendCommand(byte cmd, byte param1, byte param2, bool feedback) { - byte packet[10]; - packet[0] = FRAME_START; - packet[1] = VERSION; - packet[2] = 0x06; - packet[3] = cmd; - packet[4] = feedback ? WITH_FEEDBACK : NO_FEEDBACK; - packet[5] = param1; - packet[6] = param2; - uint16_t sum = packet[1] + packet[2] + packet[3] + packet[4] + packet[5] + packet[6]; - uint16_t checksum = 0xFFFF - sum + 1; - packet[7] = (checksum >> 8) & 0xFF; - packet[8] = checksum & 0xFF; - packet[9] = FRAME_END; - fnM16pSerial.write(packet, 10); -} - -void setVolume(int volume) { - if (volume < 0) volume = 0; - if (volume > 30) volume = 30; - sendCommand(CMD_VOLUME, 0x00, volume, false); -} - -void playAudioFile(int fileNumber) { - if (fileNumber < 1 || fileNumber > 10) return; - sendCommand(CMD_PLAY_TRACK, 0x00, fileNumber, false); - delay(60); -} - -void stopAudio() { - sendCommand(CMD_STOP, 0x00, 0x00, false); -} - -// ========================= Calibration ========================= - -void calibrateSensors() { - Serial.println("Starting sensor calibration..."); - delay(500); - calibrateSensor(MQ2_ANALOG_PIN, &mq2_cal, "MQ2", MQ2_DANGER_THRESHOLD); - calibrateSensor(MQ9_ANALOG_PIN, &mq9_cal, "MQ9", MQ9_DANGER_THRESHOLD); - calibrateSensor(MQ135_ANALOG_PIN, &mq135_cal, "MQ135", MQ135_DANGER_THRESHOLD); - Serial.println("Calibration completed."); -} - -void calibrateSensor(int pin, SensorCalibration* cal, String sensorName, int minThreshold) { - Serial.print("Calibrating " + sensorName + " ..."); - float sum = 0; - for (int i=0;ibaseline = sum / CALIBRATION_SAMPLES; - float calculatedThreshold = cal->baseline * DANGER_MULTIPLIER; - cal->dangerThreshold = (calculatedThreshold > minThreshold) ? calculatedThreshold : minThreshold; - cal->calibrated = true; - Serial.println(" Done"); -} - -// ========================= Sensors reading/display/alerts/LoRa ========================= - -SensorData readAllSensors() { - SensorData data; - data.timestamp = millis(); - - if (millis() - lastDHTReading > DHT_READING_INTERVAL) { - float rt = dht.readTemperature(); - float rh = dht.readHumidity(); - if (!isnan(rt) && rt >= -40 && rt <= 80) { data.temperature = rt; lastValidTemperature = rt; } - else data.temperature = lastValidTemperature; - - if (!isnan(rh) && rh >= 0 && rh <= 100) { - float corr = rh - 30.0f; - if (corr < 0.0f) corr = 0.0f; - if (corr > 100.0f) corr = 100.0f; - data.humidity = corr; - lastValidHumidity = corr; - } else data.humidity = lastValidHumidity; - - lastDHTReading = millis(); - } else { - data.temperature = lastValidTemperature; - data.humidity = lastValidHumidity; - } - - data.mq2_analog = analogRead(MQ2_ANALOG_PIN); - data.mq9_analog = analogRead(MQ9_ANALOG_PIN); - data.mq135_analog = analogRead(MQ135_ANALOG_PIN); - - data.mq2_digital = digitalRead(MQ2_DIGITAL_PIN) == LOW; - data.mq9_digital = digitalRead(MQ9_DIGITAL_PIN) == LOW; - data.mq135_digital = digitalRead(MQ135_DIGITAL_PIN) == LOW; - - data.emergency = false; - data.motion = motionData; - return data; -} - -void displayReadings(SensorData data) { - Serial.println("=== SENSOR READINGS ==="); - Serial.printf("Timestamp: %lu\n", data.timestamp); - Serial.println("DHT11:"); - Serial.printf(" Temperature: %.2f°C\n", data.temperature); - Serial.printf(" Humidity: %.2f%%\n", data.humidity); - Serial.println("MQ2 (Smoke/LPG/Gas):"); - Serial.printf(" Digital: %s | Analog: %d", data.mq2_digital ? "GAS DETECTED" : "No Gas", data.mq2_analog); - Serial.println(checkSensorDanger(data.mq2_analog, &mq2_cal, MQ2_DANGER_THRESHOLD) ? " [DANGER]" : " [Safe]"); - Serial.println("MQ9 (Carbon Monoxide):"); - Serial.printf(" Digital: %s | Analog: %d", data.mq9_digital ? "CO DETECTED" : "No CO", data.mq9_analog); - Serial.println(checkSensorDanger(data.mq9_analog, &mq9_cal, MQ9_DANGER_THRESHOLD) ? " [DANGER]" : " [Safe]"); - Serial.println("MQ135 (Air Quality):"); - Serial.printf(" Digital: %s | Analog: %d", data.mq135_digital ? "POOR AIR" : "Good Air", data.mq135_analog); - Serial.println(checkSensorDanger(data.mq135_analog, &mq135_cal, MQ135_DANGER_THRESHOLD) ? " [DANGER]" : " [Safe]"); - Serial.println("MOTION:"); - Serial.printf(" Accel: %.2f g\n", data.motion.totalAccel / G); - Serial.printf(" Gyro: %.2f °/s\n", data.motion.totalGyro); - Serial.printf(" Fall Detected: %s\n", data.motion.fallDetected ? "YES" : "NO"); -} - -void checkAlerts(SensorData data) { - static unsigned long lastAlert = 0; - unsigned long now = millis(); - if (now - lastAlert < 60000) return; - - if (data.motion.fallDetected) { - if (audioReady) playAudioFile(FALL_DETECTED); - lastAlert = now; - return; - } - if (checkSensorDanger(data.mq2_analog, &mq2_cal, MQ2_DANGER_THRESHOLD)) { - playAudioFile(SMOKE_ALERT); lastAlert = now; return; - } - if (checkSensorDanger(data.mq9_analog, &mq9_cal, MQ9_DANGER_THRESHOLD)) { - playAudioFile(CO_ALERT); lastAlert = now; return; - } - if (checkSensorDanger(data.mq135_analog, &mq135_cal, MQ135_DANGER_THRESHOLD)) { - playAudioFile(AIR_QUALITY_WARNING); lastAlert = now; return; - } - if (data.temperature > 45.0) { playAudioFile(HIGH_TEMP_ALERT); lastAlert = now; return; } - if (data.temperature < 0.0) { playAudioFile(LOW_TEMP_ALERT); lastAlert = now; return; } - if (data.humidity > 90.0) { playAudioFile(HIGH_HUMIDITY_ALERT); lastAlert = now; return; } - if (data.humidity < 10.0) { playAudioFile(LOW_HUMIDITY_ALERT); lastAlert = now; return; } -} - -void sendLoRaData(SensorData data) { - packetCount++; - StaticJsonDocument<512> doc; - doc["nodeId"] = NODE_ID; - doc["packetCount"] = packetCount; - doc["timestamp"] = data.timestamp; - doc["temperature"] = data.temperature; - doc["humidity"] = data.humidity; - doc["mq2_analog"] = data.mq2_analog; - doc["mq9_analog"] = data.mq9_analog; - doc["mq135_analog"] = data.mq135_analog; - doc["mq2_digital"] = data.mq2_digital; - doc["mq9_digital"] = data.mq9_digital; - doc["mq135_digital"] = data.mq135_digital; - doc["air_quality"] = getAirQualityRating(data.mq135_analog); - doc["emergency"] = data.emergency; - // motion - doc["fall_detected"] = data.motion.fallDetected; - doc["activity"] = data.motion.fallDetected ? String("FALLEN - HELP!") : String("Stationary"); - doc["total_accel"] = data.motion.totalAccel; - doc["total_gyro"] = data.motion.totalGyro; - doc["motion_active"] = data.motion.motionDetected; - - String jsonString; - serializeJson(doc, jsonString); - - if (data.emergency || data.motion.fallDetected) { - Serial.println("\n╔════════ EMERGENCY LoRa ═════════╗"); - if (data.motion.fallDetected) Serial.println("║ FALL DETECTED! ║"); - Serial.println("╚════════════════════════════════╝"); - } - - Serial.print("LoRa Packet #"); Serial.print(packetCount); Serial.println(data.emergency ? " [EMERGENCY]" : " [NORMAL]"); - Serial.print("Payload size: "); Serial.print(jsonString.length()); Serial.println(" bytes"); - Serial.println("Payload: " + jsonString); - - LoRa.beginPacket(); - LoRa.print(jsonString); - LoRa.endPacket(); - - Serial.println("✓ Packet sent"); -} - -String getAirQualityRating(int value) { - if (value < 800) return "Excellent"; - else if (value < 1200) return "Good"; - else if (value < 1800) return "Moderate"; - else if (value < 2400) return "Poor"; - else return "Very Poor"; -} - -// ========================= Emergency Button ========================= -void checkEmergencyButton() { - static bool lastState = HIGH; // using INPUT_PULLUP -> HIGH when released - static unsigned long lastChangeTime = 0; - bool currentState = digitalRead(EMERGENCY_BUTTON_PIN); - if (currentState != lastState && (millis() - lastChangeTime > 50)) { - lastChangeTime = millis(); - if (currentState == LOW) { // pressed - unsigned long now = millis(); - if (now - lastTapTime < TAP_TIMEOUT) tapCount++; - else tapCount = 1; - lastTapTime = now; - Serial.printf("BUTTON TAP #%d/%d\n", tapCount, REQUIRED_TAPS); - if (tapCount >= REQUIRED_TAPS) { - Serial.println("TRIPLE TAP - EMERGENCY TRIGGERED"); - emergencyTriggered = true; - tapCount = 0; - } - } - } - if (millis() - lastTapTime > TAP_TIMEOUT && tapCount > 0) tapCount = 0; - lastState = currentState; -} - -void handleEmergency() { - Serial.println("\n████████ EMERGENCY MODE █████████"); - SensorData s = readAllSensors(); - s.emergency = true; - s.motion = motionData; - Serial.println("EMERGENCY SNAPSHOT:"); - Serial.printf(" Temp: %.2f C Hum: %.2f %%\n", s.temperature, s.humidity); - Serial.printf(" MQ2:%d MQ9:%d MQ135:%d\n", s.mq2_analog, s.mq9_analog, s.mq135_analog); - Serial.printf(" Fall: %s\n", s.motion.fallDetected ? "YES":"NO"); - if (loraReady) sendLoRaData(s); else Serial.println("LoRa not ready."); - if (audioReady) playAudioFile(FALL_DETECTED); - delay(800); -} - -// ========================= Status ========================= -void printSystemStatus() { - Serial.println("\n=== SYSTEM STATUS ==="); - Serial.printf("Node ID: %s\n", NODE_ID); - Serial.printf("Uptime: %lu s\n", millis()/1000); - Serial.printf("Packets sent: %d\n", packetCount); - Serial.printf("DHT: %s MPU: %s LoRa: %s Audio: %s\n", - dhtReady ? "YES":"NO", - mpuReady ? "YES":"NO", - loraReady ? "YES":"NO", - audioReady ? "YES":"NO"); - Serial.printf("MQ2 cal: %s MQ9 cal: %s MQ135 cal: %s\n", - mq2_cal.calibrated?"YES":"NO", - mq9_cal.calibrated?"YES":"NO", - mq135_cal.calibrated?"YES":"NO"); - Serial.printf("Last Temp: %.2f C Last Hum (corrected): %.2f %%\n", lastValidTemperature, lastValidHumidity); - Serial.println("=====================\n"); -} diff --git a/mac_addresses.txt b/mac_addresses.txt deleted file mode 100644 index 38bea99..0000000 --- a/mac_addresses.txt +++ /dev/null @@ -1,2 +0,0 @@ -Wristband MAC: 0C:4E:A0:66:B2:78 -Edge Node MAC: 28:56:2f:49:56:ac diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index dee1037..0000000 --- a/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "SIREN", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} diff --git a/prototype1/Prototype1.cpp b/prototype1/Prototype1.cpp deleted file mode 100644 index d90353b..0000000 --- a/prototype1/Prototype1.cpp +++ /dev/null @@ -1,88 +0,0 @@ -/* -ESP8266MOD MQ-9 Sensor MQ-135 Sensor ----------- ----------- ------------- -3.3V --> VCC --> VCC -GND --> GND --> GND -A0 --> A0 (analog output) -D1 --> D0 (digital output) - -Created by Shishir Dwivedi - 17 Aug 2025 1:11 AM - -Both MQ series sensors are connected in different GPIOs, MQ-9 is in Analog -output and MQ-135 in Digital Output, -This prototype is pre multiplexer :D - - -*/ - - - -#include - -// Pin definitions -const int MQ9_ANALOG = A0; // MQ-9 connected to analog pin -const int MQ135_DIGITAL = D1; // MQ-135 digital output - -// Calibration values - TO BE CHANGED -float MQ9_THRESHOLD = 1.5; // Voltage threshold for MQ-9 -bool MQ135_ALERT_STATE = LOW; // MQ-135 alerts when pin goes LOW - -void setup() { - Serial.begin(115200); // Serial Monitor baud rate - pinMode(MQ135_DIGITAL, INPUT); - - delay(2000); - Serial.println("=== ESP8266 Gas Detection System ==="); - Serial.println("MQ-9: Analog reading (CO, LPG, Methane)"); - Serial.println("MQ-135: Digital alert (Air pollution)"); - Serial.println("Warming up sensors... wait 20 seconds"); - - // Sensor warm-up period - delay(20000); - Serial.println("System ready!"); -} - -void readAndIdentifyGases() { - Serial.println("\n=== Gas Detection Results ==="); - - // Read MQ-9 analog value - int mq9_raw = analogRead(MQ9_ANALOG); - float mq9_voltage = mq9_raw * (3.3 / 1023.0); - - Serial.print("MQ-9 Raw Value: "); - Serial.print(mq9_raw); - Serial.print(" | Voltage: "); - Serial.print(mq9_voltage, 2); - Serial.print("V | Status: "); - - // Analyze MQ-9 readings - if (mq9_voltage > 2.0) { - Serial.println(" >>> HIGH DANGER - Toxic gas levels!"); - Serial.println(" Likely gases: Carbon Monoxide, LPG, or Methane"); - } else if (mq9_voltage > MQ9_THRESHOLD) { - Serial.println(" >>> MODERATE - Gas detected"); - Serial.println(" Possible: Low levels of CO/LPG/Methane"); - } else { - Serial.println(" >>> NORMAL - No dangerous gases"); - } - - // Read MQ-135 digital alert - int mq135_digital = digitalRead(MQ135_DIGITAL); - Serial.print("MQ-135 Digital Pin: "); - Serial.print(mq135_digital == HIGH ? "HIGH" : "LOW"); - Serial.print(" | Status: "); - - if (mq135_digital == MQ135_ALERT_STATE) { - Serial.println(" >>> ALERT - Air pollution detected!"); - Serial.println(" Possible pollutants: Ammonia, Smoke, Benzene, Alcohol, CO2"); - } else { - Serial.println(" >>> GOOD - Air quality acceptable"); - } - - Serial.println("========================"); -} - -void loop() { - readAndIdentifyGases(); - delay(3000); // Read every 3 seconds -} diff --git a/prototype1/TInyMLProto.cpp b/prototype1/TInyMLProto.cpp deleted file mode 100644 index 6923577..0000000 --- a/prototype1/TInyMLProto.cpp +++ /dev/null @@ -1,917 +0,0 @@ -/* - * ESP32 Gas Sensor Monitor - MQ-9 and MQ-135 with Advanced Signal Processing - * - * Hardware: - * - ESP32-WROOM DevKit - * - MQ-9: Analog → GPIO34, Heater PWM → GPIO25 - * - MQ-135: Analog → GPIO33 - * - 5V supply, 10kΩ load resistors, ADC ref ~1.1V - * - * Features: - * - Two-phase heater cycling for MQ-9 - * - Adaptive thresholds using rolling statistics - * - Gas classification (CO, flammable, VOC, smoke) - * - Serial CLI with JSON/CSV output modes - * - Persistent calibration storage - * - * Author: Shishir Dwivedi - * Date: 29/08/2025 - * Generated for prototype development - */ - -#include -#include -#include -#include - -// ========== PIN DEFINITIONS ========== -#define PIN_MQ9_AO 34 -#define PIN_MQ135_AO 33 -#define PIN_MQ9_HEATER 25 - -float lastK9Low = 1.0f; -float lastK9High = 1.0f; -float lastK135 = 1.0f; - -// ========== HARDWARE CONSTANTS ========== -constexpr float VCC = 5.0f; // Sensor supply voltage -constexpr float VADC_REF = 1.10f; // ESP32 ADC effective reference (calibrated) -constexpr float RL_OHMS = 10000.0f; // Load resistor value -constexpr int ADC_SAMPLES = 12; // Samples to average per reading - -// ========== HEATER CONTROL ========== -constexpr float DUTY_LOW = 0.45f; // 45% duty for low phase -constexpr float DUTY_HIGH = 1.00f; // 100% duty for high phase -constexpr int T_LOW_MS = 10000; // Low phase duration (ms) -constexpr int T_HIGH_MS = 10000; // High phase duration (ms) - -// ========== TIMING CONSTANTS ========== -constexpr int WARMUP_MS = 180000; // 3 minutes warm-up after flash -constexpr int CALIB_MS = 300000; // 5 minutes R0 calibration -constexpr int STAB_MS = 120000; // 2 minutes stabilization - -// ========== SIGNAL PROCESSING ========== -constexpr float EMA_ALPHA = 0.3f; // Exponential moving average factor -constexpr int MED_WIN = 5; // Median filter window size -constexpr int SLOPE_WIN = 6; // Slope calculation window (cycles) - -// ========== ALERT THRESHOLDS ========== -constexpr int DEBOUNCE_N = 3; // Consecutive cycles for alert -constexpr int T_WARN_MS = 30000; // Warning dwell time -constexpr int T_DANGER_MS = 90000; // Danger dwell time - -// ========== CLASSIFICATION CONSTANTS ========== -constexpr float ICO_CO_MIN = 1.4f; // Min I_CO for CO detection -constexpr float ICO_CH4_MAX = 0.8f; // Max I_CO for CH4 detection - -// ========== ABSOLUTE THRESHOLDS ========== -constexpr float ABS_VOC_WARN = 1.5f; -constexpr float ABS_VOC_DANGER = 2.5f; -constexpr float ABS_FLAM_WARN = 1.6f; -constexpr float ABS_FLAM_DANGER = 2.6f; - -// ========== ENUMS ========== -enum EventType : int { - NONE = 0, - GAS_DETECTED = 2, - HW_FAULT = 4, - MULTI_ALERT = 6 -}; - -enum Subtype : int { - VOC_LIKELY = 10, - CO_LIKELY = 11, - FLAMMABLE_LIKELY = 12, - SMOKE_MIXED = 13, - UNKNOWN_GAS = 19 -}; - -enum HeaterPhase { - PHASE_LOW, - PHASE_HIGH -}; - -enum ThresholdMode { - THRESH_ABSOLUTE, - THRESH_MAD -}; - -// ========== GLOBAL STATE ========== -Preferences prefs; -bool csvMode = false; -bool armed = false; -bool rebasing = false; -ThresholdMode threshMode = THRESH_MAD; - -// Heater control -HeaterPhase currentPhase = PHASE_LOW; -unsigned long phaseStartTime = 0; -unsigned long cycleCount = 0; - -// Calibration baselines -float R0_mq9 = 0.0f; -float R0_mq135 = 0.0f; -bool calibrated = false; - -// Raw sensor data -float medianK9Low[MED_WIN] = {0}; -float medianK9High[MED_WIN] = {0}; -float medianK135[MED_WIN] = {0}; -int medianIndex = 0; - -// Processed indices with EMA -float I_CO = 1.0f; -float I_FLAM = 1.0f; -float I_VOC = 1.0f; - -// Slope calculation buffers -float slopeBufferFLAM[SLOPE_WIN] = {0}; -float slopeBufferVOC[SLOPE_WIN] = {0}; -int slopeIndex = 0; - -// Rolling statistics for adaptive thresholds -#define STATS_SIZE 120 // 30-60 minutes at 30s/cycle -float rollingCO[STATS_SIZE] = {0}; -float rollingFLAM[STATS_SIZE] = {0}; -float rollingVOC[STATS_SIZE] = {0}; -int statsIndex = 0; -int statsCount = 0; - -float mu_CO = 1.0f, mu_FLAM = 1.0f, mu_VOC = 1.0f; -float mad_CO = 0.1f, mad_FLAM = 0.1f, mad_VOC = 0.1f; - -// Alert state -bool warnCO = false, warnFLAM = false, warnVOC = false; -bool dangerCO = false, dangerFLAM = false, dangerVOC = false; -int debounceCounters[6] = {0}; // CO_warn, CO_danger, FLAM_warn, FLAM_danger, VOC_warn, VOC_danger -unsigned long alertStartTimes[6] = {0}; - -// System timing -unsigned long bootTime; -bool firstBoot = false; - -// ========== UTILITY FUNCTIONS ========== - -float clamp01(float x) { - return constrain(x, 0.0f, 1.0f); -} - -float clampFloat(float x, float minVal, float maxVal) { - if (isnan(x) || isinf(x)) return minVal; - return constrain(x, minVal, maxVal); -} - -// ========== ADC AND SENSOR FUNCTIONS ========== - -float readADC(int pin) { - long sum = 0; - for (int i = 0; i < ADC_SAMPLES; i++) { - sum += analogRead(pin); - delayMicroseconds(100); - } - return (float)sum / ADC_SAMPLES; -} - -float computeRs(float adcValue) { - float voltage = adcValue * (VADC_REF / 4095.0f); - voltage = clampFloat(voltage, 0.001f, VADC_REF * 0.99f); - - float Rs = RL_OHMS * (VCC - voltage) / voltage; - return clampFloat(Rs, 100.0f, 1000000.0f); -} - -float medianFilter(float* buffer, float newValue) { - // Simple insertion sort median filter - buffer[medianIndex] = newValue; - - float sorted[MED_WIN]; - memcpy(sorted, buffer, sizeof(sorted)); - - for (int i = 1; i < MED_WIN; i++) { - float key = sorted[i]; - int j = i - 1; - while (j >= 0 && sorted[j] > key) { - sorted[j + 1] = sorted[j]; - j--; - } - sorted[j + 1] = key; - } - - return sorted[MED_WIN / 2]; -} - -float ema(float current, float newValue, float alpha) { - return alpha * newValue + (1.0f - alpha) * current; -} - -float computeSlope(float* buffer, int windowSize) { - if (windowSize < 2) return 0.0f; - - float sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0; - - for (int i = 0; i < windowSize; i++) { - sumX += i; - sumY += buffer[i]; - sumXY += i * buffer[i]; - sumX2 += i * i; - } - - float denominator = windowSize * sumX2 - sumX * sumX; - if (abs(denominator) < 0.001f) return 0.0f; - - return (windowSize * sumXY - sumX * sumY) / denominator; -} - -// ========== HEATER CONTROL ========== - -void setupHeater() { - // ESP32 Arduino Core 3.x uses ledcAttach instead of ledcSetup + ledcAttachPin - if (!ledcAttach(PIN_MQ9_HEATER, 1000, 8)) { - Serial.println("ERROR: Failed to attach heater pin to LEDC"); - } -} - -void applyHeaterPhase(HeaterPhase phase) { - float duty = (phase == PHASE_LOW) ? DUTY_LOW : DUTY_HIGH; - int dutyCycle = (int)(duty * 255); - ledcWrite(PIN_MQ9_HEATER, dutyCycle); // Changed from ledcWrite(0, dutyCycle) - - currentPhase = phase; - phaseStartTime = millis(); -} - -// ========== STATISTICS ========== - -void updateRollingStats() { - if (!armed) return; - - rollingCO[statsIndex] = I_CO; - rollingFLAM[statsIndex] = I_FLAM; - rollingVOC[statsIndex] = I_VOC; - - statsIndex = (statsIndex + 1) % STATS_SIZE; - if (statsCount < STATS_SIZE) statsCount++; - - // Compute median and MAD - if (statsCount >= 10) { - float sortedCO[STATS_SIZE], sortedFLAM[STATS_SIZE], sortedVOC[STATS_SIZE]; - memcpy(sortedCO, rollingCO, statsCount * sizeof(float)); - memcpy(sortedFLAM, rollingFLAM, statsCount * sizeof(float)); - memcpy(sortedVOC, rollingVOC, statsCount * sizeof(float)); - - // Simple bubble sort for median - for (int i = 0; i < statsCount - 1; i++) { - for (int j = 0; j < statsCount - i - 1; j++) { - if (sortedCO[j] > sortedCO[j + 1]) { - float temp = sortedCO[j]; sortedCO[j] = sortedCO[j + 1]; sortedCO[j + 1] = temp; - } - if (sortedFLAM[j] > sortedFLAM[j + 1]) { - float temp = sortedFLAM[j]; sortedFLAM[j] = sortedFLAM[j + 1]; sortedFLAM[j + 1] = temp; - } - if (sortedVOC[j] > sortedVOC[j + 1]) { - float temp = sortedVOC[j]; sortedVOC[j] = sortedVOC[j + 1]; sortedVOC[j + 1] = temp; - } - } - } - - mu_CO = sortedCO[statsCount / 2]; - mu_FLAM = sortedFLAM[statsCount / 2]; - mu_VOC = sortedVOC[statsCount / 2]; - - // Compute MAD - float madBufferCO[STATS_SIZE], madBufferFLAM[STATS_SIZE], madBufferVOC[STATS_SIZE]; - for (int i = 0; i < statsCount; i++) { - madBufferCO[i] = abs(rollingCO[i] - mu_CO); - madBufferFLAM[i] = abs(rollingFLAM[i] - mu_FLAM); - madBufferVOC[i] = abs(rollingVOC[i] - mu_VOC); - } - - // Sort MAD buffers - for (int i = 0; i < statsCount - 1; i++) { - for (int j = 0; j < statsCount - i - 1; j++) { - if (madBufferCO[j] > madBufferCO[j + 1]) { - float temp = madBufferCO[j]; madBufferCO[j] = madBufferCO[j + 1]; madBufferCO[j + 1] = temp; - } - if (madBufferFLAM[j] > madBufferFLAM[j + 1]) { - float temp = madBufferFLAM[j]; madBufferFLAM[j] = madBufferFLAM[j + 1]; madBufferFLAM[j + 1] = temp; - } - if (madBufferVOC[j] > madBufferVOC[j + 1]) { - float temp = madBufferVOC[j]; madBufferVOC[j] = madBufferVOC[j + 1]; madBufferVOC[j + 1] = temp; - } - } - } - - mad_CO = max(madBufferCO[statsCount / 2], 0.05f); - mad_FLAM = max(madBufferFLAM[statsCount / 2], 0.05f); - mad_VOC = max(madBufferVOC[statsCount / 2], 0.05f); - } -} - -// ========== ALERT PROCESSING ========== - -void updateAlerts() { - unsigned long now = millis(); - - // Determine thresholds - float threshWarnCO, threshDangerCO, threshWarnFLAM, threshDangerFLAM, threshWarnVOC, threshDangerVOC; - - if (threshMode == THRESH_MAD) { - threshWarnCO = mu_CO + 3.0f * mad_CO; - threshDangerCO = mu_CO + 6.0f * mad_CO; - threshWarnFLAM = mu_FLAM + 3.0f * mad_FLAM; - threshDangerFLAM = mu_FLAM + 6.0f * mad_FLAM; - threshWarnVOC = mu_VOC + 3.0f * mad_VOC; - threshDangerVOC = mu_VOC + 6.0f * mad_VOC; - } else { - threshWarnCO = 1.4f; - threshDangerCO = 2.0f; - threshWarnFLAM = ABS_FLAM_WARN; - threshDangerFLAM = ABS_FLAM_DANGER; - threshWarnVOC = ABS_VOC_WARN; - threshDangerVOC = ABS_VOC_DANGER; - } - - // Apply absolute minimums - threshWarnFLAM = max(threshWarnFLAM, ABS_FLAM_WARN); - threshDangerFLAM = max(threshDangerFLAM, ABS_FLAM_DANGER); - threshWarnVOC = max(threshWarnVOC, ABS_VOC_WARN); - threshDangerVOC = max(threshDangerVOC, ABS_VOC_DANGER); - - // Check conditions with debouncing - bool conditions[6] = { - I_CO >= threshWarnCO, // CO warn - I_CO >= threshDangerCO, // CO danger - I_FLAM >= threshWarnFLAM, // FLAM warn - I_FLAM >= threshDangerFLAM, // FLAM danger - I_VOC >= threshWarnVOC, // VOC warn - I_VOC >= threshDangerVOC // VOC danger - }; - - bool newAlerts[6] = {false, false, false, false, false, false}; - - for (int i = 0; i < 6; i++) { - if (conditions[i]) { - debounceCounters[i]++; - if (debounceCounters[i] >= DEBOUNCE_N) { - if (alertStartTimes[i] == 0) { - alertStartTimes[i] = now; - } - - unsigned long alertDuration = now - alertStartTimes[i]; - unsigned long requiredDuration = (i % 2 == 0) ? T_WARN_MS : T_DANGER_MS; - - if (alertDuration >= requiredDuration) { - newAlerts[i] = true; - } - } - } else { - debounceCounters[i] = 0; - alertStartTimes[i] = 0; - } - } - - // Apply hysteresis - only clear when dropping below lower threshold for 2x dwell - if (warnCO && !newAlerts[0] && I_CO < threshWarnCO * 0.9f) { - static unsigned long coWarnClearStart = 0; - if (coWarnClearStart == 0) coWarnClearStart = now; - if (now - coWarnClearStart >= T_WARN_MS * 2) { - warnCO = false; - coWarnClearStart = 0; - } - } else if (newAlerts[0]) { - warnCO = true; - } - - // Similar hysteresis for other alerts (simplified for brevity) - warnFLAM = newAlerts[2] || (warnFLAM && I_FLAM >= threshWarnFLAM * 0.9f); - warnVOC = newAlerts[4] || (warnVOC && I_VOC >= threshWarnVOC * 0.9f); - dangerCO = newAlerts[1] || (dangerCO && I_CO >= threshDangerCO * 0.9f); - dangerFLAM = newAlerts[3] || (dangerFLAM && I_FLAM >= threshDangerFLAM * 0.9f); - dangerVOC = newAlerts[5] || (dangerVOC && I_VOC >= threshDangerVOC * 0.9f); -} - -// ========== CLASSIFICATION ========== - -void classify(EventType& eventType, Subtype& subtype, float& confidence) { - eventType = NONE; - subtype = (Subtype)0; - confidence = 0.0f; - - if (!armed) return; - - // Compute slopes - float slopeFLAM = computeSlope(slopeBufferFLAM, min(SLOPE_WIN, (int)cycleCount)); - float slopeVOC = computeSlope(slopeBufferVOC, min(SLOPE_WIN, (int)cycleCount)); - - bool anyWarn = warnCO || warnFLAM || warnVOC; - bool anyDanger = dangerCO || dangerFLAM || dangerVOC; - - if (!anyWarn && !anyDanger) { - eventType = NONE; - return; - } - - eventType = GAS_DETECTED; - - // Count simultaneous dangers for MULTI_ALERT - int dangerCount = (dangerCO ? 1 : 0) + (dangerFLAM ? 1 : 0) + (dangerVOC ? 1 : 0); - if (dangerCount >= 2) { - eventType = MULTI_ALERT; - subtype = (Subtype)0; // Mixed - confidence = 0.95f; - return; - } - - // Classification rules (in order) - if (I_CO >= ICO_CO_MIN && (warnFLAM || dangerFLAM)) { - subtype = CO_LIKELY; - confidence = clamp01(0.5f * (I_CO - ICO_CO_MIN) / ICO_CO_MIN + 0.5f * I_FLAM / ABS_FLAM_WARN); - } - else if ((warnFLAM || dangerFLAM) && I_CO <= ICO_CH4_MAX) { - subtype = FLAMMABLE_LIKELY; - confidence = clamp01(I_FLAM / ABS_FLAM_WARN); - } - else if ((warnVOC || dangerVOC) && !warnFLAM && !dangerFLAM) { - subtype = VOC_LIKELY; - confidence = clamp01(I_VOC / ABS_VOC_WARN); - } - else if ((warnVOC || dangerVOC) && (warnFLAM || dangerFLAM) && - slopeFLAM > 0.001f && slopeVOC > 0.001f) { - subtype = SMOKE_MIXED; - confidence = clamp01(0.5f * (I_VOC / ABS_VOC_WARN + I_FLAM / ABS_FLAM_WARN)); - } - else if (anyDanger) { - subtype = UNKNOWN_GAS; - confidence = 0.6f; - } - else { - subtype = UNKNOWN_GAS; - confidence = 0.3f; - } -} - -// ========== CALIBRATION ========== - -void saveR0() { - if (!prefs.begin("gasmonitor", false)) { - Serial.println("ERROR: Failed to open preferences"); - return; - } - - prefs.putFloat("R0_mq9", R0_mq9); - prefs.putFloat("R0_mq135", R0_mq135); - prefs.putBool("calibrated", true); - prefs.end(); - - Serial.printf("R0 saved: MQ9=%.1f, MQ135=%.1f\n", R0_mq9, R0_mq135); -} - -void loadR0() { - if (!prefs.begin("gasmonitor", true)) { - Serial.println("WARNING: Failed to open preferences for reading"); - return; - } - - R0_mq9 = prefs.getFloat("R0_mq9", 0.0f); - R0_mq135 = prefs.getFloat("R0_mq135", 0.0f); - calibrated = prefs.getBool("calibrated", false); - firstBoot = !prefs.getBool("booted", false); - - prefs.end(); - - if (calibrated && R0_mq9 > 0 && R0_mq135 > 0) { - Serial.printf("R0 loaded: MQ9=%.1f, MQ135=%.1f\n", R0_mq9, R0_mq135); - } else { - calibrated = false; - Serial.println("No valid calibration found. Run REBASE command."); - } -} - -void performCalibration() { - Serial.println("Starting R0 calibration (clean air assumed)..."); - rebasing = true; - - const int numSamples = CALIB_MS / 1000; // 1 sample per second - float sumR9 = 0, sumR135 = 0; - int validSamples = 0; - - for (int i = 0; i < numSamples; i++) { - float adc9 = readADC(PIN_MQ9_AO); - float adc135 = readADC(PIN_MQ135_AO); - - float Rs9 = computeRs(adc9); - float Rs135 = computeRs(adc135); - - if (Rs9 > 1000 && Rs135 > 1000) { // Sanity check - sumR9 += Rs9; - sumR135 += Rs135; - validSamples++; - } - - if (i % 30 == 0) { // Progress every 30 seconds - Serial.printf("Calibration progress: %d%% (Rs9=%.0f, Rs135=%.0f)\n", - (i * 100) / numSamples, Rs9, Rs135); - } - - delay(1000); - } - - if (validSamples >= numSamples * 0.8f) { - R0_mq9 = sumR9 / validSamples; - R0_mq135 = sumR135 / validSamples; - calibrated = true; - saveR0(); - Serial.printf("Calibration complete: R0_MQ9=%.1f, R0_MQ135=%.1f\n", R0_mq9, R0_mq135); - } else { - Serial.printf("Calibration failed: only %d/%d valid samples\n", validSamples, numSamples); - } - - rebasing = false; -} - -// ========== OUTPUT FUNCTIONS ========== - -void printHuman(float adc9, float adc135, float Rs9Low, float Rs9High, float Rs135, - float K9Low, float K9High, float K135, EventType eventType, Subtype subtype, float confidence) { - - Serial.printf("\n=== Cycle %lu | Phase: %s ===\n", - cycleCount, (currentPhase == PHASE_LOW) ? "LOW" : "HIGH"); - - Serial.printf("Raw ADC: MQ9=%.0f, MQ135=%.0f\n", adc9, adc135); - Serial.printf("Rs (Ohm): MQ9_low=%.0f, MQ9_high=%.0f, MQ135=%.0f\n", Rs9Low, Rs9High, Rs135); - Serial.printf("K (Rs/R0): MQ9_low=%.2f, MQ9_high=%.2f, MQ135=%.2f\n", K9Low, K9High, K135); - Serial.printf("Indices: I_CO=%.2f, I_FLAM=%.2f, I_VOC=%.2f\n", I_CO, I_FLAM, I_VOC); - Serial.printf("Stats: μ(%.2f,%.2f,%.2f) MAD(%.2f,%.2f,%.2f)\n", - mu_CO, mu_FLAM, mu_VOC, mad_CO, mad_FLAM, mad_VOC); - Serial.printf("Alerts: WARN(%c%c%c) DANGER(%c%c%c)\n", - warnCO ? 'C' : '-', warnFLAM ? 'F' : '-', warnVOC ? 'V' : '-', - dangerCO ? 'C' : '-', dangerFLAM ? 'F' : '-', dangerVOC ? 'V' : '-'); - - String classification = "NONE"; - if (eventType == GAS_DETECTED || eventType == MULTI_ALERT) { - switch (subtype) { - case CO_LIKELY: classification = "CO_LIKELY"; break; - case FLAMMABLE_LIKELY: classification = "FLAMMABLE"; break; - case VOC_LIKELY: classification = "VOC"; break; - case SMOKE_MIXED: classification = "SMOKE_MIXED"; break; - case UNKNOWN_GAS: classification = "UNKNOWN_GAS"; break; - } - } - - Serial.printf("Classification: %s (conf=%.2f)\n", classification.c_str(), confidence); - Serial.printf("Armed: %s | Rebasing: %s\n", armed ? "YES" : "NO", rebasing ? "YES" : "NO"); -} - -void printJSON(float adc9, float adc135, float K9Low, float K9High, float K135, - EventType eventType, Subtype subtype, float confidence) { - if (csvMode) return; - - DynamicJsonDocument doc(512); - doc["cycle"] = cycleCount; - doc["mq135_k"] = K135; - doc["mq9_k_low"] = K9Low; - doc["mq9_k_high"] = K9High; - doc["i_co"] = I_CO; - doc["i_flam"] = I_FLAM; - doc["i_voc"] = I_VOC; - - JsonObject warn = doc.createNestedObject("warn"); - warn["co"] = warnCO; - warn["flam"] = warnFLAM; - warn["voc"] = warnVOC; - - JsonObject danger = doc.createNestedObject("danger"); - danger["co"] = dangerCO; - danger["flam"] = dangerFLAM; - danger["voc"] = dangerVOC; - - doc["event_type"] = (int)eventType; - doc["subtype"] = (int)subtype; - doc["confidence"] = confidence; - doc["armed"] = armed; - doc["rebasing"] = rebasing; - - serializeJson(doc, Serial); - Serial.println(); -} - -void printCSV(float adc9, float adc135, float Rs9Low, float Rs9High, float Rs135, - float K9Low, float K9High, float K135, EventType eventType, float confidence) { - if (!csvMode) return; - - Serial.printf("%lu,%.0f,%.0f,%.0f,%.0f,%.0f,%.0f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%d,%.2f\n", - millis(), adc9, adc9, adc135, Rs9Low, Rs9High, Rs135, - K9Low, K9High, K135, I_CO, I_FLAM, I_VOC, (int)eventType, confidence); -} - -// ========== SERIAL COMMAND INTERFACE ========== - -void printHelp() { - Serial.println("\n=== AVAILABLE COMMANDS ==="); - Serial.println("HELP - Show this help"); - Serial.println("STATUS - Show current settings and baselines"); - Serial.println("REBASE - Start R0 calibration sequence"); - Serial.println("CSV ON|OFF - Toggle CSV output mode"); - Serial.println("THRESH ABS|MAD - Set threshold mode (absolute or MAD-based)"); - Serial.println("SET - Change runtime parameters:"); - Serial.println(" T_LOW - Low phase duration"); - Serial.println(" T_HIGH - High phase duration"); - Serial.println(" DUTY_LOW <0.0-1.0> - Low phase duty cycle"); - Serial.println(" DUTY_HIGH <0.0-1.0>- High phase duty cycle"); - Serial.println(" EMA_ALPHA <0.0-1.0>- Exponential moving average factor"); - Serial.println(); -} - -void printStatus() { - Serial.println("\n=== SYSTEM STATUS ==="); - Serial.printf("Pin Map: MQ9_AO=%d, MQ135_AO=%d, HEATER=%d\n", - PIN_MQ9_AO, PIN_MQ135_AO, PIN_MQ9_HEATER); - Serial.printf("Hardware: VCC=%.1fV, VADC_REF=%.2fV, RL=%.0fΩ\n", - VCC, VADC_REF, RL_OHMS); - Serial.printf("Timing: T_LOW=%ds, T_HIGH=%ds\n", T_LOW_MS/1000, T_HIGH_MS/1000); - Serial.printf("Heater: DUTY_LOW=%.1f%%, DUTY_HIGH=%.1f%%\n", - DUTY_LOW*100, DUTY_HIGH*100); - Serial.printf("Calibration: R0_MQ9=%.1f, R0_MQ135=%.1f (Valid: %s)\n", - R0_mq9, R0_mq135, calibrated ? "YES" : "NO"); - Serial.printf("State: Armed=%s, CSV_Mode=%s, Thresh_Mode=%s\n", - armed ? "YES" : "NO", csvMode ? "YES" : "NO", - threshMode == THRESH_MAD ? "MAD" : "ABS"); - Serial.printf("Statistics: μ(%.2f,%.2f,%.2f) MAD(%.2f,%.2f,%.2f)\n", - mu_CO, mu_FLAM, mu_VOC, mad_CO, mad_FLAM, mad_VOC); - Serial.printf("Runtime: %lu cycles, %lu minutes since boot\n", - cycleCount, (millis() - bootTime) / 60000); - Serial.println(); -} - -void handleSerialCommand() { - if (!Serial.available()) return; - - String command = Serial.readStringUntil('\n'); - command.trim(); - command.toUpperCase(); - - if (command == "HELP") { - printHelp(); - } - else if (command == "STATUS") { - printStatus(); - } - else if (command == "REBASE") { - if (rebasing) { - Serial.println("Calibration already in progress"); - return; - } - performCalibration(); - } - else if (command == "CSV ON") { - csvMode = true; - Serial.println("CSV mode ON"); - Serial.println("t_ms,adc_mq135,adc_mq9_low,adc_mq9_high,Rs135,Rs9_low,Rs9_high,K135,K9_low,K9_high,I_CO,I_FLAM,I_VOC,event,conf"); - } - else if (command == "CSV OFF") { - csvMode = false; - Serial.println("CSV mode OFF"); - } - else if (command == "THRESH ABS") { - threshMode = THRESH_ABSOLUTE; - Serial.println("Threshold mode: ABSOLUTE"); - } - else if (command == "THRESH MAD") { - threshMode = THRESH_MAD; - Serial.println("Threshold mode: MAD-based adaptive"); - } - else if (command.startsWith("SET ")) { - // Parse SET commands - int firstSpace = command.indexOf(' ', 4); - if (firstSpace > 0) { - String key = command.substring(4, firstSpace); - String valueStr = command.substring(firstSpace + 1); - float value = valueStr.toFloat(); - - Serial.printf("SET command: %s = %.3f\n", key.c_str(), value); - Serial.println("Note: Runtime parameter changes require code modification for persistence"); - } else { - Serial.println("Invalid SET syntax. Use: SET "); - } - } - else if (command.length() > 0) { - Serial.printf("Unknown command: %s\n", command.c_str()); - Serial.println("Type HELP for available commands"); - } -} - -// ========== MAIN SENSOR PROCESSING ========== - -void processSensorCycle() { - unsigned long now = millis(); - - // Check if phase should change - unsigned long phaseElapsed = now - phaseStartTime; - unsigned long phaseDuration = (currentPhase == PHASE_LOW) ? T_LOW_MS : T_HIGH_MS; - - if (phaseElapsed >= phaseDuration) { - // Switch phase - currentPhase = (currentPhase == PHASE_LOW) ? PHASE_HIGH : PHASE_LOW; - applyHeaterPhase(currentPhase); - - // Only process measurements at end of each complete cycle (after high phase) - if (currentPhase == PHASE_LOW) { - // We just finished a complete cycle, process the data - cycleCount++; - - // Read sensors - float adc9 = readADC(PIN_MQ9_AO); - float adc135 = readADC(PIN_MQ135_AO); - - float Rs9 = computeRs(adc9); - float Rs135 = computeRs(adc135); - - // Compute normalized K values - float K9Low = 1.0f, K9High = 1.0f, K135 = 1.0f; - - if (calibrated && R0_mq9 > 0 && R0_mq135 > 0) { - if (currentPhase == PHASE_LOW) { - lastK9Low = Rs9 / R0_mq9; // Capture LOW phase reading - } else { - lastK9High = Rs9 / R0_mq9; // Capture HIGH phase reading - } - } - - // Apply median filtering - float filteredK9Low = medianFilter(medianK9Low, K9Low); - float filteredK9High = medianFilter(medianK9High, K9High); - float filteredK135 = medianFilter(medianK135, K135); - - medianIndex = (medianIndex + 1) % MED_WIN; - - // Update indices with EMA - I_FLAM = ema(I_FLAM, filteredK9High, EMA_ALPHA); - I_VOC = ema(I_VOC, filteredK135, EMA_ALPHA); - - // I_CO requires both phases - if (filteredK9High > 0.001f) { - float newI_CO = filteredK9Low / filteredK9High; - I_CO = ema(I_CO, newI_CO, EMA_ALPHA); - } - - // Update slope buffers - slopeBufferFLAM[slopeIndex] = I_FLAM; - slopeBufferVOC[slopeIndex] = I_VOC; - slopeIndex = (slopeIndex + 1) % SLOPE_WIN; - - // Update rolling statistics and alerts - updateRollingStats(); - updateAlerts(); - - // Classification - EventType eventType; - Subtype subtype; - float confidence; - classify(eventType, subtype, confidence); - - // Output results - if (!csvMode) { - printHuman(adc9, adc135, Rs9, Rs9, Rs135, - filteredK9Low, filteredK9High, filteredK135, - eventType, subtype, confidence); - } - - printJSON(adc9, adc135, filteredK9Low, filteredK9High, filteredK135, - eventType, subtype, confidence); - - printCSV(adc9, adc135, Rs9, Rs9, Rs135, - filteredK9Low, filteredK9High, filteredK135, - eventType, confidence); - } - } -} - -// ========== INITIALIZATION ========== - -void markFirstBootComplete() { - if (!prefs.begin("gasmonitor", false)) return; - prefs.putBool("booted", true); - prefs.end(); -} - -void setup() { - Serial.begin(115200); - while (!Serial) delay(100); - - bootTime = millis(); - - Serial.println("\n" + String('=', 60)); - Serial.println(" ESP32 GAS SENSOR MONITOR v1.0"); - Serial.println(" MQ-9 + MQ-135 with Adaptive Classification"); - Serial.println(String('=', 60)); - - // Initialize hardware - analogReadResolution(12); - analogSetAttenuation(ADC_11db); // For 0-3.3V range - - setupHeater(); - applyHeaterPhase(PHASE_LOW); - - // Load calibration - loadR0(); - - // Print configuration - Serial.printf("\nHARDWARE CONFIG:\n"); - Serial.printf(" MQ9 Analog: GPIO%d, MQ135 Analog: GPIO%d\n", PIN_MQ9_AO, PIN_MQ135_AO); - Serial.printf(" MQ9 Heater: GPIO%d (PWM)\n", PIN_MQ9_HEATER); - Serial.printf(" Supply: %.1fV, ADC_Ref: %.2fV, Load_R: %.0fΩ\n", VCC, VADC_REF, RL_OHMS); - Serial.printf(" Heater: Low=%.0f%% (%ds), High=%.0f%% (%ds)\n", - DUTY_LOW*100, T_LOW_MS/1000, DUTY_HIGH*100, T_HIGH_MS/1000); - - Serial.printf("\nSIGNAL PROCESSING:\n"); - Serial.printf(" ADC Samples: %d, Median Window: %d, EMA Alpha: %.2f\n", - ADC_SAMPLES, MED_WIN, EMA_ALPHA); - Serial.printf(" Debounce: %d cycles, Warn: %ds, Danger: %ds\n", - DEBOUNCE_N, T_WARN_MS/1000, T_DANGER_MS/1000); - - Serial.printf("\nCALIBRATION STATUS:\n"); - if (calibrated) { - Serial.printf(" R0 Loaded: MQ9=%.1fΩ, MQ135=%.1fΩ\n", R0_mq9, R0_mq135); - } else { - Serial.println(" NO CALIBRATION - Run 'REBASE' command in clean air"); - } - - // Warm-up sequence - if (firstBoot) { - Serial.printf("\nFIRST BOOT WARM-UP: %d minutes\n", WARMUP_MS/60000); - unsigned long warmupEnd = millis() + WARMUP_MS; - - while (millis() < warmupEnd) { - unsigned long remaining = (warmupEnd - millis()) / 1000; - Serial.printf("Warm-up: %lu seconds remaining...\r", remaining); - delay(10000); // Update every 10 seconds - } - Serial.println("\nWarm-up complete!"); - markFirstBootComplete(); - } - - // Stabilization period - Serial.printf("\nSTABILIZATION: %d seconds\n", STAB_MS/1000); - unsigned long stabEnd = millis() + STAB_MS; - - while (millis() < stabEnd) { - unsigned long remaining = (stabEnd - millis()) / 1000; - Serial.printf("Stabilizing: %lu seconds remaining...\r", remaining); - - // Continue heater cycling during stabilization - processSensorCycle(); - delay(1000); - } - - Serial.println("\nStabilization complete!"); - - if (calibrated) { - armed = true; - Serial.println("SYSTEM ARMED - Gas monitoring active"); - } else { - Serial.println("SYSTEM NOT ARMED - Calibration required (REBASE command)"); - } - - Serial.println("\nType 'HELP' for available commands"); - Serial.println("Starting sensor monitoring...\n"); -} - -// ========== MAIN LOOP ========== - -void loop() { - // Handle serial commands - handleSerialCommand(); - - // Process sensor measurements and heater cycling - processSensorCycle(); - - // Small delay to prevent overwhelming the system - delay(100); -} - -/* - * EXAMPLE SERIAL OUTPUT: - * - * === Cycle 45 | Phase: HIGH === - * Raw ADC: MQ9=1250, MQ135=890 - * Rs (Ohm): MQ9_low=15420, MQ9_high=12100, MQ135=18900 - * K (Rs/R0): MQ9_low=1.15, MQ9_high=0.95, MQ135=1.82 - * Indices: I_CO=1.21, I_FLAM=0.95, I_VOC=1.82 - * Stats: μ(1.05,1.02,1.15) MAD(0.08,0.12,0.25) - * Alerts: WARN(--V) DANGER(---) - * Classification: VOC_LIKELY (conf=0.67) - * Armed: YES | Rebasing: NO - * - * {"cycle":45,"mq135_k":1.82,"mq9_k_low":1.15,"mq9_k_high":0.95,"i_co":1.21, - * "i_flam":0.95,"i_voc":1.82,"warn":{"co":false,"flam":false,"voc":true}, - * "danger":{"co":false,"flam":false,"voc":false},"event_type":2,"subtype":10, - * "confidence":0.67,"armed":true,"rebasing":false} - * - * COMMANDS TESTED: - * > HELP - * > STATUS - * > REBASE - * > CSV ON - * > THRESH MAD - * > SET EMA_ALPHA 0.25 - */ diff --git a/prototype1/platformio.ini b/prototype1/platformio.ini deleted file mode 100644 index abcf920..0000000 --- a/prototype1/platformio.ini +++ /dev/null @@ -1,5 +0,0 @@ -[env:esp8266] -platform = espressif8266 -board = nodemcuv2 -framework = arduino -monitor_speed = 115200 \ No newline at end of file diff --git a/prototype1/prot101notfound.cpp b/prototype1/prot101notfound.cpp deleted file mode 100644 index 05bebef..0000000 --- a/prototype1/prot101notfound.cpp +++ /dev/null @@ -1,913 +0,0 @@ -/* - * ESP32 Gas Sensor Monitor - MQ-9 and MQ-135 with Advanced Signal Processing - * - * Hardware: - * - ESP32-WROOM DevKit - * - MQ-9: Analog → GPIO34, Heater PWM → GPIO25 - * - MQ-135: Analog → GPIO33 - * - 5V supply, 10kΩ load resistors, ADC ref ~1.1V - * - * Features: - * - Two-phase heater cycling for MQ-9 - * - Adaptive thresholds using rolling statistics - * - Gas classification (CO, flammable, VOC, smoke) - * - Serial CLI with JSON/CSV output modes - * - Persistent calibration storage - * - * Author: Shishir Dwivedi - * Date: 29/08/2025 - * Generated for prototype development - */ - -#include -#include -#include -#include - -// ========== PIN DEFINITIONS ========== -#define PIN_MQ9_AO 34 -#define PIN_MQ135_AO 33 -#define PIN_MQ9_HEATER 25 - -#define HEATER_CHANNEL 0 // PWM channel 0 (can be 0–15 on ESP32) -#define HEATER_FREQ 1000 // 1 kHz PWM frequency -#define HEATER_RESOLUTION 8 // 8-bit resolution - - -float lastK9Low = 1.0f; -float lastK9High = 1.0f; -float lastK135 = 1.0f; -unsigned long lastRollingUpdate = 0; - -// ========== HARDWARE CONSTANTS ========== -constexpr float VCC = 5.0f; // Sensor supply voltage -constexpr float VADC_REF = 1.10f; // ESP32 ADC effective reference (calibrated) -constexpr float RL_OHMS = 10000.0f; // Load resistor value -constexpr int ADC_SAMPLES = 12; // Samples to average per reading - -// ========== HEATER CONTROL ========== -constexpr float DUTY_LOW = 0.45f; // 45% duty for low phase -constexpr float DUTY_HIGH = 1.00f; // 100% duty for high phase -constexpr int T_LOW_MS = 10000; // Low phase duration (ms) -constexpr int T_HIGH_MS = 10000; // High phase duration (ms) - -// ========== TIMING CONSTANTS ========== -constexpr int WARMUP_MS = 180000; // 3 minutes warm-up after flash -constexpr int CALIB_MS = 300000; // 5 minutes R0 calibration -constexpr int STAB_MS = 120000; // 2 minutes stabilization - -// ========== SIGNAL PROCESSING ========== -constexpr float EMA_ALPHA = 0.3f; // Exponential moving average factor -constexpr int MED_WIN = 5; // Median filter window size -constexpr int SLOPE_WIN = 6; // Slope calculation window (cycles) - -// ========== ALERT THRESHOLDS ========== -constexpr int DEBOUNCE_N = 3; // Consecutive cycles for alert -constexpr int T_WARN_MS = 30000; // Warning dwell time -constexpr int T_DANGER_MS = 90000; // Danger dwell time - -// ========== CLASSIFICATION CONSTANTS ========== -constexpr float ICO_CO_MIN = 1.4f; // Min I_CO for CO detection -constexpr float ICO_CH4_MAX = 0.8f; // Max I_CO for CH4 detection - -// ========== ABSOLUTE THRESHOLDS ========== -constexpr float ABS_VOC_WARN = 1.5f; -constexpr float ABS_VOC_DANGER = 2.5f; -constexpr float ABS_FLAM_WARN = 1.6f; -constexpr float ABS_FLAM_DANGER = 2.6f; - -// ========== ENUMS ========== -enum EventType : int { - NONE = 0, - GAS_DETECTED = 2, - HW_FAULT = 4, - MULTI_ALERT = 6 -}; - -enum Subtype : int { - VOC_LIKELY = 10, - CO_LIKELY = 11, - FLAMMABLE_LIKELY = 12, - SMOKE_MIXED = 13, - UNKNOWN_GAS = 19 -}; - -enum HeaterPhase { - PHASE_LOW, - PHASE_HIGH -}; - -enum ThresholdMode { - THRESH_ABSOLUTE, - THRESH_MAD -}; - -// ========== GLOBAL STATE ========== -Preferences prefs; -bool csvMode = false; -bool armed = false; -bool rebasing = false; -ThresholdMode threshMode = THRESH_MAD; - -// Heater control -HeaterPhase currentPhase = PHASE_LOW; -unsigned long phaseStartTime = 0; -unsigned long cycleCount = 0; - -// Calibration baselines -float R0_mq9 = 0.0f; -float R0_mq135 = 0.0f; -bool calibrated = false; - -// Raw sensor data -float medianK9Low[MED_WIN] = {0}; -float medianK9High[MED_WIN] = {0}; -float medianK135[MED_WIN] = {0}; -int medianIndex = 0; - -// Processed indices with EMA -float I_CO = 1.0f; -float I_FLAM = 1.0f; -float I_VOC = 1.0f; - -// Slope calculation buffers -float slopeBufferFLAM[SLOPE_WIN] = {0}; -float slopeBufferVOC[SLOPE_WIN] = {0}; -int slopeIndex = 0; - -// Rolling statistics for adaptive thresholds -#define STATS_SIZE 120 // 30-60 minutes at 30s/cycle -float rollingCO[STATS_SIZE] = {0}; -float rollingFLAM[STATS_SIZE] = {0}; -float rollingVOC[STATS_SIZE] = {0}; -int statsIndex = 0; -int statsCount = 0; - -float mu_CO = 1.0f, mu_FLAM = 1.0f, mu_VOC = 1.0f; -float mad_CO = 0.1f, mad_FLAM = 0.1f, mad_VOC = 0.1f; - -// Alert state -bool warnCO = false, warnFLAM = false, warnVOC = false; -bool dangerCO = false, dangerFLAM = false, dangerVOC = false; -int debounceCounters[6] = {0}; // CO_warn, CO_danger, FLAM_warn, FLAM_danger, VOC_warn, VOC_danger -unsigned long alertStartTimes[6] = {0}; - -// System timing -unsigned long bootTime; -bool firstBoot = false; - -// ========== UTILITY FUNCTIONS ========== - -float clamp01(float x) { - return constrain(x, 0.0f, 1.0f); -} - -float clampFloat(float x, float minVal, float maxVal) { - if (isnan(x) || isinf(x)) return minVal; - return constrain(x, minVal, maxVal); -} - -// ========== ADC AND SENSOR FUNCTIONS ========== - -float readADC(int pin) { - long sum = 0; - for (int i = 0; i < ADC_SAMPLES; i++) { - sum += analogRead(pin); - delayMicroseconds(100); - } - return (float)sum / ADC_SAMPLES; -} - -float computeRs(float adcValue) { - float voltage = adcValue * (VADC_REF / 4095.0f); - voltage = clampFloat(voltage, 0.001f, VADC_REF * 0.99f); - - float Rs = RL_OHMS * (VCC - voltage) / voltage; - return clampFloat(Rs, 100.0f, 1000000.0f); -} - -float medianFilter(float* buffer, float newValue) { - // Simple insertion sort median filter - buffer[medianIndex] = newValue; - - float sorted[MED_WIN]; - memcpy(sorted, buffer, sizeof(sorted)); - - for (int i = 1; i < MED_WIN; i++) { - float key = sorted[i]; - int j = i - 1; - while (j >= 0 && sorted[j] > key) { - sorted[j + 1] = sorted[j]; - j--; - } - sorted[j + 1] = key; - } - - return sorted[MED_WIN / 2]; -} - -float ema(float current, float newValue, float alpha) { - return alpha * newValue + (1.0f - alpha) * current; -} - -float computeSlope(float* buffer, int windowSize) { - if (windowSize < 2) return 0.0f; - - float sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0; - - for (int i = 0; i < windowSize; i++) { - sumX += i; - sumY += buffer[i]; - sumXY += i * buffer[i]; - sumX2 += i * i; - } - - float denominator = windowSize * sumX2 - sumX * sumX; - if (abs(denominator) < 0.001f) return 0.0f; - - return (windowSize * sumXY - sumX * sumY) / denominator; -} - -// ========== HEATER CONTROL ========== - -void setupHeater() { - // ESP32 Arduino Core 3.x uses ledcAttach instead of ledcSetup + ledcAttachPin - if (!ledcAttach(PIN_MQ9_HEATER, 1000, 8)) { - Serial.println("ERROR: Failed to attach heater pin to LEDC"); - } -} - -void applyHeaterPhase(HeaterPhase phase) { - float duty = (phase == PHASE_LOW) ? DUTY_LOW : DUTY_HIGH; - int dutyCycle = (int)(duty * 255); - ledcWrite(HEATER_CHANNEL, dutyCycle); // Changed from ledcWrite(0, dutyCycle) - - currentPhase = phase; - phaseStartTime = millis(); -} - -// ========== STATISTICS ========== - -void updateRollingStats() { - if (!armed) return; - - rollingCO[statsIndex] = I_CO; - rollingFLAM[statsIndex] = I_FLAM; - rollingVOC[statsIndex] = I_VOC; - - statsIndex = (statsIndex + 1) % STATS_SIZE; - if (statsCount < STATS_SIZE) statsCount++; - - // Compute median and MAD - if (statsCount >= 10) { - float sortedCO[STATS_SIZE], sortedFLAM[STATS_SIZE], sortedVOC[STATS_SIZE]; - memcpy(sortedCO, rollingCO, statsCount * sizeof(float)); - memcpy(sortedFLAM, rollingFLAM, statsCount * sizeof(float)); - memcpy(sortedVOC, rollingVOC, statsCount * sizeof(float)); - - // Simple bubble sort for median - for (int i = 0; i < statsCount - 1; i++) { - for (int j = 0; j < statsCount - i - 1; j++) { - if (sortedCO[j] > sortedCO[j + 1]) { - float temp = sortedCO[j]; sortedCO[j] = sortedCO[j + 1]; sortedCO[j + 1] = temp; - } - if (sortedFLAM[j] > sortedFLAM[j + 1]) { - float temp = sortedFLAM[j]; sortedFLAM[j] = sortedFLAM[j + 1]; sortedFLAM[j + 1] = temp; - } - if (sortedVOC[j] > sortedVOC[j + 1]) { - float temp = sortedVOC[j]; sortedVOC[j] = sortedVOC[j + 1]; sortedVOC[j + 1] = temp; - } - } - } - - mu_CO = sortedCO[statsCount / 2]; - mu_FLAM = sortedFLAM[statsCount / 2]; - mu_VOC = sortedVOC[statsCount / 2]; - - // Compute MAD - float madBufferCO[STATS_SIZE], madBufferFLAM[STATS_SIZE], madBufferVOC[STATS_SIZE]; - for (int i = 0; i < statsCount; i++) { - madBufferCO[i] = abs(rollingCO[i] - mu_CO); - madBufferFLAM[i] = abs(rollingFLAM[i] - mu_FLAM); - madBufferVOC[i] = abs(rollingVOC[i] - mu_VOC); - } - - // Sort MAD buffers - for (int i = 0; i < statsCount - 1; i++) { - for (int j = 0; j < statsCount - i - 1; j++) { - if (madBufferCO[j] > madBufferCO[j + 1]) { - float temp = madBufferCO[j]; madBufferCO[j] = madBufferCO[j + 1]; madBufferCO[j + 1] = temp; - } - if (madBufferFLAM[j] > madBufferFLAM[j + 1]) { - float temp = madBufferFLAM[j]; madBufferFLAM[j] = madBufferFLAM[j + 1]; madBufferFLAM[j + 1] = temp; - } - if (madBufferVOC[j] > madBufferVOC[j + 1]) { - float temp = madBufferVOC[j]; madBufferVOC[j] = madBufferVOC[j + 1]; madBufferVOC[j + 1] = temp; - } - } - } - - mad_CO = max(madBufferCO[statsCount / 2], 0.05f); - mad_FLAM = max(madBufferFLAM[statsCount / 2], 0.05f); - mad_VOC = max(madBufferVOC[statsCount / 2], 0.05f); - } -} - -// ========== ALERT PROCESSING ========== - -void updateAlerts() { - unsigned long now = millis(); - - // Determine thresholds - float threshWarnCO, threshDangerCO, threshWarnFLAM, threshDangerFLAM, threshWarnVOC, threshDangerVOC; - - if (threshMode == THRESH_MAD) { - threshWarnCO = mu_CO + 3.0f * mad_CO; - threshDangerCO = mu_CO + 6.0f * mad_CO; - threshWarnFLAM = mu_FLAM + 3.0f * mad_FLAM; - threshDangerFLAM = mu_FLAM + 6.0f * mad_FLAM; - threshWarnVOC = mu_VOC + 3.0f * mad_VOC; - threshDangerVOC = mu_VOC + 6.0f * mad_VOC; - } else { - threshWarnCO = 1.4f; - threshDangerCO = 2.0f; - threshWarnFLAM = ABS_FLAM_WARN; - threshDangerFLAM = ABS_FLAM_DANGER; - threshWarnVOC = ABS_VOC_WARN; - threshDangerVOC = ABS_VOC_DANGER; - } - - // Apply absolute minimums - threshWarnFLAM = max(threshWarnFLAM, ABS_FLAM_WARN); - threshDangerFLAM = max(threshDangerFLAM, ABS_FLAM_DANGER); - threshWarnVOC = max(threshWarnVOC, ABS_VOC_WARN); - threshDangerVOC = max(threshDangerVOC, ABS_VOC_DANGER); - - // Check conditions with debouncing - bool conditions[6] = { - I_CO >= threshWarnCO, // CO warn - I_CO >= threshDangerCO, // CO danger - I_FLAM >= threshWarnFLAM, // FLAM warn - I_FLAM >= threshDangerFLAM, // FLAM danger - I_VOC >= threshWarnVOC, // VOC warn - I_VOC >= threshDangerVOC // VOC danger - }; - - bool newAlerts[6] = {false, false, false, false, false, false}; - - for (int i = 0; i < 6; i++) { - if (conditions[i]) { - debounceCounters[i]++; - if (debounceCounters[i] >= DEBOUNCE_N) { - if (alertStartTimes[i] == 0) { - alertStartTimes[i] = now; - } - - unsigned long alertDuration = now - alertStartTimes[i]; - unsigned long requiredDuration = (i % 2 == 0) ? T_WARN_MS : T_DANGER_MS; - - if (alertDuration >= requiredDuration) { - newAlerts[i] = true; - } - } - } else { - debounceCounters[i] = 0; - alertStartTimes[i] = 0; - } - } - - // Apply hysteresis - only clear when dropping below lower threshold for 2x dwell - if (warnCO && !newAlerts[0] && I_CO < threshWarnCO * 0.9f) { - static unsigned long coWarnClearStart = 0; - if (coWarnClearStart == 0) coWarnClearStart = now; - if (now - coWarnClearStart >= T_WARN_MS * 2) { - warnCO = false; - coWarnClearStart = 0; - } - } else if (newAlerts[0]) { - warnCO = true; - } - - // Similar hysteresis for other alerts (simplified for brevity) - warnFLAM = newAlerts[2] || (warnFLAM && I_FLAM >= threshWarnFLAM * 0.9f); - warnVOC = newAlerts[4] || (warnVOC && I_VOC >= threshWarnVOC * 0.9f); - dangerCO = newAlerts[1] || (dangerCO && I_CO >= threshDangerCO * 0.9f); - dangerFLAM = newAlerts[3] || (dangerFLAM && I_FLAM >= threshDangerFLAM * 0.9f); - dangerVOC = newAlerts[5] || (dangerVOC && I_VOC >= threshDangerVOC * 0.9f); -} - -// ========== CLASSIFICATION ========== - -void classify(EventType& eventType, Subtype& subtype, float& confidence) { - eventType = NONE; - subtype = (Subtype)0; - confidence = 0.0f; - - if (!armed) return; - - // Compute slopes - float slopeFLAM = computeSlope(slopeBufferFLAM, min(SLOPE_WIN, (int)cycleCount)); - float slopeVOC = computeSlope(slopeBufferVOC, min(SLOPE_WIN, (int)cycleCount)); - - bool anyWarn = warnCO || warnFLAM || warnVOC; - bool anyDanger = dangerCO || dangerFLAM || dangerVOC; - - if (!anyWarn && !anyDanger) { - eventType = NONE; - return; - } - - eventType = GAS_DETECTED; - - // Count simultaneous dangers for MULTI_ALERT - int dangerCount = (dangerCO ? 1 : 0) + (dangerFLAM ? 1 : 0) + (dangerVOC ? 1 : 0); - if (dangerCount >= 2) { - eventType = MULTI_ALERT; - subtype = (Subtype)0; // Mixed - confidence = 0.95f; - return; - } - - // Classification rules (in order) - if (I_CO >= ICO_CO_MIN && (warnFLAM || dangerFLAM)) { - subtype = CO_LIKELY; - confidence = clamp01(0.5f * (I_CO - ICO_CO_MIN) / ICO_CO_MIN + 0.5f * I_FLAM / ABS_FLAM_WARN); - } - else if ((warnFLAM || dangerFLAM) && I_CO <= ICO_CH4_MAX) { - subtype = FLAMMABLE_LIKELY; - confidence = clamp01(I_FLAM / ABS_FLAM_WARN); - } - else if ((warnVOC || dangerVOC) && !warnFLAM && !dangerFLAM) { - subtype = VOC_LIKELY; - confidence = clamp01(I_VOC / ABS_VOC_WARN); - } - else if ((warnVOC || dangerVOC) && (warnFLAM || dangerFLAM) && - slopeFLAM > 0.001f && slopeVOC > 0.001f) { - subtype = SMOKE_MIXED; - confidence = clamp01(0.5f * (I_VOC / ABS_VOC_WARN + I_FLAM / ABS_FLAM_WARN)); - } - else if (anyDanger) { - subtype = UNKNOWN_GAS; - confidence = 0.6f; - } - else { - subtype = UNKNOWN_GAS; - confidence = 0.3f; - } -} - -// ========== CALIBRATION ========== - -void saveR0() { - if (!prefs.begin("gasmonitor", false)) { - Serial.println("ERROR: Failed to open preferences"); - return; - } - - prefs.putFloat("R0_mq9", R0_mq9); - prefs.putFloat("R0_mq135", R0_mq135); - prefs.putBool("calibrated", true); - prefs.end(); - - Serial.printf("R0 saved: MQ9=%.1f, MQ135=%.1f\n", R0_mq9, R0_mq135); -} - -void loadR0() { - if (!prefs.begin("gasmonitor", true)) { - Serial.println("WARNING: Failed to open preferences for reading"); - return; - } - - R0_mq9 = prefs.getFloat("R0_mq9", 0.0f); - R0_mq135 = prefs.getFloat("R0_mq135", 0.0f); - calibrated = prefs.getBool("calibrated", false); - firstBoot = !prefs.getBool("booted", false); - - prefs.end(); - - if (calibrated && R0_mq9 > 0 && R0_mq135 > 0) { - Serial.printf("R0 loaded: MQ9=%.1f, MQ135=%.1f\n", R0_mq9, R0_mq135); - } else { - calibrated = false; - Serial.println("No valid calibration found. Run REBASE command."); - } -} - -void performCalibration() { - Serial.println("Starting R0 calibration (clean air assumed)..."); - rebasing = true; - - const int numSamples = CALIB_MS / 1000; // 1 sample per second - float sumR9 = 0, sumR135 = 0; - int validSamples = 0; - - for (int i = 0; i < numSamples; i++) { - float adc9 = readADC(PIN_MQ9_AO); - float adc135 = readADC(PIN_MQ135_AO); - - float Rs9 = computeRs(adc9); - float Rs135 = computeRs(adc135); - - if (Rs9 > 1000 && Rs135 > 1000) { // Sanity check - sumR9 += Rs9; - sumR135 += Rs135; - validSamples++; - } - - if (i % 30 == 0) { // Progress every 30 seconds - Serial.printf("Calibration progress: %d%% (Rs9=%.0f, Rs135=%.0f)\n", - (i * 100) / numSamples, Rs9, Rs135); - } - - delay(1000); - } - - if (validSamples >= numSamples * 0.8f) { - R0_mq9 = sumR9 / validSamples; - R0_mq135 = sumR135 / validSamples; - calibrated = true; - saveR0(); - Serial.printf("Calibration complete: R0_MQ9=%.1f, R0_MQ135=%.1f\n", R0_mq9, R0_mq135); - } else { - Serial.printf("Calibration failed: only %d/%d valid samples\n", validSamples, numSamples); - } - - rebasing = false; -} - -// ========== OUTPUT FUNCTIONS ========== - -void printHuman(float adc9, float adc135, float Rs9Low, float Rs9High, float Rs135, - float K9Low, float K9High, float K135, EventType eventType, Subtype subtype, float confidence) { - - Serial.printf("\n=== Cycle %lu | Phase: %s ===\n", - cycleCount, (currentPhase == PHASE_LOW) ? "LOW" : "HIGH"); - - Serial.printf("Raw ADC: MQ9=%.0f, MQ135=%.0f\n", adc9, adc135); - Serial.printf("Rs (Ohm): MQ9_low=%.0f, MQ9_high=%.0f, MQ135=%.0f\n", Rs9Low, Rs9High, Rs135); - Serial.printf("K (Rs/R0): MQ9_low=%.2f, MQ9_high=%.2f, MQ135=%.2f\n", K9Low, K9High, K135); - Serial.printf("Indices: I_CO=%.2f, I_FLAM=%.2f, I_VOC=%.2f\n", I_CO, I_FLAM, I_VOC); - Serial.printf("Stats: μ(%.2f,%.2f,%.2f) MAD(%.2f,%.2f,%.2f)\n", - mu_CO, mu_FLAM, mu_VOC, mad_CO, mad_FLAM, mad_VOC); - Serial.printf("Alerts: WARN(%c%c%c) DANGER(%c%c%c)\n", - warnCO ? 'C' : '-', warnFLAM ? 'F' : '-', warnVOC ? 'V' : '-', - dangerCO ? 'C' : '-', dangerFLAM ? 'F' : '-', dangerVOC ? 'V' : '-'); - - String classification = "NONE"; - if (eventType == GAS_DETECTED || eventType == MULTI_ALERT) { - switch (subtype) { - case CO_LIKELY: classification = "CO_LIKELY"; break; - case FLAMMABLE_LIKELY: classification = "FLAMMABLE"; break; - case VOC_LIKELY: classification = "VOC"; break; - case SMOKE_MIXED: classification = "SMOKE_MIXED"; break; - case UNKNOWN_GAS: classification = "UNKNOWN_GAS"; break; - } - } - - Serial.printf("Classification: %s (conf=%.2f)\n", classification.c_str(), confidence); - Serial.printf("Armed: %s | Rebasing: %s\n", armed ? "YES" : "NO", rebasing ? "YES" : "NO"); -} - -void printJSON(float adc9, float adc135, float K9Low, float K9High, float K135, - EventType eventType, Subtype subtype, float confidence) { - if (csvMode) return; - - DynamicJsonDocument doc(512); - doc["cycle"] = cycleCount; - doc["mq135_k"] = K135; - doc["mq9_k_low"] = K9Low; - doc["mq9_k_high"] = K9High; - doc["i_co"] = I_CO; - doc["i_flam"] = I_FLAM; - doc["i_voc"] = I_VOC; - - JsonObject warn = doc.createNestedObject("warn"); - warn["co"] = warnCO; - warn["flam"] = warnFLAM; - warn["voc"] = warnVOC; - - JsonObject danger = doc.createNestedObject("danger"); - danger["co"] = dangerCO; - danger["flam"] = dangerFLAM; - danger["voc"] = dangerVOC; - - doc["event_type"] = (int)eventType; - doc["subtype"] = (int)subtype; - doc["confidence"] = confidence; - doc["armed"] = armed; - doc["rebasing"] = rebasing; - - serializeJson(doc, Serial); - Serial.println(); -} - -void printCSV(float adc9, float adc135, float Rs9Low, float Rs9High, float Rs135, - float K9Low, float K9High, float K135, EventType eventType, float confidence) { - if (!csvMode) return; - - Serial.printf("%lu,%.0f,%.0f,%.0f,%.0f,%.0f,%.0f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%d,%.2f\n", - millis(), adc135, adc9, adc9, Rs135, Rs9Low, Rs9High, K135, K9Low, K9High, - I_CO, I_FLAM, I_VOC, (int)eventType, confidence); -} - -// ========== SERIAL COMMAND INTERFACE ========== - -void printHelp() { - Serial.println("\n=== AVAILABLE COMMANDS ==="); - Serial.println("HELP - Show this help"); - Serial.println("STATUS - Show current settings and baselines"); - Serial.println("REBASE - Start R0 calibration sequence"); - Serial.println("CSV ON|OFF - Toggle CSV output mode"); - Serial.println("THRESH ABS|MAD - Set threshold mode (absolute or MAD-based)"); - Serial.println("SET - Change runtime parameters:"); - Serial.println(" T_LOW - Low phase duration"); - Serial.println(" T_HIGH - High phase duration"); - Serial.println(" DUTY_LOW <0.0-1.0> - Low phase duty cycle"); - Serial.println(" DUTY_HIGH <0.0-1.0>- High phase duty cycle"); - Serial.println(" EMA_ALPHA <0.0-1.0>- Exponential moving average factor"); - Serial.println(); -} - -void printStatus() { - Serial.println("\n=== SYSTEM STATUS ==="); - Serial.printf("Pin Map: MQ9_AO=%d, MQ135_AO=%d, HEATER=%d\n", - PIN_MQ9_AO, PIN_MQ135_AO, PIN_MQ9_HEATER); - Serial.printf("Hardware: VCC=%.1fV, VADC_REF=%.2fV, RL=%.0fΩ\n", - VCC, VADC_REF, RL_OHMS); - Serial.printf("Timing: T_LOW=%ds, T_HIGH=%ds\n", T_LOW_MS/1000, T_HIGH_MS/1000); - Serial.printf("Heater: DUTY_LOW=%.1f%%, DUTY_HIGH=%.1f%%\n", - DUTY_LOW*100, DUTY_HIGH*100); - Serial.printf("Calibration: R0_MQ9=%.1f, R0_MQ135=%.1f (Valid: %s)\n", - R0_mq9, R0_mq135, calibrated ? "YES" : "NO"); - Serial.printf("State: Armed=%s, CSV_Mode=%s, Thresh_Mode=%s\n", - armed ? "YES" : "NO", csvMode ? "YES" : "NO", - threshMode == THRESH_MAD ? "MAD" : "ABS"); - Serial.printf("Statistics: μ(%.2f,%.2f,%.2f) MAD(%.2f,%.2f,%.2f)\n", - mu_CO, mu_FLAM, mu_VOC, mad_CO, mad_FLAM, mad_VOC); - Serial.printf("Runtime: %lu cycles, %lu minutes since boot\n", - cycleCount, (millis() - bootTime) / 60000); - Serial.println(); -} - -void handleSerialCommand() { - if (!Serial.available()) return; - - String command = Serial.readStringUntil('\n'); - command.trim(); - command.toUpperCase(); - - if (command == "HELP") { - printHelp(); - } - else if (command == "STATUS") { - printStatus(); - } - else if (command == "REBASE") { - if (rebasing) { - Serial.println("Calibration already in progress"); - return; - } - performCalibration(); - } - else if (command == "CSV ON") { - csvMode = true; - Serial.println("CSV mode ON"); - Serial.println("t_ms,adc_mq135,adc_mq9_low,adc_mq9_high,Rs135,Rs9_low,Rs9_high,K135,K9_low,K9_high,I_CO,I_FLAM,I_VOC,event,conf"); - } - else if (command == "CSV OFF") { - csvMode = false; - Serial.println("CSV mode OFF"); - } - else if (command == "THRESH ABS") { - threshMode = THRESH_ABSOLUTE; - Serial.println("Threshold mode: ABSOLUTE"); - } - else if (command == "THRESH MAD") { - threshMode = THRESH_MAD; - Serial.println("Threshold mode: MAD-based adaptive"); - } - else if (command.startsWith("SET ")) { - // Parse SET commands - int firstSpace = command.indexOf(' ', 4); - if (firstSpace > 0) { - String key = command.substring(4, firstSpace); - String valueStr = command.substring(firstSpace + 1); - float value = valueStr.toFloat(); - - Serial.printf("SET command: %s = %.3f\n", key.c_str(), value); - Serial.println("Note: Runtime parameter changes require code modification for persistence"); - } else { - Serial.println("Invalid SET syntax. Use: SET "); - } - } - else if (command.length() > 0) { - Serial.printf("Unknown command: %s\n", command.c_str()); - Serial.println("Type HELP for available commands"); - } -} - -// ========== MAIN SENSOR PROCESSING ========== - -void processSensorCycle() { - static float rs9_low = NAN; - static float rs9_high = NAN; - - unsigned long now = millis(); - unsigned long phaseElapsed = now - phaseStartTime; - unsigned long phaseDuration = (currentPhase == PHASE_LOW) ? T_LOW_MS : T_HIGH_MS; - - if (phaseElapsed < phaseDuration) return; - - // 1) SAMPLE at end of CURRENT phase (BEFORE switching) - float adc9 = readADC(PIN_MQ9_AO); - float adc135 = readADC(PIN_MQ135_AO); - float Rs9 = computeRs(adc9); - float Rs135 = computeRs(adc135); - - if (currentPhase == PHASE_LOW) { - rs9_low = Rs9; - } else { - rs9_high = Rs9; - } - - // 2) TOGGLE PHASE and start its timer - currentPhase = (currentPhase == PHASE_LOW) ? PHASE_HIGH : PHASE_LOW; - applyHeaterPhase(currentPhase); - - // 3) If we just finished a HIGH phase (i.e., we switched to LOW), we have a full cycle - if (currentPhase == PHASE_LOW) { - cycleCount++; - - // --- Compute K values from the cached phase resistances --- - float K9Low = 1.0f; - float K9High = 1.0f; - float K135 = 1.0f; - - if (calibrated && R0_mq9 > 0.0f && R0_mq135 > 0.0f) { - if (!isnan(rs9_low)) K9Low = clampFloat(rs9_low / R0_mq9, 0.01f, 100.0f); - if (!isnan(rs9_high)) K9High = clampFloat(rs9_high / R0_mq9, 0.01f, 100.0f); - K135 = clampFloat(Rs135 / R0_mq135, 0.01f, 100.0f); - lastK9Low = K9Low; - lastK9High = K9High; - lastK135 = K135; - } - - // --- Median filtering on real K values --- - float filteredK9Low = medianFilter(medianK9Low, K9Low); - float filteredK9High = medianFilter(medianK9High, K9High); - float filteredK135 = medianFilter(medianK135, K135); - medianIndex = (medianIndex + 1) % MED_WIN; - - // --- Update indices (EMA) --- - I_FLAM = ema(I_FLAM, filteredK9High, EMA_ALPHA); - I_VOC = ema(I_VOC, filteredK135, EMA_ALPHA); - if (filteredK9High > 0.001f) { - float newI_CO = filteredK9Low / filteredK9High; - I_CO = ema(I_CO, newI_CO, EMA_ALPHA); - } - - // --- Slope buffers --- - slopeBufferFLAM[slopeIndex] = I_FLAM; - slopeBufferVOC[slopeIndex] = I_VOC; - slopeIndex = (slopeIndex + 1) % SLOPE_WIN; - - // --- Stats, alerts, classification --- - updateRollingStats(); - updateAlerts(); - - EventType eventType; - Subtype subtype; - float confidence; - classify(eventType, subtype, confidence); - - // --- Output --- - if (!csvMode) { - // Note: Rs9_low/high are shown as same Rs9 here; you can print both if you prefer - printHuman(adc9, adc135, rs9_low, rs9_high, Rs135, - filteredK9Low, filteredK9High, filteredK135, - eventType, subtype, confidence); - } - printJSON(adc9, adc135, filteredK9Low, filteredK9High, filteredK135, - eventType, subtype, confidence); - printCSV(adc9, adc135, rs9_low, rs9_high, Rs135, - filteredK9Low, filteredK9High, filteredK135, - eventType, confidence); - } -} - - - -// ========== INITIALIZATION ========== - -void markFirstBootComplete() { - if (!prefs.begin("gasmonitor", false)) return; - prefs.putBool("booted", true); - prefs.end(); -} - -// ========================== SETUP & LOOP ============================= // -void calibrateSensors(); // forward declaration - -void setup() { - Serial.begin(115200); - delay(1000); - - Serial.println("ESP32 Gas Sensor Monitor (MQ-9 + MQ-135)"); - - // Heater pin setup - pinMode(PIN_MQ9_HEATER, OUTPUT); - applyHeaterPhase(PHASE_LOW); - - // --- LEDC / PWM setup for heater --- - ledcSetup(HEATER_CHANNEL, HEATER_FREQ, HEATER_RESOLUTION); // 1 kHz, 8-bit - ledcAttachPin(PIN_MQ9_HEATER, HEATER_CHANNEL); - - // Calibrate sensors - calibrateSensors(); - - // Initialize buffers - for (int i = 0; i < MED_WIN; i++) { - medianK9Low[i] = medianK9High[i] = medianK135[i] = 1.0f; - } - for (int i = 0; i < SLOPE_WIN; i++) { - slopeBufferFLAM[i] = slopeBufferVOC[i] = 0.0f; - } - - lastRollingUpdate = millis(); - phaseStartTime = millis(); - currentPhase = PHASE_LOW; -} - -void loop() { - // Process heater-driven measurement cycle - processSensorCycle(); - - // Handle serial commands for mode switching - if (Serial.available()) { - char c = Serial.read(); - if (c == 'c' || c == 'C') { - csvMode = !csvMode; - Serial.printf("CSV mode %s\n", csvMode ? "enabled" : "disabled"); - } - } -} - -// Function to calibrate sensors -// ================== Sensor Calibration Function ================== -void calibrateSensors() { - Serial.println("=== Sensor Calibration Started ==="); - - // Number of samples to take for calibration - const int numSamples = 100; - long sensorSum = 0; - - // Simulated sensor pin (change this to your actual sensor input) - int sensorPin = A0; - - // Collect samples - for (int i = 0; i < numSamples; i++) { - int reading = analogRead(sensorPin); // Replace with your sensor's read function - sensorSum += reading; - delay(10); // small delay between samples - } - - // Compute average offset (baseline value) - int sensorOffset = sensorSum / numSamples; - - // Store or print offset - Serial.print("Calibration complete. Baseline value: "); - Serial.println(sensorOffset); - - // You can store the offset in a global variable for later use - // Example: globalSensorOffset = sensorOffset; -} - - - - -/* - * EXAMPLE SERIAL OUTPUT: - * - * === Cycle 45 | Phase: HIGH === - * Raw ADC: MQ9=1250, MQ135=890 - * Rs (Ohm): MQ9_low=15420, MQ9_high=12100, MQ135=18900 - * K (Rs/R0): MQ9_low=1.15, MQ9_high=0.95, MQ135=1.82 - * Indices: I_CO=1.21, I_FLAM=0.95, I_VOC=1.82 - * Stats: μ(1.05,1.02,1.15) MAD(0.08,0.12,0.25) - * Alerts: WARN(--V) DANGER(---) - * Classification: VOC_LIKELY (conf=0.67) - * Armed: YES | Rebasing: NO - * - * {"cycle":45,"mq135_k":1.82,"mq9_k_low":1.15,"mq9_k_high":0.95,"i_co":1.21, - * "i_flam":0.95,"i_voc":1.82,"warn":{"co":false,"flam":false,"voc":true}, - * "danger":{"co":false,"flam":false,"voc":false},"event_type":2,"subtype":10, - * "confidence":0.67,"armed":true,"rebasing":false} - * - * COMMANDS TESTED: - * > HELP - * > STATUS - * > REBASE - * > CSV ON - * > THRESH MAD - * > SET EMA_ALPHA 0.25 - */ diff --git a/prototype2.cpp b/prototype2.cpp deleted file mode 100644 index c9f1947..0000000 --- a/prototype2.cpp +++ /dev/null @@ -1,521 +0,0 @@ -/* - Complete ESP32 Multi-Sensor System with Firebase - - MQ2, MQ9, MQ135 Gas Sensors - - HTU21D Temperature & Humidity - - FN-M16P Audio Module with PAM8403 Amplifier - - LoRa Communication Module - - WiFi & Firebase Integration - - NTP Time Synchronization -*/ - -#include -#include -#include -#include -#include -#include -#include - -// Firebase helper -#include "addons/TokenHelper.h" -#include "addons/RTDBHelper.h" - -// HTU21D Library (install "Adafruit HTU21DF Library" from Library Manager) -#include - -// WiFi Credentials -#define WIFI_SSID "419" -#define WIFI_PASSWORD "xyz@1234" - -// Firebase Configuration -#define API_KEY "AIzaSyDy49OYNumyIrIBrPTOP8dvkeYZGkXn4ac" -#define DATABASE_URL "https://siren-4951f-default-rtdb.asia-southeast1.firebasedatabase.app/" - -// Firebase objects -FirebaseData fbdo; -FirebaseAuth auth; -FirebaseConfig config; - -// NTP Configuration -const char* ntpServer = "pool.ntp.org"; -const long gmtOffset_sec = 19800; // GMT+5:30 India -const int daylightOffset_sec = 0; - -// Pin definitions for MQ Sensors -#define MQ2_DIGITAL_PIN 18 -#define MQ2_ANALOG_PIN 32 -#define MQ9_DIGITAL_PIN 19 -#define MQ9_ANALOG_PIN 33 -#define MQ135_DIGITAL_PIN 21 -#define MQ135_ANALOG_PIN 35 - -// Pin definitions for FN-M16P Audio Module -#define FN_M16P_RX 16 // Connect to TX of FN-M16P -#define FN_M16P_TX 17 // Connect to RX of FN-M16P - -// LoRa Module (SPI) -#define LORA_SCK 14 -#define LORA_MISO 12 -#define LORA_MOSI 13 -#define LORA_SS 15 -#define LORA_RST 2 -#define LORA_DIO0 4 - -// Thresholds -#define MQ2_THRESHOLD 2000 -#define MQ9_THRESHOLD 1800 -#define MQ135_THRESHOLD 1000 - -// Initialize modules -Adafruit_HTU21DF htu = Adafruit_HTU21DF(); -SoftwareSerial fnM16pSerial(FN_M16P_TX, FN_M16P_RX); - -// Status flags -bool wifiConnected = false; -bool firebaseReady = false; -bool ntpSynced = false; -bool audioReady = false; - -// Data structure for sensor readings -struct SensorData { - float temperature; - float humidity; - int mq2_analog; - int mq9_analog; - int mq135_analog; - bool mq2_digital; - bool mq9_digital; - bool mq135_digital; - unsigned long timestamp; - String dateTime; -}; - -// FN-M16P Command Functions -void fnM16pSendCommand(uint8_t cmd, uint16_t data) { - uint8_t packet[8] = {0x7E, 0xFF, 0x06, cmd, 0x00, (uint8_t)(data >> 8), (uint8_t)(data & 0xFF), 0xEF}; - fnM16pSerial.write(packet, 8); - delay(100); -} - -void fnM16pSetVolume(uint8_t volume) { - // Volume range: 0-30 - fnM16pSendCommand(0x06, volume); - Serial.println("FN-M16P: Volume set to " + String(volume)); -} - -void fnM16pPlayFile(uint16_t fileNumber) { - fnM16pSendCommand(0x03, fileNumber); - Serial.println("FN-M16P: Playing file " + String(fileNumber, 4) + ".mp3"); -} - -void fnM16pStop() { - fnM16pSendCommand(0x16, 0); - Serial.println("FN-M16P: Stopped playback"); -} - -void fnM16pPause() { - fnM16pSendCommand(0x0E, 0); - Serial.println("FN-M16P: Paused playback"); -} - -void fnM16pResume() { - fnM16pSendCommand(0x0D, 0); - Serial.println("FN-M16P: Resumed playback"); -} - -bool fnM16pIsPlaying() { - // Send query command for current status - fnM16pSendCommand(0x42, 0); - delay(100); - - // Check for response (simplified - you might want to implement proper response parsing) - return fnM16pSerial.available() > 0; -} - -void setup() { - Serial.begin(115200); - Serial.println("================================="); - Serial.println("ESP32 Complete Sensor System v3.0"); - Serial.println("With FN-M16P Audio & Firebase"); - Serial.println("================================="); - - // Initialize WiFi - initializeWiFi(); - - // Initialize NTP - if (wifiConnected) { - initializeNTP(); - } - - // Initialize Firebase - if (wifiConnected) { - initializeFirebase(); - } - - // Initialize MQ sensor pins - pinMode(MQ2_DIGITAL_PIN, INPUT); - pinMode(MQ9_DIGITAL_PIN, INPUT); - pinMode(MQ135_DIGITAL_PIN, INPUT); - - // Initialize I2C for HTU21D on custom pins - Wire.begin(26, 25); // SDA=26, SCL=25 - if (!htu.begin()) { - Serial.println("HTU21D not detected. Check wiring."); - } else { - Serial.println("HTU21D initialized successfully!"); - } - - // Initialize FN-M16P Audio Module - fnM16pSerial.begin(9600); - delay(1000); // Give FN-M16P time to initialize - - Serial.println("Initializing FN-M16P Audio Module..."); - fnM16pSetVolume(20); // Set volume (0-30) - delay(500); - - // Test if FN-M16P is responding - audioReady = true; // Assume it's working (FN-M16P doesn't have easy status check) - Serial.println("FN-M16P initialized successfully!"); - Serial.println("Audio files should be named: 0001.mp3, 0002.mp3, etc. in root directory"); - - // Initialize LoRa - SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_SS); - LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0); - - if (!LoRa.begin(433E6)) { // 433MHz frequency - Serial.println("LoRa initialization failed. Check wiring."); - } else { - Serial.println("LoRa initialized successfully!"); - LoRa.setTxPower(20); // Set transmission power - } - - Serial.println("Warming up gas sensors..."); - delay(10000); // 10 second warm-up - Serial.println("System ready!"); - Serial.println(); - - // Play startup sound - if (audioReady) { - fnM16pPlayFile(1); // Play 0001.mp3 (startup/system ready) - delay(2000); - } -} - -void loop() { - // Check WiFi connection - if (WiFi.status() != WL_CONNECTED && wifiConnected) { - Serial.println("WiFi disconnected. Reconnecting..."); - initializeWiFi(); - } - - // Read all sensors - SensorData data = readAllSensors(); - - // Display readings - displayReadings(data); - - // Check for alerts - checkAlerts(data); - - // Send data to Firebase - if (firebaseReady) { - sendToFirebase(data); - } - - // Send LoRa data - sendLoRaData(data); - - Serial.println("------------------------"); - delay(10000); // Read every 10 seconds for Firebase -} - -void initializeWiFi() { - Serial.print("Connecting to WiFi"); - 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) { - wifiConnected = true; - Serial.println(); - Serial.println("WiFi connected!"); - Serial.print("IP address: "); - Serial.println(WiFi.localIP()); - } else { - wifiConnected = false; - Serial.println(); - Serial.println("WiFi connection failed!"); - } -} - -void initializeNTP() { - Serial.println("Synchronizing time with NTP server..."); - configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); - - struct tm timeinfo; - int attempts = 0; - while (!getLocalTime(&timeinfo) && attempts < 10) { - delay(1000); - Serial.print("."); - attempts++; - } - - if (getLocalTime(&timeinfo)) { - ntpSynced = true; - Serial.println(); - Serial.println("NTP time synchronized!"); - Serial.printf("Current time: %s", asctime(&timeinfo)); - } else { - ntpSynced = false; - Serial.println(); - Serial.println("Failed to sync NTP time!"); - } -} - -void initializeFirebase() { - Serial.println("Initializing Firebase..."); - - // Configure Firebase - config.api_key = API_KEY; - config.database_url = DATABASE_URL; - - // Anonymous authentication - Serial.println("Signing up for anonymous authentication..."); - if (Firebase.signUp(&config, &auth, "", "")) { - Serial.println("Firebase authentication successful!"); - firebaseReady = true; - } else { - Serial.printf("Firebase authentication failed: %s\n", config.signer.signupError.message.c_str()); - firebaseReady = false; - } - - // Assign callback function for token generation - config.token_status_callback = tokenStatusCallback; - - // Initialize Firebase - Firebase.begin(&config, &auth); - Firebase.reconnectWiFi(true); - - Serial.println("Firebase initialized!"); -} - -SensorData readAllSensors() { - SensorData data; - data.timestamp = millis(); - data.dateTime = getCurrentDateTime(); - - // Read HTU21D - data.temperature = htu.readTemperature(); - data.humidity = htu.readHumidity(); - - - // Handle sensor read errors - if (isnan(data.temperature)) data.temperature = -999; - if (isnan(data.humidity)) data.humidity = -999; - - // Read MQ sensors - data.mq2_analog = analogRead(MQ2_ANALOG_PIN); - data.mq9_analog = analogRead(MQ9_ANALOG_PIN); - data.mq135_analog = analogRead(MQ135_ANALOG_PIN); - - data.mq2_digital = digitalRead(MQ2_DIGITAL_PIN) == LOW; - data.mq9_digital = digitalRead(MQ9_DIGITAL_PIN) == LOW; - data.mq135_digital = digitalRead(MQ135_DIGITAL_PIN) == LOW; - - return data; -} - -String getCurrentDateTime() { - if (!ntpSynced) return "Time not synced"; - - struct tm timeinfo; - if (!getLocalTime(&timeinfo)) { - return "Failed to get time"; - } - - char timeString[64]; - strftime(timeString, sizeof(timeString), "%Y-%m-%d %H:%M:%S", &timeinfo); - return String(timeString); -} - -void displayReadings(SensorData data) { - Serial.println("=== SENSOR READINGS ==="); - Serial.println("Time: " + data.dateTime); - Serial.println("WiFi: " + String(wifiConnected ? "Connected" : "Disconnected")); - Serial.println("Firebase: " + String(firebaseReady ? "Ready" : "Not Ready")); - Serial.println("Audio: " + String(audioReady ? "Ready" : "Not Ready")); - Serial.println(); - - // Environmental data - Serial.println("HTU21D (Temperature & Humidity):"); - if (data.temperature != -999 && data.humidity != -999) { - Serial.printf(" Temperature: %.2f°C\n", data.temperature); - Serial.printf(" Humidity: %.2f%%\n", data.humidity); - } else { - Serial.println(" HTU21D reading error!"); - } - Serial.println(); - - // Gas sensors - Serial.println("MQ2 (Smoke/LPG/Gas):"); - Serial.printf(" Digital: %s | Analog: %d", - data.mq2_digital ? "GAS DETECTED" : "No Gas", data.mq2_analog); - Serial.println(data.mq2_analog > MQ2_THRESHOLD ? " - HIGH!" : " - Normal"); - - Serial.println("MQ9 (Carbon Monoxide):"); - Serial.printf(" Digital: %s | Analog: %d", - data.mq9_digital ? "CO DETECTED" : "No CO", data.mq9_analog); - Serial.println(data.mq9_analog > MQ9_THRESHOLD ? " - HIGH!" : " - Normal"); - - Serial.println("MQ135 (Air Quality/CO2):"); - Serial.printf(" Digital: %s | Analog: %d", - data.mq135_digital ? "POOR AIR" : "Good Air", data.mq135_analog); - Serial.println(data.mq135_analog > MQ135_THRESHOLD ? " - POOR!" : " - Good"); -} - -void checkAlerts(SensorData data) { - static unsigned long lastAlert = 0; - unsigned long now = millis(); - - // Avoid too frequent alerts (minimum 30 seconds between alerts) - if (now - lastAlert < 30000) return; - - // Check for dangerous conditions - if (data.mq2_analog > MQ2_THRESHOLD || data.mq2_digital) { - Serial.println("🚨 ALERT: Smoke/Gas detected!"); - playAlert(2); // Play 0002.mp3 - Smoke/Gas Alert - sendAlert("SMOKE_GAS_ALERT", "Smoke or gas detected!", data); - lastAlert = now; - } - else if (data.mq9_analog > MQ9_THRESHOLD || data.mq9_digital) { - Serial.println("🚨 ALERT: Carbon Monoxide detected!"); - playAlert(3); // Play 0003.mp3 - Carbon Monoxide Alert - sendAlert("CO_ALERT", "Carbon monoxide detected!", data); - lastAlert = now; - } - else if (data.mq135_analog > MQ135_THRESHOLD || data.mq135_digital) { - Serial.println("⚠️ WARNING: Poor air quality!"); - playAlert(4); // Play 0004.mp3 - Air Quality Warning - sendAlert("AIR_QUALITY_WARNING", "Poor air quality detected!", data); - lastAlert = now; - } - - // Temperature alerts - if (data.temperature > 40.0 && data.temperature != -999) { - Serial.println("🌡️ ALERT: High temperature!"); - playAlert(5); // Play 0005.mp3 - High Temperature Alert - sendAlert("HIGH_TEMP_ALERT", "High temperature detected!", data); - lastAlert = now; - } - else if (data.temperature < 5.0 && data.temperature != -999) { - Serial.println("❄️ ALERT: Low temperature!"); - playAlert(6); // Play 0006.mp3 - Low Temperature Alert - sendAlert("LOW_TEMP_ALERT", "Low temperature detected!", data); - lastAlert = now; - } - - // Humidity alerts - if (data.humidity > 85.0 && data.humidity != -999) { - Serial.println("💧 ALERT: High humidity!"); - playAlert(7); // Play 0007.mp3 - High Humidity Alert - sendAlert("HIGH_HUMIDITY_ALERT", "High humidity detected!", data); - lastAlert = now; - } - else if (data.humidity < 20.0 && data.humidity != -999) { - Serial.println("🏜️ ALERT: Low humidity!"); - playAlert(8); // Play 0008.mp3 - Low Humidity Alert - sendAlert("LOW_HUMIDITY_ALERT", "Low humidity detected!", data); - lastAlert = now; - } -} - -void sendToFirebase(SensorData data) { - if (!Firebase.ready()) return; - - // Create JSON object for sensor data - FirebaseJson json; - json.set("timestamp", data.timestamp); - json.set("dateTime", data.dateTime); - json.set("temperature", data.temperature); - json.set("humidity", data.humidity); - json.set("mq2_analog", data.mq2_analog); - json.set("mq9_analog", data.mq9_analog); - json.set("mq135_analog", data.mq135_analog); - json.set("mq2_digital", data.mq2_digital); - json.set("mq9_digital", data.mq9_digital); - json.set("mq135_digital", data.mq135_digital); - json.set("air_quality", getAirQualityRating(data.mq135_analog)); - - // Send to Firebase with timestamp as key - String path = "/sensor_data/" + String(data.timestamp); - - if (Firebase.RTDB.setJSON(&fbdo, path.c_str(), &json)) { - Serial.println("✅ Data sent to Firebase successfully!"); - } else { - Serial.println("❌ Firebase send failed: " + fbdo.errorReason()); - } - - // Also update latest readings - if (Firebase.RTDB.setJSON(&fbdo, "/latest_reading", &json)) { - Serial.println("✅ Latest reading updated!"); - } -} - -void sendAlert(String alertType, String message, SensorData data) { - if (!Firebase.ready()) return; - - FirebaseJson alertJson; - alertJson.set("alertType", alertType); - alertJson.set("message", message); - alertJson.set("timestamp", data.timestamp); - alertJson.set("dateTime", data.dateTime); - alertJson.set("temperature", data.temperature); - alertJson.set("humidity", data.humidity); - alertJson.set("mq2_analog", data.mq2_analog); - alertJson.set("mq9_analog", data.mq9_analog); - alertJson.set("mq135_analog", data.mq135_analog); - - String alertPath = "/alerts/" + String(data.timestamp); - - if (Firebase.RTDB.setJSON(&fbdo, alertPath.c_str(), &alertJson)) { - Serial.println("🚨 Alert sent to Firebase!"); - } else { - Serial.println("❌ Alert send failed: " + fbdo.errorReason()); - } -} - -void playAlert(int fileNumber) { - if (audioReady) { - fnM16pPlayFile(fileNumber); - delay(500); // Allow time for playback to start - } -} - -void sendLoRaData(SensorData data) { - String packet = "SENSOR_DATA,"; - packet += String(data.timestamp) + ","; - packet += String(data.temperature, 2) + ","; - packet += String(data.humidity, 2) + ","; - packet += String(data.mq2_analog) + ","; - packet += String(data.mq9_analog) + ","; - packet += String(data.mq135_analog); - - LoRa.beginPacket(); - LoRa.print(packet); - LoRa.endPacket(); - - Serial.println("📡 LoRa packet sent: " + packet); -} - -String getAirQualityRating(int value) { - if (value < 800) return "Excellent"; - else if (value < 1200) return "Good"; - else if (value < 1800) return "Moderate"; - else if (value < 2400) return "Poor"; - else return "Very Poor"; -} diff --git a/try_central_for_lora.cpp b/try_central_for_lora.cpp deleted file mode 100644 index 11c28f0..0000000 --- a/try_central_for_lora.cpp +++ /dev/null @@ -1,764 +0,0 @@ -/* - ESP32 LoRa Central Node - Supabase Gateway + Email Alerts - FIXED: Enhanced SMTP error logging and connection handling -*/ - -#include -#include -#include -#include -#include -#include -#include - -#define WIFI_SSID "iPhone" -#define WIFI_PASSWORD "12345678" - -#define SUPABASE_URL "https://kfwngukvlsjjhwslktbn.supabase.co" -#define SUPABASE_ANON_KEY "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imtmd25ndWt2bHNqamh3c2xrdGJuIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTgzNzYwMzksImV4cCI6MjA3Mzk1MjAzOX0.qY_JlPE6g5ewfBodJZYDS6ABFySvEMLgqOhCeQg8U8I" - -#define SMTP_SERVER "smtp.gmail.com" -#define SMTP_PORT 465 -#define SENDER_EMAIL "caneriesiren@gmail.com" -#define SENDER_PASSWORD "jczhurwioeagagiw" // App password without spaces -#define SUPERVISOR_EMAIL "caneriesiren@gmail.com" - -#define DEBUG_MODE true - -const char* ntpServer = "pool.ntp.org"; -const long gmtOffset_sec = 19800; -const int daylightOffset_sec = 0; - -#define LORA_SCK 5 -#define LORA_MISO 19 -#define LORA_MOSI 27 -#define LORA_SS 18 -#define LORA_RST 14 -#define LORA_DIO0 2 -#define LORA_BAND 915E6 -#define CENTRAL_NODE_ID "CENTRAL_GATEWAY_001" - -#define EMAIL_SEND_TIMEOUT 300000 -#define EMAIL_BUFFER_SIZE 10 - -bool wifiConnected = false; -bool supabaseReady = false; -bool ntpSynced = false; -bool loraReady = false; - -unsigned long packetsReceived = 0; -unsigned long packetsUploaded = 0; -unsigned long packetsCorrupted = 0; -unsigned long emergenciesDetected = 0; -unsigned long emailsSent = 0; -unsigned long lastStatsDisplay = 0; -unsigned long lastWiFiCheck = 0; -unsigned long lastNTPSync = 0; - -struct EmergencyRecord { - String nodeId; - unsigned long lastAlertTime; -}; - -EmergencyRecord emergencyBuffer[EMAIL_BUFFER_SIZE]; -int emergencyBufferIndex = 0; - -HTTPClient http; -WiFiClientSecure client; - -void displayStatistics(); -String getCurrentDateTime(); -bool canSendEmergencyEmail(String nodeId); -void recordEmergency(String nodeId); -void sendEmergencyEmail(JsonDocument& sensorData, int rssi, float snr); -bool sendSMTPEmail(String subject, String body); -String base64Encode(String input); -String readSMTPResponse(WiFiClient& client, int timeout = 5000); - -void setup() { - Serial.begin(115200); - delay(2000); - - Serial.println("\n====================================="); - Serial.println("ESP32 LoRa Central Node v2.1"); - Serial.println("Email Debug Enhanced"); - Serial.println("=====================================\n"); - - for (int i = 0; i < EMAIL_BUFFER_SIZE; i++) { - emergencyBuffer[i].nodeId = ""; - emergencyBuffer[i].lastAlertTime = 0; - } - - initializeLoRa(); - initializeWiFi(); - - if (wifiConnected) { - initializeNTP(); - initializeSupabase(); - } - - Serial.println("Central Node ready!\n"); -} - -void loop() { - if (millis() - lastWiFiCheck > 30000) { - checkWiFiConnection(); - lastWiFiCheck = millis(); - } - - if (!ntpSynced && wifiConnected && (millis() - lastNTPSync > 300000)) { - initializeNTP(); - lastNTPSync = millis(); - } - - if (loraReady) { - handleLoRaPackets(); - } - - if (millis() - lastStatsDisplay > 60000) { - displayStatistics(); - lastStatsDisplay = millis(); - } - - delay(50); -} - -void initializeLoRa() { - Serial.println("Initializing LoRa..."); - - SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_SS); - LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0); - - if (!LoRa.begin(LORA_BAND)) { - Serial.println("ERROR: LoRa failed!"); - loraReady = false; - return; - } - - LoRa.setTxPower(20); - LoRa.setSpreadingFactor(12); - LoRa.setSignalBandwidth(125E3); - LoRa.setCodingRate4(8); - LoRa.setPreambleLength(8); - LoRa.setSyncWord(0x34); - - Serial.println("LoRa OK\n"); - loraReady = true; -} - -void initializeWiFi() { - Serial.print("Connecting to WiFi"); - WiFi.mode(WIFI_STA); - WiFi.begin(WIFI_SSID, WIFI_PASSWORD); - - int attempts = 0; - while (WiFi.status() != WL_CONNECTED && attempts < 30) { - delay(500); - Serial.print("."); - attempts++; - } - - if (WiFi.status() == WL_CONNECTED) { - wifiConnected = true; - Serial.println("\nWiFi OK"); - Serial.println("IP: " + WiFi.localIP().toString() + "\n"); - } else { - wifiConnected = false; - Serial.println("\nWiFi FAILED\n"); - } -} - -void checkWiFiConnection() { - if (WiFi.status() != WL_CONNECTED && wifiConnected) { - Serial.println("WiFi lost. Reconnecting..."); - wifiConnected = false; - supabaseReady = false; - initializeWiFi(); - - if (wifiConnected && !supabaseReady) { - initializeSupabase(); - } - } -} - -void initializeNTP() { - if (!wifiConnected) return; - - Serial.println("Syncing NTP..."); - configTime(gmtOffset_sec, daylightOffset_sec, "pool.ntp.org"); - - struct tm timeinfo; - int attempts = 0; - while (!getLocalTime(&timeinfo) && attempts < 20) { - delay(500); - attempts++; - } - - if (attempts < 20) { - ntpSynced = true; - Serial.println("NTP OK\n"); - } -} - -void initializeSupabase() { - if (!wifiConnected) return; - - Serial.println("Testing Supabase connection..."); - client.setInsecure(); - - String testUrl = String(SUPABASE_URL) + "/rest/v1/"; - http.begin(client, testUrl); - http.addHeader("Content-Type", "application/json"); - http.addHeader("apikey", SUPABASE_ANON_KEY); - - int httpResponseCode = http.GET(); - - if (httpResponseCode > 0) { - Serial.println("Supabase OK\n"); - supabaseReady = true; - } else { - Serial.println("Supabase FAILED\n"); - supabaseReady = false; - } - - http.end(); -} - -void handleLoRaPackets() { - int packetSize = LoRa.parsePacket(); - if (packetSize == 0) return; - - String receivedPacket = ""; - while (LoRa.available()) { - char c = (char)LoRa.read(); - if (c >= 20 && c <= 126) { - receivedPacket += c; - } - } - - if (receivedPacket.length() < 5) return; - - int rssi = LoRa.packetRssi(); - float snr = LoRa.packetSnr(); - - packetsReceived++; - - Serial.println("========================"); - Serial.println("LoRa Packet #" + String(packetsReceived)); - Serial.println("RSSI: " + String(rssi) + " | SNR: " + String(snr, 1)); - Serial.println("Size: " + String(receivedPacket.length()) + " bytes"); - - String cleanedPacket = cleanJsonPacket(receivedPacket); - if (cleanedPacket.length() > 0) { - processAndUploadPacket(cleanedPacket, rssi, snr); - } else { - packetsCorrupted++; - Serial.println("ERROR: Packet cleanup failed"); - } - Serial.println("========================\n"); -} - -String cleanJsonPacket(String rawPacket) { - DynamicJsonDocument testDoc(512); - if (deserializeJson(testDoc, rawPacket) == DeserializationError::Ok) { - return rawPacket; - } - - String cleaned = rawPacket; - - if (cleaned.endsWith(",p")) { - cleaned = cleaned.substring(0, cleaned.length() - 2) + "od\"}"; - } else if (cleaned.endsWith("Go,p")) { - cleaned = cleaned.substring(0, cleaned.length() - 4) + "Good\"}"; - } - - int openBraces = 0, closeBraces = 0; - for (char c : cleaned) { - if (c == '{') openBraces++; - if (c == '}') closeBraces++; - } - - while (closeBraces < openBraces) { - cleaned += "}"; - closeBraces++; - } - - int quotes = 0; - for (char c : cleaned) { - if (c == '"') quotes++; - } - - if (quotes % 2 != 0) cleaned += "\""; - - return cleaned; -} - -void processAndUploadPacket(String cleanedPacket, int rssi, float snr) { - DynamicJsonDocument receivedDoc(768); - - DeserializationError error = deserializeJson(receivedDoc, cleanedPacket); - if (error) { - packetsCorrupted++; - Serial.println("ERROR: JSON parse failed: " + String(error.c_str())); - Serial.println("Raw packet: " + cleanedPacket); - return; - } - - // DEBUG: Print entire received JSON - if (DEBUG_MODE) { - Serial.println("\n=== RECEIVED JSON DEBUG ==="); - serializeJsonPretty(receivedDoc, Serial); - Serial.println("\n==========================="); - } - - // CRITICAL FIX: Check for emergency field more robustly - bool isEmergency = false; - - // Method 1: Check if field exists and is true - if (receivedDoc.containsKey("emergency")) { - JsonVariant emergencyVar = receivedDoc["emergency"]; - - if (emergencyVar.is()) { - isEmergency = emergencyVar.as(); - } else if (emergencyVar.is()) { - isEmergency = (emergencyVar.as() != 0); - } else if (emergencyVar.is()) { - String emergencyStr = emergencyVar.as(); - emergencyStr.toLowerCase(); - isEmergency = (emergencyStr == "true" || emergencyStr == "1"); - } - - Serial.println("Emergency field found: " + String(isEmergency ? "TRUE" : "FALSE")); - } else { - Serial.println("WARNING: No 'emergency' field in JSON"); - } - - String nodeId = receivedDoc["node"] | "UNKNOWN"; - - // Handle emergency - if (isEmergency) { - emergenciesDetected++; - - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.println("║ 🚨 EMERGENCY SIGNAL RECEIVED! 🚨 ║"); - Serial.println("║ Node: " + nodeId + String(35 - nodeId.length(), ' ') + "║"); - Serial.println("║ Emergency Count: " + String(emergenciesDetected) + String(27 - String(emergenciesDetected).length(), ' ') + "║"); - Serial.println("╚════════════════════════════════════════════╝\n"); - - if (canSendEmergencyEmail(nodeId)) { - Serial.println(">>> SENDING EMERGENCY EMAIL <<<"); - sendEmergencyEmail(receivedDoc, rssi, snr); - recordEmergency(nodeId); - } else { - unsigned long timeSinceLastEmail = 0; - for (int i = 0; i < EMAIL_BUFFER_SIZE; i++) { - if (emergencyBuffer[i].nodeId == nodeId) { - timeSinceLastEmail = (millis() - emergencyBuffer[i].lastAlertTime) / 1000; - break; - } - } - Serial.println("⏱ Rate limit active for node " + nodeId); - Serial.println(" Time since last email: " + String(timeSinceLastEmail) + "s"); - Serial.println(" Cooldown period: " + String(EMAIL_SEND_TIMEOUT/1000) + "s"); - Serial.println(" Email skipped\n"); - } - } else { - if (DEBUG_MODE) { - Serial.println("📊 Normal packet (non-emergency) from node " + nodeId); - } - } - - // Build upload document - DynamicJsonDocument uploadDoc(1536); - uploadDoc["sensor_node_id"] = nodeId; - uploadDoc["sensor_packet_count"] = 0; - uploadDoc["sensor_timestamp"] = receivedDoc["timestamp"] | 0; - uploadDoc["temperature"] = receivedDoc["temp"]; - uploadDoc["humidity"] = receivedDoc["hum"] | 0; - uploadDoc["mq2_analog"] = receivedDoc["mq2"] | 0; - uploadDoc["mq9_analog"] = receivedDoc["mq9"] | 0; - uploadDoc["mq135_analog"] = receivedDoc["mq135"] | 0; - uploadDoc["mq2_digital"] = false; - uploadDoc["mq9_digital"] = false; - uploadDoc["mq135_digital"] = false; - uploadDoc["air_quality"] = "Unknown"; - uploadDoc["motion_accel"] = receivedDoc["motion_accel"] | 0; - uploadDoc["motion_gyro"] = receivedDoc["motion_gyro"] | 0; - uploadDoc["bpm"] = receivedDoc["bpm"] | 0; - uploadDoc["spo2"] = receivedDoc["spo2"] | 0; - uploadDoc["wristband_connected"] = receivedDoc["wristband_connected"] | 0; - uploadDoc["emergency"] = isEmergency; // Add emergency field to database - uploadDoc["central_node_id"] = CENTRAL_NODE_ID; - uploadDoc["received_time"] = getCurrentDateTime(); - uploadDoc["received_timestamp"] = millis(); - uploadDoc["rssi"] = rssi; - uploadDoc["snr"] = snr; - uploadDoc["gateway_packet_count"] = packetsReceived; - - String jsonString; - serializeJson(uploadDoc, jsonString); - - if (DEBUG_MODE || isEmergency) { - Serial.println("Upload Payload: " + jsonString); - } - - if (supabaseReady) { - uploadToSupabase(jsonString); - } else { - Serial.println("WARNING: Supabase not ready - data not uploaded"); - } -} - -bool canSendEmergencyEmail(String nodeId) { - unsigned long now = millis(); - - for (int i = 0; i < EMAIL_BUFFER_SIZE; i++) { - if (emergencyBuffer[i].nodeId == nodeId) { - if (now - emergencyBuffer[i].lastAlertTime >= EMAIL_SEND_TIMEOUT) { - return true; - } else { - return false; - } - } - } - - return true; -} - -void recordEmergency(String nodeId) { - unsigned long now = millis(); - - for (int i = 0; i < EMAIL_BUFFER_SIZE; i++) { - if (emergencyBuffer[i].nodeId == nodeId) { - emergencyBuffer[i].lastAlertTime = now; - return; - } - } - - if (emergencyBufferIndex >= EMAIL_BUFFER_SIZE) { - emergencyBufferIndex = 0; - } - - emergencyBuffer[emergencyBufferIndex].nodeId = nodeId; - emergencyBuffer[emergencyBufferIndex].lastAlertTime = now; - emergencyBufferIndex++; -} - -void sendEmergencyEmail(JsonDocument& sensorData, int rssi, float snr) { - if (!wifiConnected) { - Serial.println("❌ ERROR: WiFi disconnected - cannot send email"); - return; - } - - Serial.println("\n╔══════════════════════════════════════╗"); - Serial.println("║ PREPARING EMERGENCY EMAIL ║"); - Serial.println("╚══════════════════════════════════════╝\n"); - - String nodeId = sensorData["node"] | "UNKNOWN"; - float temperature = sensorData["temp"] | 0; - float humidity = sensorData["hum"] | 0; - int mq2 = sensorData["mq2"] | 0; - int mq9 = sensorData["mq9"] | 0; - int mq135 = sensorData["mq135"] | 0; - int bpm = sensorData["bpm"] | 0; - int spo2 = sensorData["spo2"] | 0; - bool wristbandConnected = sensorData["wristband_connected"] | 0; - float motionAccel = sensorData["motion_accel"] | 0; - float motionGyro = sensorData["motion_gyro"] | 0; - - String subject = "🚨 URGENT: Mine Worker Emergency - Node " + nodeId; - - String emailBody = "╔═══════════════════════════════════════════╗\n"; - emailBody += "║ EMERGENCY DISTRESS SIGNAL DETECTED ║\n"; - emailBody += "╚═══════════════════════════════════════════╝\n\n"; - emailBody += "Node ID: " + nodeId + "\n"; - emailBody += "Timestamp: " + getCurrentDateTime() + "\n"; - emailBody += "Emergency Count: #" + String(emergenciesDetected) + "\n\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "ENVIRONMENTAL CONDITIONS:\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "Temperature: " + String(temperature, 1) + "°C\n"; - emailBody += "Humidity: " + String(humidity, 1) + "%\n"; - emailBody += "MQ2 (Smoke): " + String(mq2) + "\n"; - emailBody += "MQ9 (CO): " + String(mq9) + "\n"; - emailBody += "MQ135 (Quality): " + String(mq135) + "\n\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "MOTION & VITALS:\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "Acceleration: " + String(motionAccel, 2) + " m/s²\n"; - emailBody += "Gyro Rotation: " + String(motionGyro, 2) + " °/s\n"; - emailBody += "Heart Rate: " + String(bpm) + " BPM\n"; - emailBody += "Blood Oxygen: " + String(spo2) + "%\n"; - emailBody += "Wristband: " + String(wristbandConnected ? "✓ Connected" : "✗ Disconnected") + "\n\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "SIGNAL QUALITY:\n"; - emailBody += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; - emailBody += "RSSI: " + String(rssi) + " dBm\n"; - emailBody += "SNR: " + String(snr) + " dB\n\n"; - emailBody += "⚠️ IMMEDIATE ACTION REQUIRED!\n"; - emailBody += " Contact emergency response team.\n"; - - Serial.println("Email Subject: " + subject); - Serial.println("Email Length: " + String(emailBody.length()) + " characters"); - Serial.println(); - - // Try to send email with retries - for (int attempt = 1; attempt <= 3; attempt++) { - Serial.println("📧 Email Send Attempt " + String(attempt) + "/3..."); - - if (sendSMTPEmail(subject, emailBody)) { - emailsSent++; - Serial.println("\n✅ Email sent successfully!"); - Serial.println(" Total emails sent: " + String(emailsSent)); - Serial.println("╚══════════════════════════════════════╝\n"); - return; - } - - if (attempt < 3) { - Serial.println("❌ Attempt " + String(attempt) + " failed. Retrying in 5 seconds...\n"); - delay(5000); - } - } - - Serial.println("\n❌ ERROR: Email send failed after 3 attempts"); - Serial.println(" Check:"); - Serial.println(" 1. WiFi connection"); - Serial.println(" 2. SMTP credentials"); - Serial.println(" 3. Gmail app password"); - Serial.println(" 4. Internet connectivity"); - Serial.println("╚══════════════════════════════════════╝\n"); -} - -String readSMTPResponse(WiFiClient& client, int timeout) { - unsigned long start = millis(); - String response = ""; - - while (millis() - start < timeout) { - if (client.available()) { - char c = client.read(); - response += c; - if (c == '\n') { - Serial.print("SMTP: "); - Serial.print(response); - if (response.indexOf("250") >= 0 || response.indexOf("334") >= 0 || - response.indexOf("235") >= 0 || response.indexOf("220") >= 0 || - response.indexOf("354") >= 0) { - return response; - } - if (response.indexOf("5") == 0) { // Error codes start with 5 - Serial.println("SMTP ERROR: " + response); - return response; - } - response = ""; - } - } - delay(10); - } - - Serial.println("SMTP Timeout"); - return ""; -} - -bool sendSMTPEmail(String subject, String body) { - // Use secure client from the start and connect to port 465 (SSL/TLS) - // Gmail supports both 587 (STARTTLS) and 465 (direct SSL) - // Port 465 is simpler as it's SSL from the start - - WiFiClientSecure secureClient; - secureClient.setInsecure(); - - Serial.println("Connecting to SMTP server (SSL)..."); - if (!secureClient.connect(SMTP_SERVER, 465)) { // Use port 465 for direct SSL - Serial.println("ERROR: Cannot connect to SMTP server"); - return false; - } - Serial.println("Connected to SMTP server via SSL"); - - // Wait for greeting - String response = readSMTPResponse(secureClient, 10000); - if (response.indexOf("220") < 0) { - Serial.println("ERROR: No greeting from server"); - secureClient.stop(); - return false; - } - - // EHLO - secureClient.println("EHLO ESP32"); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("250") < 0) { - Serial.println("ERROR: EHLO failed"); - secureClient.stop(); - return false; - } - - // Clear any remaining response lines - delay(500); - while (secureClient.available()) secureClient.read(); - - // AUTH LOGIN - secureClient.println("AUTH LOGIN"); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("334") < 0) { - Serial.println("ERROR: AUTH LOGIN failed"); - secureClient.stop(); - return false; - } - - // Username - secureClient.println(base64Encode(SENDER_EMAIL)); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("334") < 0) { - Serial.println("ERROR: Username rejected"); - secureClient.stop(); - return false; - } - - // Password - secureClient.println(base64Encode(SENDER_PASSWORD)); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("235") < 0) { - Serial.println("ERROR: Authentication failed - Check app password"); - secureClient.stop(); - return false; - } - Serial.println("Authentication successful"); - - // MAIL FROM - secureClient.print("MAIL FROM:<"); - secureClient.print(SENDER_EMAIL); - secureClient.println(">"); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("250") < 0) { - Serial.println("ERROR: MAIL FROM rejected"); - secureClient.stop(); - return false; - } - - // RCPT TO - secureClient.print("RCPT TO:<"); - secureClient.print(SUPERVISOR_EMAIL); - secureClient.println(">"); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("250") < 0) { - Serial.println("ERROR: RCPT TO rejected"); - secureClient.stop(); - return false; - } - - // DATA - secureClient.println("DATA"); - response = readSMTPResponse(secureClient, 5000); - if (response.indexOf("354") < 0) { - Serial.println("ERROR: DATA command rejected"); - secureClient.stop(); - return false; - } - - // Email headers and body - secureClient.print("From: "); - secureClient.println(SENDER_EMAIL); - secureClient.print("To: "); - secureClient.println(SUPERVISOR_EMAIL); - secureClient.print("Subject: "); - secureClient.println(subject); - secureClient.println(); - secureClient.println(body); - secureClient.println("."); - - response = readSMTPResponse(secureClient, 10000); - if (response.indexOf("250") < 0) { - Serial.println("ERROR: Message rejected"); - secureClient.stop(); - return false; - } - - // QUIT - secureClient.println("QUIT"); - secureClient.stop(); - - Serial.println("Email sent successfully!"); - return true; -} - -String base64Encode(String input) { - const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - String result = ""; - int val = 0, valb = 0; - - for (byte c : input) { - val = (val << 8) + c; - valb += 8; - while (valb >= 6) { - valb -= 6; - result += base64_chars[(val >> valb) & 0x3F]; - } - } - - if (valb > 0) result += base64_chars[(val << (6 - valb)) & 0x3F]; - while (result.length() % 4) result += "="; - return result; -} - -void uploadToSupabase(String jsonData) { - if (!supabaseReady) return; - - String url = String(SUPABASE_URL) + "/rest/v1/sensor_data"; - - http.begin(client, url); - http.addHeader("Content-Type", "application/json"); - http.addHeader("apikey", SUPABASE_ANON_KEY); - http.addHeader("Authorization", "Bearer " + String(SUPABASE_ANON_KEY)); - - int httpResponseCode = http.POST(jsonData); - - if (httpResponseCode == 201 || httpResponseCode == 200) { - packetsUploaded++; - Serial.println("✓ Uploaded to Supabase\n"); - } else { - Serial.println("✗ Upload failed: " + String(httpResponseCode)); - String response = http.getString(); - Serial.println("Response: " + response + "\n"); - } - - http.end(); -} - -String getCurrentDateTime() { - if (!ntpSynced) { - return String(millis()/1000) + "s"; - } - - struct tm timeinfo; - if (!getLocalTime(&timeinfo)) { - return "Error"; - } - - char timeString[64]; - strftime(timeString, sizeof(timeString), "%Y-%m-%dT%H:%M:%S+05:30", &timeinfo); - return String(timeString); -} - -void displayStatistics() { - Serial.println("\n╔═══════════════════════════════════════╗"); - Serial.println("║ SYSTEM STATISTICS ║"); - Serial.println("╠═══════════════════════════════════════╣"); - Serial.println("║ Component Status: ║"); - Serial.println("║ WiFi: " + String(wifiConnected ? "✓ Connected " : "✗ Disconnected") + " ║"); - Serial.println("║ Supabase: " + String(supabaseReady ? "✓ Ready " : "✗ Not Ready ") + " ║"); - Serial.println("║ NTP: " + String(ntpSynced ? "✓ Synced " : "✗ Not Synced ") + " ║"); - Serial.println("║ LoRa: " + String(loraReady ? "✓ Active " : "✗ Offline ") + " ║"); - Serial.println("╠═══════════════════════════════════════╣"); - Serial.println("║ Packet Statistics: ║"); - Serial.println("║ Received: " + String(packetsReceived) + String(21 - String(packetsReceived).length(), ' ') + "║"); - Serial.println("║ Uploaded: " + String(packetsUploaded) + String(21 - String(packetsUploaded).length(), ' ') + "║"); - Serial.println("║ Corrupted: " + String(packetsCorrupted) + String(21 - String(packetsCorrupted).length(), ' ') + "║"); - Serial.println("╠═══════════════════════════════════════╣"); - Serial.println("║ Emergency Statistics: ║"); - Serial.println("║ 🚨 Emergencies: " + String(emergenciesDetected) + String(19 - String(emergenciesDetected).length(), ' ') + "║"); - Serial.println("║ 📧 Emails Sent: " + String(emailsSent) + String(19 - String(emailsSent).length(), ' ') + "║"); - Serial.println("╚═══════════════════════════════════════╝\n"); - - // Show current time - Serial.println("Current Time: " + getCurrentDateTime()); - Serial.println(); -} diff --git a/try_edge_for_lora.cpp b/try_edge_for_lora.cpp deleted file mode 100644 index f3d242b..0000000 --- a/try_edge_for_lora.cpp +++ /dev/null @@ -1,1663 +0,0 @@ -/* - edge_node_espnow.ino - Edge Node with ESP-NOW integration for wristband vitals & text relay. - Full, self-contained sketch. Make sure to copy the entire file into Arduino IDE, - no extra lines above the first #include. -*/ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// ========================= MPU6050 I2C Register Definitions ========================= -#define MPU6050_ADDR 0x68 -#define PWR_MGMT_1 0x6B -#define ACCEL_XOUT_H 0x3B -#define GYRO_XOUT_H 0x43 -#define CONFIG 0x1A -#define GYRO_CONFIG 0x1B -#define ACCEL_CONFIG 0x1C -#define WHO_AM_I 0x75 - -// ========================= Pin definitions (preserved) ========================= -// MQ sensors -#define MQ2_DIGITAL_PIN 27 -#define MQ2_ANALOG_PIN 32 -#define MQ9_DIGITAL_PIN 14 -#define MQ9_ANALOG_PIN 33 -#define MQ135_DIGITAL_PIN 13 -#define MQ135_ANALOG_PIN 35 - -// FN-M16P Audio Module pins (UART2) -#define FN_M16P_RX 16 -#define FN_M16P_TX 17 - -// DHT11 pin -#define DHT11_PIN 25 -#define DHT_TYPE DHT11 - -// MPU6050 pins (I2C) -#define MPU6050_SDA 21 -#define MPU6050_SCL 22 -#define MPU6050_INT 34 - -// LoRa Module pins (SPI) -#define LORA_SCK 18 -#define LORA_MISO 19 -#define LORA_MOSI 23 -#define LORA_SS 5 -#define LORA_RST 4 -#define LORA_DIO0 26 - -// EMERGENCY BUTTON PIN -#define EMERGENCY_BUTTON_PIN 15 - -// LoRa frequency -#define LORA_BAND 915E6 - -// ========================= System constants ========================= -#define NODE_ID "001" - -#define MQ2_DANGER_THRESHOLD 1600 -#define MQ9_DANGER_THRESHOLD 3800 -#define MQ135_DANGER_THRESHOLD 1800 - -#define FALLBACK_TEMPERATURE 27.0 -#define FALLBACK_HUMIDITY 47.0 - -#define CALIBRATION_SAMPLES 10 -#define DANGER_MULTIPLIER 2.0 -#define CALIBRATION_DELAY 2000 - -bool pendingEmergency = false; - -const int TAP_TIMEOUT = 600; -const int REQUIRED_TAPS = 3; - -const byte FRAME_START = 0x7E; -const byte FRAME_END = 0xEF; -const byte VERSION = 0xFF; -const byte NO_FEEDBACK = 0x00; -const byte WITH_FEEDBACK = 0x01; -const byte CMD_PLAY_TRACK = 0x03; -const byte CMD_VOLUME = 0x06; -const byte CMD_STOP = 0x16; - -// Audio files mapping -enum AudioFiles { - BOOT_AUDIO = 1, // 0001.mp3 - System Boot - SMOKE_ALERT = 2, // 0002.mp3 - Smoke and Gas - CO_ALERT = 3, // 0003.mp3 - Carbon Monoxide - AIR_QUALITY_WARNING = 4, // 0004.mp3 - Air Quality Warning - HIGH_TEMP_ALERT = 5, // 0005.mp3 - High Temperature - LOW_TEMP_ALERT = 6, // 0006.mp3 - Low Temperature - HIGH_HUMIDITY_ALERT = 7, // 0007.mp3 - High Humidity - LOW_HUMIDITY_ALERT = 8, // 0008.mp3 - Low Humidity - MESSAGE_RECEIVED = 9, // 0009.mp3 - NEW: Message received on wristband - EMERGENCY_TRIPLE_TAP = 10, // 0010.mp3 - NEW: Triple tap emergency - FALL_ALERT = 11 // 0011.mp3 - NEW: Fall detection alert -}; - -// ========================= ESP-NOW message types ========================= -#define MSG_TYPE_VITALS 0x01 -#define MSG_TYPE_TEXT 0x02 -#define MSG_TYPE_ACK 0x03 - -#define MAX_TEXT_LEN 128 - -typedef struct __attribute__((packed)) { - uint8_t msgType; // MSG_TYPE_VITALS - uint8_t bpm; // 0..255 - uint8_t spo2; // 0..100 - uint8_t finger; // 0/1 - uint32_t timestamp; -} espnow_vitals_t; - -typedef struct __attribute__((packed)) { - uint8_t msgType; // MSG_TYPE_TEXT - uint32_t messageId; - uint8_t length; - char text[MAX_TEXT_LEN]; -} espnow_text_t; - -typedef struct __attribute__((packed)) { - uint8_t msgType; // MSG_TYPE_ACK - uint32_t messageId; - uint8_t success; // 0/1 -} espnow_ack_t; - -// ========================= Types & Globals ========================= -struct SensorCalibration { - float baseline; - float dangerThreshold; - bool calibrated; -}; - -struct MotionData { - float totalAccel; - float totalGyro; - bool fallDetected; - bool impactDetected; - bool motionDetected; - unsigned long lastMotionTime; -}; - -struct SensorData { - float temperature; - float humidity; - int mq2_analog; - int mq9_analog; - int mq135_analog; - bool mq2_digital; - bool mq9_digital; - bool mq135_digital; - bool emergency; - unsigned long timestamp; - MotionData motion; -}; - -SensorCalibration mq2_cal = {0,0,false}; -SensorCalibration mq9_cal = {0,0,false}; -SensorCalibration mq135_cal = {0,0,false}; - -DHT dht(DHT11_PIN, DHT_TYPE); -HardwareSerial fnM16pSerial(2); - -bool audioReady = false; -bool loraReady = false; -bool dhtReady = false; -bool mpuReady = false; - -unsigned long lastLoRaSend = 0; -int loraInterval = 30000; -int packetCount = 0; - -unsigned long lastDHTReading = 0; -const unsigned long DHT_READING_INTERVAL = 2000; -float lastValidTemperature = FALLBACK_TEMPERATURE; -float lastValidHumidity = FALLBACK_HUMIDITY; - -// Emergency Button Variables -volatile int tapCount = 0; -volatile unsigned long lastTapTime = 0; -volatile bool emergencyTriggered = false; - -// Motion variables -MotionData motionData = {0}; - -// MPU internals -const float G = 9.80665f; -const float FREE_FALL_G_THRESHOLD = 0.6f; -const unsigned long FREE_FALL_MIN_MS = 120; -const float IMPACT_G_THRESHOLD = 3.5f; -const unsigned long IMPACT_WINDOW_MS = 1200; -const unsigned long STATIONARY_CONFIRM_MS = 800; -const float ROTATION_IMPACT_THRESHOLD = 400.0f; - -bool inFreeFall = false; -bool fallInProgress = false; -bool impactSeen = false; -unsigned long freeFallStart = 0; -unsigned long fallStartTime = 0; -unsigned long impactTime = 0; -unsigned long stationarySince = 0; -float accelFiltered = G; -const float ALPHA = 0.85f; - -unsigned long lastI2CAttempt = 0; - -// ========================= ESP-NOW / Wristband status globals ========================= -struct WristbandStatus { - uint8_t bpm; - uint8_t spo2; - bool fingerDetected; - unsigned long lastUpdate; - bool connected; - uint32_t lastMessageId; - bool messageAcknowledged; -} wristbandStatus = {0,0,false,0,false,0,false}; - -// Wristband MAC address (update if different) -uint8_t wristbandMac[6] = {0x0C, 0x4E, 0xA0, 0x66, 0xB2, 0x78}; - -bool espnowReady = false; -esp_err_t lastEspNowSendStatus = ESP_OK; -uint32_t outgoingMessageCounter = 1; - -unsigned long messagesRelayedToWristband = 0; - -// ========================= Forward declarations ========================= -void printTestMenu(); -void handleTestCommand(String cmd); -void testDHT(); -void testMQ2(); -void testMQ9(); -void testMQ135(); -void testAllMQ(); -void testAudio(int fileNum); -void testLoRa(); -void testEmergency(); -void testButton(); -void testAllSensors(); -void printSystemStatus(); -void scanI2CDevices(); -void setVolume(int volume); -void playAudioFile(int fileNumber); -void stopAudio(); -void calibrateSensors(); -void calibrateSensor(int pin, SensorCalibration* cal, String sensorName, int minThreshold); -bool checkSensorDanger(int currentValue, SensorCalibration* cal, int staticDangerThreshold); -SensorData readAllSensors(); -void displayReadings(SensorData data); -void checkAlerts(SensorData data); -void sendLoRaData(SensorData data); -String getAirQualityRating(int value); -void checkEmergencyButton(); -void handleEmergency(); -void sendCommand(byte cmd, byte param1, byte param2, bool feedback); - -// I2C helpers & MPU -bool i2cBusRecover(); -bool safeWireRequest(uint8_t addr, uint8_t reg, uint8_t *buf, size_t len, int retries=3); -bool safeWireWrite(uint8_t addr, uint8_t reg, uint8_t val, int retries=3); -bool initMPU6050(); -void readMPU6050Data(int16_t *ax, int16_t *ay, int16_t *az, int16_t *gx, int16_t *gy, int16_t *gz); -void monitorMotion(); -void detectFallAndHandle(); - -// ESP-NOW -void initESPNOW(); -void onDataRecv(const esp_now_recv_info_t *recv_info, const uint8_t *data, int len); -void onDataSent(const wifi_tx_info_t *tx_info, esp_now_send_status_t status); -bool sendTextToWristband(const String &message); -void checkWristbandConnection(); -void receiveLoRaMessages(); - -// -------------------------- Implementations -------------------------- -// I2C recovery & safeWire -bool i2cBusRecover() { - Wire.end(); - delay(10); - pinMode(MPU6050_SCL, OUTPUT); - pinMode(MPU6050_SDA, INPUT_PULLUP); - if (digitalRead(MPU6050_SDA) == HIGH) { - Wire.begin(MPU6050_SDA, MPU6050_SCL); - Wire.setClock(100000); - return true; - } - for (int i=0;i<9;i++) { - digitalWrite(MPU6050_SCL, HIGH); - delayMicroseconds(500); - digitalWrite(MPU6050_SCL, LOW); - delayMicroseconds(500); - if (digitalRead(MPU6050_SDA) == HIGH) break; - } - pinMode(MPU6050_SDA, OUTPUT); - digitalWrite(MPU6050_SDA, LOW); - delayMicroseconds(200); - digitalWrite(MPU6050_SCL, HIGH); - delayMicroseconds(200); - digitalWrite(MPU6050_SDA, HIGH); - delayMicroseconds(200); - Wire.begin(MPU6050_SDA, MPU6050_SCL); - Wire.setClock(100000); - pinMode(MPU6050_SDA, INPUT_PULLUP); - pinMode(MPU6050_SCL, INPUT_PULLUP); - return digitalRead(MPU6050_SDA) == HIGH; -} - -bool safeWireRequest(uint8_t addr, uint8_t reg, uint8_t *buf, size_t len, int retries) { - for (int attempt=0; attempt 0.2f * G || motionData.totalGyro > 25.0f) { - motionData.lastMotionTime = millis(); - motionData.motionDetected = true; - } else motionData.motionDetected = false; - detectFallAndHandle(); -} - -void detectFallAndHandle() { - unsigned long now = millis(); - float totG = motionData.totalAccel / G; - float totGyro = motionData.totalGyro; - if (totG < FREE_FALL_G_THRESHOLD) { - if (!inFreeFall) { inFreeFall = true; freeFallStart = now; } - else if ((now - freeFallStart) >= FREE_FALL_MIN_MS && !fallInProgress) { - fallInProgress = true; - fallStartTime = now; - impactSeen = false; - motionData.impactDetected = false; - } - } else { if (inFreeFall) inFreeFall = false; } - if (fallInProgress && !impactSeen) { - if (totG >= IMPACT_G_THRESHOLD || totGyro >= ROTATION_IMPACT_THRESHOLD) { - impactSeen = true; impactTime = now; motionData.impactDetected = true; - } else if (now - fallStartTime > IMPACT_WINDOW_MS) { fallInProgress = false; impactSeen = false; motionData.impactDetected = false; } - } - if (impactSeen) { - float accelVariationG = fabs((motionData.totalAccel / G) - 1.0f); - if (accelVariationG < 0.35f && motionData.totalGyro < 50.0f) { - if (stationarySince == 0) stationarySince = now; - if (now - stationarySince >= STATIONARY_CONFIRM_MS) { - motionData.fallDetected = true; - emergencyTriggered = true; - if (audioReady) playAudioFile(EMERGENCY_TRIPLE_TAP); - Serial.println("\n╔════════════════════════════════════╗"); - Serial.println("║ FALL CONFIRMED - IMPACT + STATIONARY ║"); - Serial.println("╚════════════════════════════════════╝"); - Serial.printf("Acceleration: %.2f g\n", motionData.totalAccel / G); - Serial.printf("Gyroscope: %.2f °/s\n", motionData.totalGyro); - fallInProgress = false; impactSeen = false; stationarySince = 0; - } - } else { - stationarySince = 0; - if (now - impactTime > IMPACT_WINDOW_MS) { fallInProgress = false; impactSeen = false; stationarySince = 0; motionData.impactDetected = false; } - } - } -} - -// Air Quality Rating function -String getAirQualityRating(int value) { - if (value < 800) return "Excellent"; - else if (value < 1200) return "Good"; - else if (value < 1800) return "Moderate"; - else if (value < 2400) return "Poor"; - else return "Very Poor"; -} - -// Emergency Button handler -void checkEmergencyButton() { - static bool lastState = HIGH; // using INPUT_PULLUP -> HIGH when released - static unsigned long lastChangeTime = 0; - bool currentState = digitalRead(EMERGENCY_BUTTON_PIN); - if (currentState != lastState && (millis() - lastChangeTime > 50)) { - lastChangeTime = millis(); - if (currentState == LOW) { // pressed - unsigned long now = millis(); - if (now - lastTapTime < TAP_TIMEOUT) tapCount++; - else tapCount = 1; - lastTapTime = now; - Serial.printf("BUTTON TAP #%d/%d\n", tapCount, REQUIRED_TAPS); - if (tapCount >= REQUIRED_TAPS) { - Serial.println("TRIPLE TAP - EMERGENCY TRIGGERED"); - emergencyTriggered = true; - tapCount = 0; - } - } - } - if (millis() - lastTapTime > TAP_TIMEOUT && tapCount > 0) tapCount = 0; - lastState = currentState; -} - -// Emergency Handler function -void handleEmergency() { - Serial.println("\n████████ EMERGENCY MODE █████████"); - - // Play emergency audio FIRST before doing anything else - if (audioReady) { - playAudioFile(EMERGENCY_TRIPLE_TAP); // Play 0010.mp3 for triple tap emergency - Serial.println("✓ Playing emergency triple-tap audio"); - delay(100); // Small delay to ensure audio command is sent - } - - // Set the pending emergency flag so the main loop will send it - pendingEmergency = true; - - SensorData s = readAllSensors(); - s.emergency = true; - s.motion = motionData; - - Serial.println("EMERGENCY SNAPSHOT:"); - Serial.printf(" Temp: %.2f C Hum: %.2f %%\n", s.temperature, s.humidity); - Serial.printf(" MQ2:%d MQ9:%d MQ135:%d\n", s.mq2_analog, s.mq9_analog, s.mq135_analog); - Serial.printf(" Fall: %s\n", s.motion.fallDetected ? "YES":"NO"); - - // Send emergency packet immediately - if (loraReady) { - sendLoRaData(s); - Serial.println("✓ Emergency packet sent via LoRa"); - } else { - Serial.println("⚠ LoRa not ready - emergency packet queued"); - } - - delay(500); // Give some time for audio to play -} - -// -------------------------- Setup & Loop -------------------------- -/* - COMPLETE SETUP AND LOOP FOR EDGE NODE - Optimized boot sequence to prevent conflicts between: - - LoRa (SPI) - - ESP-NOW (WiFi) - - MPU6050 (I2C) - - All other peripherals -*/ - -// ==================== SETUP FUNCTION ==================== -void setup() { - Serial.begin(115200); - delay(1000); - - Serial.println("\n\n"); - Serial.println("╔════════════════════════════════════════════════════╗"); - Serial.println("║ ESP32 Multi-Sensor Edge Node v3.0 ║"); - Serial.println("║ Optimized Boot Sequence ║"); - Serial.println("╚════════════════════════════════════════════════════╝\n"); - - // ======================================== - // PHASE 1: GPIO INITIALIZATION (No conflicts) - // ======================================== - Serial.println("PHASE 1: Initializing GPIO pins..."); - - pinMode(MQ2_DIGITAL_PIN, INPUT); - pinMode(MQ9_DIGITAL_PIN, INPUT); - pinMode(MQ135_DIGITAL_PIN, INPUT); - pinMode(EMERGENCY_BUTTON_PIN, INPUT_PULLUP); - pinMode(MPU6050_INT, INPUT); - - Serial.println("✓ GPIO pins configured"); - delay(100); - - // ======================================== - // PHASE 2: I2C INITIALIZATION (MPU6050) - // Do this BEFORE SPI to avoid bus conflicts - // ======================================== - Serial.println("\nPHASE 2: Initializing I2C (MPU6050)..."); - - Wire.begin(MPU6050_SDA, MPU6050_SCL); - Wire.setClock(100000); // 100kHz for compatibility - pinMode(MPU6050_SDA, INPUT_PULLUP); - pinMode(MPU6050_SCL, INPUT_PULLUP); - delay(100); - - // Multiple attempts for MPU6050 (clones can be finicky) - mpuReady = false; - for (int attempt = 1; attempt <= 3; attempt++) { - Serial.printf(" MPU6050 initialization attempt %d/3...\n", attempt); - if (initMPU6050()) { - mpuReady = true; - Serial.println("✓ MPU6050 initialized successfully"); - motionData.lastMotionTime = millis(); - accelFiltered = G; - break; - } - delay(500); - } - - if (!mpuReady) { - Serial.println("⚠ MPU6050 initialization failed - motion features disabled"); - Serial.println(" System will continue without motion detection"); - } - delay(200); - - // ======================================== - // PHASE 3: SPI INITIALIZATION (LoRa) - // Do this BEFORE WiFi to claim SPI bus - // ======================================== - Serial.println("\nPHASE 3: Initializing SPI (LoRa)..."); - - // Prepare LoRa pins - pinMode(LORA_SS, OUTPUT); - digitalWrite(LORA_SS, HIGH); - pinMode(LORA_RST, OUTPUT); - digitalWrite(LORA_RST, HIGH); - - // Initialize SPI bus - SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_SS); - delay(50); - - // Set LoRa pins - LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0); - - // Hardware reset sequence - digitalWrite(LORA_RST, LOW); - delay(10); - digitalWrite(LORA_RST, HIGH); - delay(10); - - // Initialize LoRa - loraReady = false; - if (!LoRa.begin(LORA_BAND)) { - Serial.println("✗ LoRa initialization FAILED"); - Serial.println(" Check: Wiring, antenna, power supply"); - } else { - // CRITICAL: Configure LoRa parameters to match central node - LoRa.setTxPower(20); // Maximum power - LoRa.setSpreadingFactor(12); // Maximum range - LoRa.setSignalBandwidth(125E3); // 125 kHz - LoRa.setCodingRate4(8); // Error correction - LoRa.setPreambleLength(8); // Standard preamble - LoRa.setSyncWord(0x34); // Must match central node - - loraReady = true; - Serial.println("✓ LoRa initialized successfully"); - Serial.println(" Configuration:"); - Serial.println(" - Frequency: 915 MHz"); - Serial.println(" - TX Power: 20 dBm"); - Serial.println(" - Spreading Factor: 12"); - Serial.println(" - Bandwidth: 125 kHz"); - Serial.println(" - Coding Rate: 4/8"); - Serial.println(" - Sync Word: 0x34"); - } - delay(200); - - // ======================================== - // PHASE 4: WiFi INITIALIZATION (for ESP-NOW) - // Do this AFTER LoRa to avoid interference - // ======================================== - Serial.println("\nPHASE 4: Initializing WiFi (ESP-NOW)..."); - - // Turn off WiFi completely first - WiFi.disconnect(true); - WiFi.mode(WIFI_OFF); - delay(100); - - // Now configure for ESP-NOW (Station mode only) - WiFi.mode(WIFI_STA); - WiFi.disconnect(false); // Don't erase config - delay(100); - - // Set WiFi to low power to minimize interference with LoRa - esp_wifi_set_ps(WIFI_PS_MIN_MODEM); - - // Set WiFi channel (same as wristband) - int wifiChannel = 1; - esp_err_t channelResult = esp_wifi_set_channel(wifiChannel, WIFI_SECOND_CHAN_NONE); - if (channelResult == ESP_OK) { - Serial.printf("✓ WiFi channel set to %d\n", wifiChannel); - } else { - Serial.printf("⚠ Failed to set WiFi channel (error: %d)\n", channelResult); - } - - delay(200); - - // ======================================== - // PHASE 5: ESP-NOW INITIALIZATION - // ======================================== - Serial.println("\nPHASE 5: Initializing ESP-NOW..."); - - espnowReady = false; - - // Initialize ESP-NOW - esp_err_t espnowResult = esp_now_init(); - if (espnowResult != ESP_OK) { - Serial.printf("✗ ESP-NOW init failed (error: %d)\n", espnowResult); - Serial.println(" Retrying once..."); - delay(500); - espnowResult = esp_now_init(); - } - - if (espnowResult == ESP_OK) { - espnowReady = true; - Serial.println("✓ ESP-NOW initialized"); - - // Register callbacks - esp_now_register_send_cb(onDataSent); - esp_now_register_recv_cb(onDataRecv); - delay(100); - - // Add wristband as peer - esp_now_peer_info_t peerInfo; - memset(&peerInfo, 0, sizeof(peerInfo)); - memcpy(peerInfo.peer_addr, wristbandMac, 6); - peerInfo.channel = wifiChannel; - peerInfo.encrypt = false; - - #if defined(ESP_IF_WIFI_STA) - peerInfo.ifidx = ESP_IF_WIFI_STA; - #elif defined(WIFI_IF_STA) - peerInfo.ifidx = WIFI_IF_STA; - #else - peerInfo.ifidx = (wifi_interface_t)0; - #endif - - if (!esp_now_is_peer_exist(wristbandMac)) { - esp_err_t addPeerResult = esp_now_add_peer(&peerInfo); - if (addPeerResult == ESP_OK) { - Serial.println("✓ Wristband peer added"); - Serial.printf(" MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", - wristbandMac[0], wristbandMac[1], wristbandMac[2], - wristbandMac[3], wristbandMac[4], wristbandMac[5]); - } else { - Serial.printf("⚠ Failed to add wristband peer (error: %d)\n", addPeerResult); - } - } else { - Serial.println("✓ Wristband peer already exists"); - } - } else { - Serial.println("✗ ESP-NOW initialization failed"); - Serial.println(" System will continue without wristband communication"); - } - - delay(200); - - // ======================================== - // PHASE 6: UART INITIALIZATION (Audio Module) - // ======================================== - Serial.println("\nPHASE 6: Initializing UART (Audio)..."); - - fnM16pSerial.begin(9600, SERIAL_8N1, FN_M16P_RX, FN_M16P_TX); - delay(200); - - // Set initial volume - setVolume(25); - delay(100); - - audioReady = true; - Serial.println("✓ Audio module initialized (volume: 25)"); - delay(200); - - // ======================================== - // PHASE 7: DHT11 INITIALIZATION - // ======================================== - Serial.println("\nPHASE 7: Initializing DHT11..."); - - dht.begin(); - delay(1500); // DHT11 needs time to stabilize - - // Test DHT11 with multiple attempts - dhtReady = false; - for (int attempt = 1; attempt <= 3; attempt++) { - Serial.printf(" DHT11 reading attempt %d/3...\n", attempt); - float t = dht.readTemperature(); - float h = dht.readHumidity(); - - if (!isnan(t) && !isnan(h)) { - dhtReady = true; - lastValidTemperature = t; - - // Apply humidity correction - float corrected = h - 30.0f; - if (corrected < 0.0f) corrected = 0.0f; - if (corrected > 100.0f) corrected = 100.0f; - lastValidHumidity = corrected; - - Serial.printf("✓ DHT11 initialized: %.1f°C, %.1f%% (corrected)\n", - lastValidTemperature, lastValidHumidity); - break; - } - delay(1000); - } - - if (!dhtReady) { - Serial.println("⚠ DHT11 initialization failed"); - Serial.println(" Using fallback values: 27.0°C, 47.0%"); - Serial.println(" Check: DATA pin → GPIO25, VCC → 3.3V, GND"); - } - - delay(200); - - // ======================================== - // PHASE 8: GAS SENSOR WARMUP & CALIBRATION - // ======================================== - Serial.println("\nPHASE 8: Gas sensor warmup..."); - Serial.println(" Please wait 15 seconds for sensors to stabilize"); - - // Progress indicator - for (int i = 0; i < 15; i++) { - Serial.print("."); - delay(1000); - } - Serial.println(" Done!"); - - Serial.println("\nCalibrating MQ sensors..."); - calibrateSensors(); - - delay(200); - - // ======================================== - // PHASE 9: SYSTEM READY - // ======================================== - Serial.println("\n╔════════════════════════════════════════════════════╗"); - Serial.println("║ SYSTEM INITIALIZATION COMPLETE ║"); - Serial.println("╚════════════════════════════════════════════════════╝\n"); - - // Print status summary - printBootSummary(); - - // Play boot sound - if (audioReady) { - Serial.println("Playing boot audio..."); - playAudioFile(BOOT_AUDIO); - delay(1500); - } - - // Print menu - printTestMenu(); - - Serial.println("\n🚀 Edge node is now operational!"); - Serial.println("Listening for commands and monitoring sensors...\n"); -} - -// ==================== LOOP FUNCTION ==================== -void loop() { - static unsigned long lastNormalReading = 0; - static unsigned long lastStatusPrint = 0; - - // ======================================== - // PRIORITY 1: Check for serial commands - // ======================================== - if (Serial.available() > 0) { - String cmd = Serial.readStringUntil('\n'); - cmd.trim(); - cmd.toLowerCase(); - if (cmd.length() > 0) { - handleTestCommand(cmd); - } - } - - // ======================================== - // PRIORITY 2: Emergency button monitoring (high frequency) - // ======================================== - checkEmergencyButton(); - - // ======================================== - // PRIORITY 3: Motion detection (if available) - // ======================================== - if (mpuReady) { - monitorMotion(); - } - - // ======================================== - // PRIORITY 4: Handle emergency trigger - // ======================================== - if (emergencyTriggered || pendingEmergency) { - handleEmergency(); - emergencyTriggered = false; - motionData.fallDetected = false; - pendingEmergency = false; // Clear the flag after handling -} - - // ======================================== - // PRIORITY 5: Check for incoming LoRa messages - // ======================================== - if (loraReady) { - receiveLoRaMessages(); - } - - // ======================================== - // PRIORITY 6: Wristband connection monitoring - // ======================================== - if (espnowReady) { - checkWristbandConnection(); - } - - // ======================================== - // PRIORITY 7: Normal sensor reading & transmission (10 second interval) - // ======================================== - if (millis() - lastNormalReading >= 10000) { - lastNormalReading = millis(); - - // Read all sensors - SensorData data = readAllSensors(); - data.emergency = false; // Normal reading, not emergency - data.motion = motionData; - - // Display readings - displayReadings(data); - - // Check for alerts - checkAlerts(data); - - // Send via LoRa if ready and interval passed - if (loraReady && (millis() - lastLoRaSend >= loraInterval)) { - sendLoRaData(data); - lastLoRaSend = millis(); - } - - Serial.println("────────────────────────────────────"); - } - - // ======================================== - // PRIORITY 8: Periodic status update (60 second interval) - // ======================================== - if (millis() - lastStatusPrint >= 60000) { - lastStatusPrint = millis(); - printSystemStatus(); - } - - // ======================================== - // Small delay to prevent watchdog issues - // ======================================== - delay(50); -} - -// ==================== HELPER FUNCTIONS ==================== - -void printBootSummary() { - Serial.println("Component Status:"); - Serial.println("┌─────────────────────┬─────────┐"); - Serial.printf("│ %-19s │ %7s │\n", "LoRa Module", loraReady ? "✓ OK" : "✗ FAIL"); - Serial.printf("│ %-19s │ %7s │\n", "ESP-NOW", espnowReady ? "✓ OK" : "✗ FAIL"); - Serial.printf("│ %-19s │ %7s │\n", "MPU6050 Motion", mpuReady ? "✓ OK" : "✗ FAIL"); - Serial.printf("│ %-19s │ %7s │\n", "DHT11 Temp/Hum", dhtReady ? "✓ OK" : "⚠ WARN"); - Serial.printf("│ %-19s │ %7s │\n", "Audio Module", audioReady ? "✓ OK" : "✗ FAIL"); - Serial.printf("│ %-19s │ %7s │\n", "MQ Gas Sensors", "✓ OK"); - Serial.printf("│ %-19s │ %7s │\n", "Emergency Button", "✓ OK"); - Serial.println("└─────────────────────┴─────────┘"); - - // Critical warnings - if (!loraReady) { - Serial.println("\n⚠ WARNING: LoRa not functional - cannot send data!"); - } - if (!espnowReady) { - Serial.println("\n⚠ WARNING: ESP-NOW not functional - no wristband communication!"); - } - if (!mpuReady) { - Serial.println("\n⚠ WARNING: Motion detection disabled - fall detection unavailable!"); - } - - Serial.println(); -} - -// ---------------- Test commands & helpers ---------------- -void printTestMenu() { - Serial.println("\n╔═══════════════════════════════════════╗"); - Serial.println("║ TEST COMMANDS MENU ║"); - Serial.println("╠═══════════════════════════════════════╣"); - Serial.println("║ dht, mq2, mq9, mq135, mq, all ║"); - Serial.println("║ audio1..audio10, stop, volume+, volume-║"); - Serial.println("║ lora, button, emergency, calibrate ║"); - Serial.println("║ status, scan/i2c, help/menu ║"); - Serial.println("║ sendmsg (ESP-NOW -> wristband)║"); - // ========== ADD THIS LINE ========== - Serial.println("║ relaystats (Message relay stats) ║"); - // ========== END OF NEW LINE ========== - Serial.println("╚═══════════════════════════════════════╝\n"); -} - -void handleTestCommand(String cmd) { - Serial.println("\n>>> EXECUTING TEST: " + cmd + " <<<\n"); - if (cmd == "help" || cmd == "menu") printTestMenu(); - else if (cmd == "dht") testDHT(); - else if (cmd == "mq2") testMQ2(); - else if (cmd == "mq9") testMQ9(); - else if (cmd == "mq135") testMQ135(); - else if (cmd == "mq") testAllMQ(); - else if (cmd.startsWith("audio")) { - int n = cmd.substring(5).toInt(); - if (n >= 1 && n <= 10) testAudio(n); - } else if (cmd == "stop") { stopAudio(); Serial.println("Audio stopped."); } - else if (cmd == "volume+") { setVolume(25); Serial.println("Volume 25"); } - else if (cmd == "volume-") { setVolume(15); Serial.println("Volume 15"); } - else if (cmd == "lora") testLoRa(); - else if (cmd == "button") testButton(); - else if (cmd == "emergency") testEmergency(); - else if (cmd == "all") testAllSensors(); - else if (cmd == "calibrate") calibrateSensors(); - else if (cmd == "status") printSystemStatus(); - else if (cmd == "scan" || cmd == "i2c") scanI2CDevices(); - else if (cmd.startsWith("sendmsg ")) { - String txt = cmd.substring(8); - if (txt.length() == 0) Serial.println("No message provided."); - else { - bool ok = sendTextToWristband(txt); - Serial.printf("sendTextToWristband('%s') => %s\n", txt.c_str(), ok ? "SENT":"FAILED"); - } - } - // ========== ADD THIS BLOCK HERE ========== - else if (cmd == "relaystats") { - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.println("║ MESSAGE RELAY STATISTICS ║"); - Serial.println("╚════════════════════════════════════════════╝"); - Serial.printf("Messages relayed to wristband: %lu\n", messagesRelayedToWristband); - Serial.printf("Last message ID sent: %lu\n", (unsigned long)(outgoingMessageCounter - 1)); - Serial.printf("ESP-NOW status: %s\n", espnowReady ? "✓ Ready" : "✗ Not Ready"); - Serial.printf("Wristband connected: %s\n", wristbandStatus.connected ? "✓ Yes" : "✗ No"); - if (wristbandStatus.lastMessageId > 0) { - Serial.printf("Last message acknowledged: %s\n", - wristbandStatus.messageAcknowledged ? "✓ Yes" : "✗ No"); - } - Serial.println("════════════════════════════════════════════\n"); - } - // ========== END OF NEW BLOCK ========== - else Serial.println("❌ Unknown command: " + cmd); - Serial.println("\n>>> TEST COMPLETE <<<\n"); -} - -void testDHT() { - Serial.println("Testing DHT11 (5 samples) with manual -30% humidity correction:"); - for (int i=0;i<5;i++) { - float t = dht.readTemperature(); - float h = dht.readHumidity(); - if (!isnan(t) && !isnan(h)) { - float corr = h - 30.0f; - if (corr < 0) corr = 0; if (corr > 100) corr = 100; - Serial.printf(" #%d: Temp=%.2f C, Hum(corrected)=%.2f %%\n", i+1, t, corr); - } else { - Serial.printf(" #%d: FAILED (NaN)\n", i+1); - } - delay(2000); - } -} - -void testMQ2() { - Serial.println("Testing MQ2 (10 samples):"); - for (int i=0;i<10;i++) { - int a = analogRead(MQ2_ANALOG_PIN); - bool d = digitalRead(MQ2_DIGITAL_PIN) == LOW; - Serial.printf(" #%d: analog=%d digital=%s\n", i+1, a, d?"ACTIVE":"INACTIVE"); - delay(500); - } - Serial.printf("Calibration baseline=%.1f threshold=%.1f\n", mq2_cal.baseline, mq2_cal.dangerThreshold); -} - -void testMQ9() { - Serial.println("Testing MQ9 (10 samples):"); - for (int i=0;i<10;i++) { - int a = analogRead(MQ9_ANALOG_PIN); - bool d = digitalRead(MQ9_DIGITAL_PIN) == LOW; - Serial.printf(" #%d: analog=%d digital=%s\n", i+1, a, d?"ACTIVE":"INACTIVE"); - delay(500); - } - Serial.printf("Calibration baseline=%.1f threshold=%.1f\n", mq9_cal.baseline, mq9_cal.dangerThreshold); -} - -void testMQ135() { - Serial.println("Testing MQ135 (10 samples):"); - for (int i=0;i<10;i++) { - int a = analogRead(MQ135_ANALOG_PIN); - bool d = digitalRead(MQ135_DIGITAL_PIN) == LOW; - Serial.printf(" #%d: analog=%d digital=%s rating=%s\n", i+1, a, d?"POOR":"GOOD", getAirQualityRating(a).c_str()); - delay(500); - } - Serial.printf("Calibration baseline=%.1f threshold=%.1f\n", mq135_cal.baseline, mq135_cal.dangerThreshold); -} - -void testAllMQ() { testMQ2(); testMQ9(); testMQ135(); } - -void testAudio(int fileNum) { - if (!audioReady) { Serial.println("Audio not ready"); return; } - Serial.printf("Playing audio file #%d\n", fileNum); - playAudioFile(fileNum); -} - -void testLoRa() { - if (!loraReady) { Serial.println("LoRa not ready"); return; } - Serial.println("Sending test LoRa packet..."); - SensorData d = readAllSensors(); - d.emergency = false; - d.motion = motionData; - sendLoRaData(d); - Serial.println("Test LoRa sent."); -} - -void testEmergency() { - Serial.println("Triggering emergency now..."); - emergencyTriggered = true; -} - -void testButton() { - Serial.println("Testing emergency button for 10s..."); - unsigned long start = millis(); - bool last = digitalRead(EMERGENCY_BUTTON_PIN); - while (millis() - start < 10000) { - bool cur = digitalRead(EMERGENCY_BUTTON_PIN); - if (cur != last) { - Serial.printf("Button state change: %s\n", cur ? "HIGH" : "LOW"); - last = cur; - } - delay(100); - } - Serial.println("Button test complete."); -} - -void testAllSensors() { - testDHT(); - testAllMQ(); - testLoRa(); -} - -// ---------------- System status and misc helpers ---------------- -void scanI2CDevices() { - Serial.println("\n=== I2C Device Scanner ==="); - Serial.println("Scanning I2C bus (0x00 to 0x7F)..."); - int devicesFound = 0; - for (byte address = 1; address < 127; address++) { - Wire.beginTransmission(address); - byte error = Wire.endTransmission(); - if (error == 0) { - Serial.printf("✓ Device found at 0x%02X\n", address); - devicesFound++; - if (address == 0x68 || address == 0x69) Serial.println(" → This is likely the MPU6050!"); - } - } - if (devicesFound == 0) { - Serial.println("\n❌ NO I2C devices found!"); - Serial.println("\nTroubleshooting:"); - Serial.println("1. Check VCC is connected to 3.3V (NOT 5V)"); - Serial.println("2. Check GND is connected"); - Serial.println("3. Verify SDA → GPIO21, SCL → GPIO22"); - Serial.println("4. Check all connections are firm"); - Serial.println("5. Add 4.7kΩ pull-up resistors to SDA & SCL"); - Serial.println("6. Try a different MPU6050 module (may be faulty)"); - } else { - Serial.printf("\n✓ Total devices found: %d\n", devicesFound); - } - Serial.println("=========================\n"); -} - -void sendCommand(byte cmd, byte param1, byte param2, bool feedback) { - byte packet[10]; - packet[0] = FRAME_START; - packet[1] = VERSION; - packet[2] = 0x06; - packet[3] = cmd; - packet[4] = feedback ? WITH_FEEDBACK : NO_FEEDBACK; - packet[5] = param1; - packet[6] = param2; - uint16_t sum = packet[1] + packet[2] + packet[3] + packet[4] + packet[5] + packet[6]; - uint16_t checksum = 0xFFFF - sum + 1; - packet[7] = (checksum >> 8) & 0xFF; - packet[8] = checksum & 0xFF; - packet[9] = FRAME_END; - fnM16pSerial.write(packet, 10); -} - -void setVolume(int volume) { - if (volume < 0) volume = 0; - if (volume > 30) volume = 30; - sendCommand(CMD_VOLUME, 0x00, volume, false); -} - -void playAudioFile(int fileNumber) { - if (fileNumber < 1 || fileNumber > 10) return; - sendCommand(CMD_PLAY_TRACK, 0x00, fileNumber, false); - delay(60); -} - -void stopAudio() { - sendCommand(CMD_STOP, 0x00, 0x00, false); -} - -void calibrateSensors() { - Serial.println("Starting sensor calibration..."); - delay(500); - calibrateSensor(MQ2_ANALOG_PIN, &mq2_cal, "MQ2", MQ2_DANGER_THRESHOLD); - calibrateSensor(MQ9_ANALOG_PIN, &mq9_cal, "MQ9", MQ9_DANGER_THRESHOLD); - calibrateSensor(MQ135_ANALOG_PIN, &mq135_cal, "MQ135", MQ135_DANGER_THRESHOLD); - Serial.println("Calibration completed."); -} - -void calibrateSensor(int pin, SensorCalibration* cal, String sensorName, int minThreshold) { - Serial.print("Calibrating " + sensorName + " ..."); - float sum = 0; - for (int i=0;ibaseline = sum / CALIBRATION_SAMPLES; - float calculatedThreshold = cal->baseline * DANGER_MULTIPLIER; - cal->dangerThreshold = (calculatedThreshold > minThreshold) ? calculatedThreshold : minThreshold; - cal->calibrated = true; - Serial.println(" Done"); -} - -bool checkSensorDanger(int currentValue, SensorCalibration* cal, int staticDangerThreshold) { - if (!cal->calibrated) return currentValue >= staticDangerThreshold; - return currentValue >= cal->dangerThreshold; -} - -SensorData readAllSensors() { - SensorData data; - data.timestamp = millis(); - if (millis() - lastDHTReading > DHT_READING_INTERVAL) { - float rt = dht.readTemperature(); - float rh = dht.readHumidity(); - if (!isnan(rt) && rt >= -40 && rt <= 80) { data.temperature = rt; lastValidTemperature = rt; } - else data.temperature = lastValidTemperature; - if (!isnan(rh) && rh >= 0 && rh <= 100) { - float corr = rh - 30.0f; - if (corr < 0.0f) corr = 0.0f; - if (corr > 100.0f) corr = 100.0f; - data.humidity = corr; - lastValidHumidity = corr; - } else data.humidity = lastValidHumidity; - lastDHTReading = millis(); - } else { - data.temperature = lastValidTemperature; - data.humidity = lastValidHumidity; - } - data.mq2_analog = analogRead(MQ2_ANALOG_PIN); - data.mq9_analog = analogRead(MQ9_ANALOG_PIN); - data.mq135_analog = analogRead(MQ135_ANALOG_PIN); - data.mq2_digital = digitalRead(MQ2_DIGITAL_PIN) == LOW; - data.mq9_digital = digitalRead(MQ9_DIGITAL_PIN) == LOW; - data.mq135_digital = digitalRead(MQ135_DIGITAL_PIN) == LOW; - data.emergency = digitalRead(EMERGENCY_BUTTON_PIN) == LOW; - data.motion = motionData; - return data; -} - -void displayReadings(SensorData data) { - Serial.println("\n--- SENSOR SNAPSHOT ---"); - Serial.printf("Timestamp: %lu\n", data.timestamp); - Serial.printf("Temp: %.1f C Hum: %.1f %%\n", data.temperature, data.humidity); - Serial.printf("MQ2: %d (%s) MQ9: %d (%s) MQ135: %d (%s)\n", - data.mq2_analog, data.mq2_digital ? "ALERT":"OK", - data.mq9_analog, data.mq9_digital ? "ALERT":"OK", - data.mq135_analog, data.mq135_digital ? "ALERT":"OK"); - Serial.printf("Motion: accel=%.2fm/s2 gyro=%.2f deg/s\n", data.motion.totalAccel, data.motion.totalGyro); - Serial.printf("Emergency Button: %s\n", data.emergency ? "PRESSED" : "RELEASED"); - if (wristbandStatus.connected) { - unsigned long age = (millis() - wristbandStatus.lastUpdate) / 1000; - Serial.printf("Wristband: CONNECTED (BPM=%u SpO2=%u finger=%s, %lus ago)\n", - wristbandStatus.bpm, wristbandStatus.spo2, wristbandStatus.fingerDetected?"YES":"NO", age); - } else { - Serial.println("Wristband: DISCONNECTED"); - } -} - -void checkAlerts(SensorData data) { - if (checkSensorDanger(data.mq2_analog, &mq2_cal, MQ2_DANGER_THRESHOLD)) { - Serial.println("!!! MQ2 Danger detected"); - if (audioReady) playAudioFile(SMOKE_ALERT); - } - if (checkSensorDanger(data.mq9_analog, &mq9_cal, MQ9_DANGER_THRESHOLD)) { - Serial.println("!!! MQ9 Danger detected"); - if (audioReady) playAudioFile(CO_ALERT); - } - if (checkSensorDanger(data.mq135_analog, &mq135_cal, MQ135_DANGER_THRESHOLD)) { - Serial.println("!!! MQ135 Air quality degraded"); - if (audioReady) playAudioFile(AIR_QUALITY_WARNING); - } -} - -void sendLoRaData(SensorData data) { - StaticJsonDocument<512> doc; - doc["node"] = NODE_ID; - doc["timestamp"] = data.timestamp; - doc["temp"] = data.temperature; - doc["hum"] = data.humidity; - doc["mq2"] = data.mq2_analog; - doc["mq9"] = data.mq9_analog; - doc["mq135"] = data.mq135_analog; - - // CRITICAL: Explicitly set emergency field (not as 0/1 but as boolean) - doc["emergency"] = data.emergency; // This will be true or false - - doc["motion_accel"] = data.motion.totalAccel; - doc["motion_gyro"] = data.motion.totalGyro; - doc["bpm"] = wristbandStatus.connected ? wristbandStatus.bpm : 0; - doc["spo2"] = wristbandStatus.connected ? wristbandStatus.spo2 : 0; - doc["wristband_connected"] = wristbandStatus.connected ? 1 : 0; - - char payload[512]; - size_t n = serializeJson(doc, payload, sizeof(payload)); - - if (!loraReady) { - Serial.println("LoRa not ready - cannot send"); - return; - } - - LoRa.beginPacket(); - LoRa.print(payload); - LoRa.endPacket(); - - packetCount++; - - // Enhanced logging for emergency packets - if (data.emergency) { - Serial.println("\n╔═══════════════════════════════════════╗"); - Serial.println("║ EMERGENCY PACKET TRANSMITTED ║"); - Serial.println("╚═══════════════════════════════════════╝"); - } - - Serial.printf("LoRa packet #%d sent (%d bytes)%s\n", - packetCount, (int)n, data.emergency ? " [EMERGENCY]" : ""); - Serial.println("Payload: " + String(payload)); -} - -// ---------------- ESP-NOW Implementation ---------------- -void initESPNOW() { - // Ensure WiFi is properly initialized first - WiFi.mode(WIFI_STA); - WiFi.disconnect(false); - delay(100); - WiFi.begin(); - delay(100); - - int channel = 1; - esp_err_t cherr = esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE); - if (cherr == ESP_OK) { - Serial.printf("✓ WiFi channel set to %d for ESP-NOW\n", channel); - } else { - Serial.printf("⚠ Failed to set WiFi channel (%d) err=%d\n", channel, (int)cherr); - } - - if (esp_now_init() != ESP_OK) { - Serial.println("⚠ ESP-NOW init failed - retrying..."); - delay(500); - if (esp_now_init() != ESP_OK) { - Serial.println("✗ ESP-NOW init failed after retry"); - espnowReady = false; - return; - } - } - espnowReady = true; - Serial.println("✓ ESP-NOW initialized"); - - esp_now_register_send_cb(onDataSent); - esp_now_register_recv_cb(onDataRecv); - - delay(100); - - esp_now_peer_info_t peerInfo; - memset(&peerInfo, 0, sizeof(peerInfo)); - memcpy(peerInfo.peer_addr, wristbandMac, 6); - peerInfo.channel = channel; - #if defined(ESP_IF_WIFI_STA) - peerInfo.ifidx = ESP_IF_WIFI_STA; - #elif defined(WIFI_IF_STA) - peerInfo.ifidx = WIFI_IF_STA; - #else - peerInfo.ifidx = (wifi_interface_t)0; - #endif - peerInfo.encrypt = false; - - if (!esp_now_is_peer_exist(wristbandMac)) { - esp_err_t addStatus = esp_now_add_peer(&peerInfo); - if (addStatus != ESP_OK) { - Serial.printf("⚠ Failed to add ESP-NOW peer (wristband) - error: %d\n", addStatus); - } else { - Serial.println("✓ Wristband ESP-NOW peer added"); - Serial.printf(" Peer MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", - wristbandMac[0], wristbandMac[1], wristbandMac[2], - wristbandMac[3], wristbandMac[4], wristbandMac[5]); - } - } else { - Serial.println("✓ Wristband peer already exists"); - } - - delay(100); -} - - -void onDataRecv(const esp_now_recv_info_t *recv_info, const uint8_t *data, int len) { - if (data == NULL || len < 1) return; - - uint8_t msgType = data[0]; - - if (msgType == MSG_TYPE_VITALS) { - if (len < (int)sizeof(espnow_vitals_t)) { - Serial.println("⚠ Received VITALS unexpected length"); - return; - } - - espnow_vitals_t vitals; - memcpy(&vitals, data, sizeof(vitals)); - - wristbandStatus.bpm = vitals.bpm; - wristbandStatus.spo2 = vitals.spo2; - wristbandStatus.fingerDetected = (vitals.finger != 0); - wristbandStatus.lastUpdate = millis(); - wristbandStatus.connected = true; - - Serial.printf("[ESP-NOW RX] VITALS -> BPM=%u SpO2=%u finger=%s ts=%lu\n", - wristbandStatus.bpm, - wristbandStatus.spo2, - wristbandStatus.fingerDetected ? "YES" : "NO", - (unsigned long)vitals.timestamp); - - } else if (msgType == MSG_TYPE_ACK) { - if (len < (int)sizeof(espnow_ack_t)) { - Serial.println("⚠ Received ACK unexpected length"); - return; - } - - espnow_ack_t ack; - memcpy(&ack, data, sizeof(ack)); - - if (ack.messageId == wristbandStatus.lastMessageId) { - wristbandStatus.messageAcknowledged = (ack.success != 0); - - Serial.printf("[ESP-NOW RX] ACK for msgId=%lu success=%s\n", - (unsigned long)ack.messageId, - ack.success ? "YES" : "NO"); - - // NEW: Play audio when message is successfully acknowledged by wristband - if (ack.success && audioReady) { - playAudioFile(MESSAGE_RECEIVED); // Play 0009.mp3 - Serial.println("✓ Playing message received confirmation audio"); - } - - } else { - Serial.printf("[ESP-NOW RX] ACK for unknown msgId=%lu\n", - (unsigned long)ack.messageId); - } - - } else { - Serial.printf("[ESP-NOW RX] Unknown msgType: 0x%02X\n", msgType); - } -} - -void onDataSent(const wifi_tx_info_t *tx_info, esp_now_send_status_t status) { - lastEspNowSendStatus = (status == ESP_NOW_SEND_SUCCESS) ? ESP_OK : ESP_FAIL; - char macStr[18]; - sprintf(macStr, "%02X:%02X:%02X:%02X:%02X:%02X", - wristbandMac[0], wristbandMac[1], wristbandMac[2], - wristbandMac[3], wristbandMac[4], wristbandMac[5]); - Serial.printf("[ESP-NOW TX] to %s status=%s\n", macStr, (status == ESP_NOW_SEND_SUCCESS) ? "OK":"FAIL"); -} - -bool sendTextToWristband(const String &message) { - if (!espnowReady) { Serial.println("ESP-NOW not ready - cannot send text"); return false; } - espnow_text_t pkt; memset(&pkt,0,sizeof(pkt)); - pkt.msgType = MSG_TYPE_TEXT; - pkt.messageId = outgoingMessageCounter++; - size_t len = message.length(); - if (len > MAX_TEXT_LEN - 1) len = MAX_TEXT_LEN - 1; - pkt.length = (uint8_t)len; - memcpy(pkt.text, message.c_str(), pkt.length); - pkt.text[pkt.length] = '\0'; - esp_err_t rc = esp_now_send(wristbandMac, (uint8_t *)&pkt, sizeof(pkt)); - if (rc == ESP_OK) { - wristbandStatus.lastMessageId = pkt.messageId; - wristbandStatus.messageAcknowledged = false; - Serial.printf("Sent TEXT msgId=%lu len=%u\n", (unsigned long)pkt.messageId, pkt.length); - return true; - } else { - Serial.printf("Failed to send TEXT (esp_now_send rc=%d)\n", rc); - return false; - } -} - -void checkWristbandConnection() { - unsigned long now = millis(); - if (wristbandStatus.connected && (now - wristbandStatus.lastUpdate > 35000UL)) { - wristbandStatus.connected = false; - Serial.println("Wristband connection timed out (35s) -> DISCONNECTED"); - } -} - -void receiveLoRaMessages() { - int packetSize = LoRa.parsePacket(); - if (packetSize <= 0) return; - - String incoming = ""; - while (LoRa.available()) incoming += (char)LoRa.read(); - incoming.trim(); - if (incoming.length() == 0) return; - - // Get signal quality - int rssi = LoRa.packetRssi(); - float snr = LoRa.packetSnr(); - - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.printf("║ LORA MESSAGE RECEIVED (RSSI: %4d dBm) ║\n", rssi); - Serial.println("╚════════════════════════════════════════════╝"); - Serial.println("Packet size: " + String(incoming.length()) + " bytes"); - Serial.println("SNR: " + String(snr, 1) + " dB"); - Serial.println("Raw data: " + incoming); - - // Parse JSON - StaticJsonDocument<256> doc; - DeserializationError err = deserializeJson(doc, incoming); - - if (err) { - Serial.println("❌ ERROR: JSON parse failed - " + String(err.c_str())); - Serial.println("════════════════════════════════════════════\n"); - return; - } - - // Check if this is a message relay packet - if (doc.containsKey("message")) { - String message = doc["message"].as(); - String from = doc["from"] | "unknown"; - unsigned long timestamp = doc["timestamp"] | 0; - - Serial.println("\n┌────────────────────────────────────────────┐"); - Serial.println("│ 📨 MESSAGE RELAY REQUEST DETECTED │"); - Serial.println("├────────────────────────────────────────────┤"); - Serial.printf("│ From: %-37s│\n", from.c_str()); - Serial.printf("│ Timestamp: %-32lu│\n", timestamp); - Serial.printf("│ Message: %-34s│\n", message.substring(0, 34).c_str()); - if (message.length() > 34) { - Serial.printf("│ %-34s│\n", message.substring(34, 68).c_str()); - } - Serial.println("└────────────────────────────────────────────┘"); - - Serial.println("\n→ Forwarding to wristband via ESP-NOW..."); - - // Forward to wristband - bool success = sendTextToWristband(message); - - if (success) { - messagesRelayedToWristband++; - Serial.println("✓ Message forwarded successfully!"); - Serial.println(" Total messages relayed: " + String(messagesRelayedToWristband)); - - // Play audio confirmation (if audio is ready) - if (audioReady) { - delay(100); // Small delay before audio - // Note: MESSAGE_RECEIVED audio (0009.mp3) will play automatically - // when wristband sends ACK in onDataRecv() - } - } else { - Serial.println("✗ Failed to forward message to wristband"); - Serial.println(" Check: ESP-NOW status, wristband connection"); - } - - Serial.println("════════════════════════════════════════════\n"); - } - else { - // Not a message packet - could be sensor data or other packet type - Serial.println("ℹ️ Packet received but no 'message' field found"); - Serial.println(" (This is normal for sensor data packets)"); - - // List available fields for debugging - Serial.print(" Available fields: "); - JsonObject obj = doc.as(); - for (JsonPair kv : obj) { - Serial.print(kv.key().c_str()); - Serial.print(" "); - } - Serial.println(); - Serial.println("════════════════════════════════════════════\n"); - } -} -// Enhanced system status with more details -void printSystemStatus() { - unsigned long uptime = millis() / 1000; - int hours = uptime / 3600; - int minutes = (uptime % 3600) / 60; - int seconds = uptime % 60; - - Serial.println("\n╔════════════════════════════════════════════╗"); - Serial.println("║ SYSTEM STATUS REPORT ║"); - Serial.println("╚════════════════════════════════════════════╝"); - - Serial.printf("Uptime: %02d:%02d:%02d\n", hours, minutes, seconds); - Serial.println(); - - Serial.println("Hardware Status:"); - Serial.printf(" LoRa: %s (Packets sent: %d)\n", - loraReady ? "✓ Active" : "✗ Offline", packetCount); - Serial.printf(" ESP-NOW: %s\n", - espnowReady ? "✓ Active" : "✗ Offline"); - Serial.printf(" MPU6050: %s\n", - mpuReady ? "✓ Active" : "✗ Offline"); - Serial.printf(" DHT11: %s\n", - dhtReady ? "✓ Active" : "⚠ Fallback"); - Serial.printf(" Audio: %s\n", - audioReady ? "✓ Active" : "✗ Offline"); - Serial.println(); - - // ========== ADD THIS BLOCK HERE ========== - Serial.println("Message Relay Status:"); - Serial.printf(" Messages relayed to wristband: %lu\n", messagesRelayedToWristband); - Serial.printf(" Last message ID sent: %lu\n", (unsigned long)(outgoingMessageCounter - 1)); - if (wristbandStatus.lastMessageId > 0) { - Serial.printf(" Last message acknowledged: %s\n", - wristbandStatus.messageAcknowledged ? "✓ Yes" : "✗ No"); - } - Serial.println(); - // ========== END OF NEW BLOCK ========== - - if (espnowReady) { - if (wristbandStatus.connected) { - unsigned long age = (millis() - wristbandStatus.lastUpdate) / 1000; - Serial.println("Wristband Status:"); - Serial.printf(" Connection: ✓ Active (%lu seconds ago)\n", age); - Serial.printf(" Heart Rate: %u BPM\n", wristbandStatus.bpm); - Serial.printf(" SpO2: %u%%\n", wristbandStatus.spo2); - Serial.printf(" Finger: %s\n", wristbandStatus.fingerDetected ? "Detected" : "None"); - } else { - Serial.println("Wristband Status:"); - Serial.println(" Connection: ✗ Disconnected"); - } - Serial.println(); - } - - Serial.println("Sensor Calibration:"); - Serial.printf(" MQ2: Baseline=%.0f Threshold=%.0f %s\n", - mq2_cal.baseline, mq2_cal.dangerThreshold, - mq2_cal.calibrated ? "✓" : "✗"); - Serial.printf(" MQ9: Baseline=%.0f Threshold=%.0f %s\n", - mq9_cal.baseline, mq9_cal.dangerThreshold, - mq9_cal.calibrated ? "✓" : "✗"); - Serial.printf(" MQ135: Baseline=%.0f Threshold=%.0f %s\n", - mq135_cal.baseline, mq135_cal.dangerThreshold, - mq135_cal.calibrated ? "✓" : "✗"); - - Serial.println("\n════════════════════════════════════════════\n"); -} diff --git a/workingproto1.cpp b/workingproto1.cpp deleted file mode 100644 index 37876dd..0000000 --- a/workingproto1.cpp +++ /dev/null @@ -1,458 +0,0 @@ -/* - Complete ESP32 Multi-Sensor System with Firebase - - MQ2, MQ9, MQ135 Gas Sensors - - HTU21D Temperature & Humidity - - DFPlayer Mini Audio - - LoRa Communication Module - - WiFi & Firebase Integration - - NTP Time Synchronization -*/ - -#include -#include -#include -#include -#include -#include -#include - -// Firebase helper -#include "addons/TokenHelper.h" -#include "addons/RTDBHelper.h" - -// HTU21D Library (install "Adafruit HTU21DF Library" from Library Manager) -#include - -// DFPlayer Library (install "DFRobotDFPlayerMini" from Library Manager) -#include - -// WiFi Credentials -#define WIFI_SSID "419" -#define WIFI_PASSWORD "xyz@1234" - -// Firebase Configuration -#define API_KEY "AIzaSyDy49OYNumyIrIBrPTOP8dvkeYZGkXn4ac" -#define DATABASE_URL "https://siren-4951f-default-rtdb.asia-southeast1.firebasedatabase.app/" - -// Firebase objects -FirebaseData fbdo; -FirebaseAuth auth; -FirebaseConfig config; - -// NTP Configuration -const char* ntpServer = "pool.ntp.org"; -const long gmtOffset_sec = 19800; // GMT+5:30 India -const int daylightOffset_sec = 0; - -// Pin definitions for MQ Sensors -#define MQ2_DIGITAL_PIN 18 -#define MQ2_ANALOG_PIN 32 -#define MQ9_DIGITAL_PIN 19 -#define MQ9_ANALOG_PIN 33 -#define MQ135_DIGITAL_PIN 21 -#define MQ135_ANALOG_PIN 35 - -// Pin definitions for other modules -#define DFPLAYER_RX 16 -#define DFPLAYER_TX 17 - -// LoRa Module (SPI) -#define LORA_SCK 14 -#define LORA_MISO 12 -#define LORA_MOSI 13 -#define LORA_SS 15 -#define LORA_RST 2 -#define LORA_DIO0 4 - -// Thresholds -#define MQ2_THRESHOLD 2000 -#define MQ9_THRESHOLD 1800 -#define MQ135_THRESHOLD 1000 - -// Initialize modules -Adafruit_HTU21DF htu = Adafruit_HTU21DF(); -SoftwareSerial dfPlayerSerial(DFPLAYER_TX, DFPLAYER_RX); -DFRobotDFPlayerMini dfPlayer; - -// Status flags -bool wifiConnected = false; -bool firebaseReady = false; -bool ntpSynced = false; - -// Data structure for sensor readings -struct SensorData { - float temperature; - float humidity; - int mq2_analog; - int mq9_analog; - int mq135_analog; - bool mq2_digital; - bool mq9_digital; - bool mq135_digital; - unsigned long timestamp; - String dateTime; -}; - -void setup() { - Serial.begin(115200); - Serial.println("================================="); - Serial.println("ESP32 Complete Sensor System v2.0"); - Serial.println("With WiFi & Firebase Integration"); - Serial.println("================================="); - - // Initialize WiFi - initializeWiFi(); - - // Initialize NTP - if (wifiConnected) { - initializeNTP(); - } - - // Initialize Firebase - if (wifiConnected) { - initializeFirebase(); - } - - // Initialize MQ sensor pins - pinMode(MQ2_DIGITAL_PIN, INPUT); - pinMode(MQ9_DIGITAL_PIN, INPUT); - pinMode(MQ135_DIGITAL_PIN, INPUT); - - // Initialize I2C for HTU21D on custom pins - Wire.begin(26, 25); // SDA=26, SCL=25 - if (!htu.begin()) { - Serial.println("HTU21D not detected. Check wiring."); - } else { - Serial.println("HTU21D initialized successfully!"); - } - - - // Initialize DFPlayer Mini - dfPlayerSerial.begin(9600); - if (!dfPlayer.begin(dfPlayerSerial)) { - Serial.println("DFPlayer Mini not detected. Check wiring."); - } else { - Serial.println("DFPlayer Mini initialized successfully!"); - dfPlayer.volume(15); // Set volume (0-30) - } - - // Initialize LoRa - SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_SS); - LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0); - - if (!LoRa.begin(433E6)) { // 433MHz frequency - Serial.println("LoRa initialization failed. Check wiring."); - } else { - Serial.println("LoRa initialized successfully!"); - LoRa.setTxPower(20); // Set transmission power - } - - Serial.println("Warming up gas sensors..."); - delay(10000); // 10 second warm-up - Serial.println("System ready!"); - Serial.println(); -} - -void loop() { - // Check WiFi connection - if (WiFi.status() != WL_CONNECTED && wifiConnected) { - Serial.println("WiFi disconnected. Reconnecting..."); - initializeWiFi(); - } - - // Read all sensors - SensorData data = readAllSensors(); - - // Display readings - displayReadings(data); - - // Check for alerts - checkAlerts(data); - - // Send data to Firebase - if (firebaseReady) { - sendToFirebase(data); - } - - // Send LoRa data - sendLoRaData(data); - - Serial.println("------------------------"); - delay(10000); // Read every 10 seconds for Firebase -} - -void initializeWiFi() { - Serial.print("Connecting to WiFi"); - 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) { - wifiConnected = true; - Serial.println(); - Serial.println("WiFi connected!"); - Serial.print("IP address: "); - Serial.println(WiFi.localIP()); - } else { - wifiConnected = false; - Serial.println(); - Serial.println("WiFi connection failed!"); - } -} - -void initializeNTP() { - Serial.println("Synchronizing time with NTP server..."); - configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); - - struct tm timeinfo; - int attempts = 0; - while (!getLocalTime(&timeinfo) && attempts < 10) { - delay(1000); - Serial.print("."); - attempts++; - } - - if (getLocalTime(&timeinfo)) { - ntpSynced = true; - Serial.println(); - Serial.println("NTP time synchronized!"); - Serial.printf("Current time: %s", asctime(&timeinfo)); - } else { - ntpSynced = false; - Serial.println(); - Serial.println("Failed to sync NTP time!"); - } -} - -void initializeFirebase() { - Serial.println("Initializing Firebase..."); - - // Configure Firebase - config.api_key = API_KEY; - config.database_url = DATABASE_URL; - - // Anonymous authentication - Serial.println("Signing up for anonymous authentication..."); - if (Firebase.signUp(&config, &auth, "", "")) { - Serial.println("Firebase authentication successful!"); - firebaseReady = true; - } else { - Serial.printf("Firebase authentication failed: %s\n", config.signer.signupError.message.c_str()); - firebaseReady = false; - } - - // Assign callback function for token generation - config.token_status_callback = tokenStatusCallback; - - // Initialize Firebase - Firebase.begin(&config, &auth); - Firebase.reconnectWiFi(true); - - Serial.println("Firebase initialized!"); -} - -SensorData readAllSensors() { - SensorData data; - data.timestamp = millis(); - data.dateTime = getCurrentDateTime(); - - // Read HTU21D - data.temperature = htu.readTemperature(); - data.humidity = htu.readHumidity(); - - - // Handle sensor read errors - if (isnan(data.temperature)) data.temperature = -999; - if (isnan(data.humidity)) data.humidity = -999; - - // Read MQ sensors - data.mq2_analog = analogRead(MQ2_ANALOG_PIN); - data.mq9_analog = analogRead(MQ9_ANALOG_PIN); - data.mq135_analog = analogRead(MQ135_ANALOG_PIN); - - data.mq2_digital = digitalRead(MQ2_DIGITAL_PIN) == LOW; - data.mq9_digital = digitalRead(MQ9_DIGITAL_PIN) == LOW; - data.mq135_digital = digitalRead(MQ135_DIGITAL_PIN) == LOW; - - return data; -} - -String getCurrentDateTime() { - if (!ntpSynced) return "Time not synced"; - - struct tm timeinfo; - if (!getLocalTime(&timeinfo)) { - return "Failed to get time"; - } - - char timeString[64]; - strftime(timeString, sizeof(timeString), "%Y-%m-%d %H:%M:%S", &timeinfo); - return String(timeString); -} - -void displayReadings(SensorData data) { - Serial.println("=== SENSOR READINGS ==="); - Serial.println("Time: " + data.dateTime); - Serial.println("WiFi: " + String(wifiConnected ? "Connected" : "Disconnected")); - Serial.println("Firebase: " + String(firebaseReady ? "Ready" : "Not Ready")); - Serial.println(); - - // Environmental data - Serial.println("HTU21D (Temperature & Humidity):"); - if (data.temperature != -999 && data.humidity != -999) { - Serial.printf(" Temperature: %.2f°C\n", data.temperature); - Serial.printf(" Humidity: %.2f%%\n", data.humidity); - } else { - Serial.println(" HTU21D reading error!"); - } - Serial.println(); - - // Gas sensors - Serial.println("MQ2 (Smoke/LPG/Gas):"); - Serial.printf(" Digital: %s | Analog: %d", - data.mq2_digital ? "GAS DETECTED" : "No Gas", data.mq2_analog); - Serial.println(data.mq2_analog > MQ2_THRESHOLD ? " - HIGH!" : " - Normal"); - - Serial.println("MQ9 (Carbon Monoxide):"); - Serial.printf(" Digital: %s | Analog: %d", - data.mq9_digital ? "CO DETECTED" : "No CO", data.mq9_analog); - Serial.println(data.mq9_analog > MQ9_THRESHOLD ? " - HIGH!" : " - Normal"); - - Serial.println("MQ135 (Air Quality/CO2):"); - Serial.printf(" Digital: %s | Analog: %d", - data.mq135_digital ? "POOR AIR" : "Good Air", data.mq135_analog); - Serial.println(data.mq135_analog > MQ135_THRESHOLD ? " - POOR!" : " - Good"); -} - -void checkAlerts(SensorData data) { - static unsigned long lastAlert = 0; - unsigned long now = millis(); - - // Avoid too frequent alerts (minimum 30 seconds between alerts) - if (now - lastAlert < 30000) return; - - // Check for dangerous conditions - if (data.mq2_analog > MQ2_THRESHOLD || data.mq2_digital) { - Serial.println("🚨 ALERT: Smoke/Gas detected!"); - playAlert(1); - sendAlert("SMOKE_GAS_ALERT", "Smoke or gas detected!", data); - lastAlert = now; - } - else if (data.mq9_analog > MQ9_THRESHOLD || data.mq9_digital) { - Serial.println("🚨 ALERT: Carbon Monoxide detected!"); - playAlert(2); - sendAlert("CO_ALERT", "Carbon monoxide detected!", data); - lastAlert = now; - } - else if (data.mq135_analog > MQ135_THRESHOLD || data.mq135_digital) { - Serial.println("⚠️ WARNING: Poor air quality!"); - playAlert(3); - sendAlert("AIR_QUALITY_WARNING", "Poor air quality detected!", data); - lastAlert = now; - } - - // Temperature alerts - if (data.temperature > 40.0) { - Serial.println("🌡️ ALERT: High temperature!"); - playAlert(4); - sendAlert("HIGH_TEMP_ALERT", "High temperature detected!", data); - lastAlert = now; - } - else if (data.temperature < 5.0) { - Serial.println("❄️ ALERT: Low temperature!"); - playAlert(5); - sendAlert("LOW_TEMP_ALERT", "Low temperature detected!", data); - lastAlert = now; - } -} - -void sendToFirebase(SensorData data) { - if (!Firebase.ready()) return; - - // Create JSON object for sensor data - FirebaseJson json; - json.set("timestamp", data.timestamp); - json.set("dateTime", data.dateTime); - json.set("temperature", data.temperature); - json.set("humidity", data.humidity); - json.set("mq2_analog", data.mq2_analog); - json.set("mq9_analog", data.mq9_analog); - json.set("mq135_analog", data.mq135_analog); - json.set("mq2_digital", data.mq2_digital); - json.set("mq9_digital", data.mq9_digital); - json.set("mq135_digital", data.mq135_digital); - json.set("air_quality", getAirQualityRating(data.mq135_analog)); - - // Send to Firebase with timestamp as key - String path = "/sensor_data/" + String(data.timestamp); - - if (Firebase.RTDB.setJSON(&fbdo, path.c_str(), &json)) { - Serial.println("✅ Data sent to Firebase successfully!"); - } else { - Serial.println("❌ Firebase send failed: " + fbdo.errorReason()); - } - - // Also update latest readings - if (Firebase.RTDB.setJSON(&fbdo, "/latest_reading", &json)) { - Serial.println("✅ Latest reading updated!"); - } -} - -void sendAlert(String alertType, String message, SensorData data) { - if (!Firebase.ready()) return; - - FirebaseJson alertJson; - alertJson.set("alertType", alertType); - alertJson.set("message", message); - alertJson.set("timestamp", data.timestamp); - alertJson.set("dateTime", data.dateTime); - alertJson.set("temperature", data.temperature); - alertJson.set("humidity", data.humidity); - alertJson.set("mq2_analog", data.mq2_analog); - alertJson.set("mq9_analog", data.mq9_analog); - alertJson.set("mq135_analog", data.mq135_analog); - - String alertPath = "/alerts/" + String(data.timestamp); - - if (Firebase.RTDB.setJSON(&fbdo, alertPath.c_str(), &alertJson)) { - Serial.println("🚨 Alert sent to Firebase!"); - } else { - Serial.println("❌ Alert send failed: " + fbdo.errorReason()); - } -} - -void playAlert(int fileNumber) { - if (dfPlayer.available()) { - dfPlayer.play(fileNumber); - delay(100); - } -} - -void sendLoRaData(SensorData data) { - String packet = "SENSOR_DATA,"; - packet += String(data.timestamp) + ","; - packet += String(data.temperature, 2) + ","; - packet += String(data.humidity, 2) + ","; - packet += String(data.mq2_analog) + ","; - packet += String(data.mq9_analog) + ","; - packet += String(data.mq135_analog); - - LoRa.beginPacket(); - LoRa.print(packet); - LoRa.endPacket(); - - Serial.println("📡 LoRa packet sent: " + packet); -} - -String getAirQualityRating(int value) { - if (value < 800) return "Excellent"; - else if (value < 1200) return "Good"; - else if (value < 1800) return "Moderate"; - else if (value < 2400) return "Poor"; - else return "Very Poor"; -} diff --git a/wristband try for ESPNOW b/wristband try for ESPNOW deleted file mode 100644 index 90e80e7..0000000 --- a/wristband try for ESPNOW +++ /dev/null @@ -1,569 +0,0 @@ -/* - MAX30102 + OLED Wristband with ESP-NOW Communication - - Heart rate (BPM) and SpO2 monitoring - - Send vitals to edge node every 25 seconds - - Receive and display text messages for 15 seconds - - Send acknowledgment when message received - - Wristband MAC: 0C:4E:A0:66:B2:78 - Edge Node MAC: 28:56:2f:49:56:ac -*/ - -#include -#include -#include -#include -#include -#include - -// OLED Display -#define SCREEN_WIDTH 128 -#define SCREEN_HEIGHT 64 -#define OLED_RESET -1 -Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); - -// I2C Pins -#define SDA_PIN 4 -#define SCL_PIN 5 - -MAX30105 particleSensor; - -// Heart rate calculation -const byte RATE_SIZE = 4; -byte rates[RATE_SIZE]; -byte rateSpot = 0; -long lastBeat = 0; - -float beatsPerMinute = 0; -float avgSpO2 = 0; - -// Timing -unsigned long lastUpdate = 0; -const unsigned long UPDATE_INTERVAL = 500; -unsigned long lastVitalsSent = 0; -const unsigned long VITALS_SEND_INTERVAL = 25000; // 25 seconds - -// ========================= ESP-NOW Configuration ========================= -// Edge Node MAC Address -uint8_t edgeNodeAddress[] = {0x28, 0x56, 0x2f, 0x49, 0x56, 0xac}; - -// ESP-NOW Message Types -#define MSG_TYPE_VITALS 0x01 -#define MSG_TYPE_TEXT 0x02 -#define MSG_TYPE_ACK 0x03 - -// ESP-NOW Data Structures -struct VitalsData { - uint8_t msgType; - int bpm; - int spo2; - bool fingerDetected; - unsigned long timestamp; -}; - -struct TextMessage { - uint8_t msgType; - char message[128]; - unsigned long messageId; -}; - -struct AckMessage { - uint8_t msgType; - unsigned long messageId; - bool success; -}; - -// Message display state -bool displayingMessage = false; -String currentMessage = ""; -unsigned long messageStartTime = 0; -const unsigned long MESSAGE_DISPLAY_DURATION = 15000; // 15 seconds -unsigned long currentMessageId = 0; - -// Connection status -bool edgeNodeConnected = false; -unsigned long lastEdgeNodeContact = 0; -const unsigned long CONNECTION_TIMEOUT = 60000; // 60 seconds - -bool espnowReady = false; - -// ========================= ESP-NOW Functions ========================= - -void initESPNOW() { - Serial.println("\n--- Initializing ESP-NOW ---"); - - // Set device as WiFi Station - WiFi.mode(WIFI_STA); - - // Print MAC Address - Serial.print("Wristband MAC Address: "); - Serial.println(WiFi.macAddress()); - - // Initialize ESP-NOW - if (esp_now_init() != ESP_OK) { - Serial.println("❌ ESP-NOW initialization failed"); - espnowReady = false; - return; - } - - Serial.println("✓ ESP-NOW initialized"); - - // Register callbacks - esp_now_register_send_cb(onDataSent); - esp_now_register_recv_cb(onDataRecv); - - // Register edge node peer - esp_now_peer_info_t peerInfo; - memset(&peerInfo, 0, sizeof(peerInfo)); - memcpy(peerInfo.peer_addr, edgeNodeAddress, 6); - peerInfo.channel = 0; - peerInfo.encrypt = false; - - if (esp_now_add_peer(&peerInfo) != ESP_OK) { - Serial.println("❌ Failed to add edge node peer"); - espnowReady = false; - return; - } - - Serial.println("✓ Edge node peer registered"); - Serial.printf("✓ Edge Node MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", - edgeNodeAddress[0], edgeNodeAddress[1], edgeNodeAddress[2], - edgeNodeAddress[3], edgeNodeAddress[4], edgeNodeAddress[5]); - - espnowReady = true; - edgeNodeConnected = true; // Assume connected initially - lastEdgeNodeContact = millis(); -} - -// Callback when data is sent -void onDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) { - if (status == ESP_NOW_SEND_SUCCESS) { - edgeNodeConnected = true; - lastEdgeNodeContact = millis(); - } else { - Serial.println("⚠ ESP-NOW send failed"); - } -} - -// Callback when data is received -void onDataRecv(const uint8_t *mac, const uint8_t *incomingData, int len) { - edgeNodeConnected = true; - lastEdgeNodeContact = millis(); - - uint8_t msgType = incomingData[0]; - - if (msgType == MSG_TYPE_TEXT && len == sizeof(TextMessage)) { - TextMessage txtMsg; - memcpy(&txtMsg, incomingData, sizeof(txtMsg)); - - currentMessage = String(txtMsg.message); - currentMessageId = txtMsg.messageId; - displayingMessage = true; - messageStartTime = millis(); - - Serial.println("\n📨 Message received from edge node:"); - Serial.println(currentMessage); - - // Send acknowledgment - sendAcknowledgment(currentMessageId, true); - } -} - -// Send vitals to edge node -void sendVitals(int bpm, int spo2, bool fingerDetected) { - if (!espnowReady) return; - - VitalsData vitals; - vitals.msgType = MSG_TYPE_VITALS; - vitals.bpm = bpm; - vitals.spo2 = spo2; - vitals.fingerDetected = fingerDetected; - vitals.timestamp = millis(); - - esp_err_t result = esp_now_send(edgeNodeAddress, (uint8_t *)&vitals, sizeof(vitals)); - - if (result == ESP_OK) { - Serial.println("📤 Vitals sent to edge node"); - Serial.printf(" BPM: %d | SpO2: %d%% | Finger: %s\n", - bpm, spo2, fingerDetected ? "YES" : "NO"); - } else { - Serial.println("❌ Failed to send vitals"); - } -} - -// Send acknowledgment for received message -void sendAcknowledgment(unsigned long messageId, bool success) { - if (!espnowReady) return; - - AckMessage ack; - ack.msgType = MSG_TYPE_ACK; - ack.messageId = messageId; - ack.success = success; - - esp_err_t result = esp_now_send(edgeNodeAddress, (uint8_t *)&ack, sizeof(ack)); - - if (result == ESP_OK) { - Serial.println("✓ Acknowledgment sent"); - } else { - Serial.println("⚠ Failed to send acknowledgment"); - } -} - -// Check connection status -void checkConnection() { - if (edgeNodeConnected && (millis() - lastEdgeNodeContact > CONNECTION_TIMEOUT)) { - edgeNodeConnected = false; - Serial.println("⚠ Edge node connection lost"); - } -} - -// ========================= Display Functions ========================= - -void displayHeartRate(long irValue, float avgBPM) { - display.clearDisplay(); - display.setTextSize(1); - display.setTextColor(SSD1306_WHITE); - - // Title with connection status - display.setCursor(0, 0); - display.print("HR MONITOR"); - - // Connection indicator (top right) - if (edgeNodeConnected) { - display.setCursor(100, 0); - display.print("[OK]"); - } else { - display.setCursor(95, 0); - display.print("[DISC]"); - } - - display.drawLine(0, 10, 128, 10, SSD1306_WHITE); - - // Check if finger is detected - if (irValue < 50000) { - display.setTextSize(2); - display.setCursor(15, 25); - display.println("NO FINGER"); - display.setTextSize(1); - display.setCursor(5, 50); - display.println("Place finger on sensor"); - } else { - // BPM Display - display.setTextSize(3); - display.setCursor(0, 18); - if (avgBPM > 0) { - display.print((int)avgBPM); - } else { - display.print("--"); - } - display.setTextSize(1); - display.setCursor(50, 25); - display.println("BPM"); - - // SpO2 Display - display.setTextSize(2); - display.setCursor(0, 45); - display.print((int)avgSpO2); - display.println("%"); - - display.setTextSize(1); - display.setCursor(50, 50); - display.println("SpO2"); - - // Signal indicator - display.setCursor(90, 50); - if (irValue > 150000) { - display.println("+++"); - } else if (irValue > 100000) { - display.println("++"); - } else { - display.println("+"); - } - } - - display.display(); -} - -void displayMessage() { - display.clearDisplay(); - display.setTextSize(1); - display.setTextColor(SSD1306_WHITE); - - // Header - display.setCursor(0, 0); - display.println("MESSAGE RECEIVED"); - display.drawLine(0, 10, 128, 10, SSD1306_WHITE); - - // Calculate time remaining - unsigned long elapsed = millis() - messageStartTime; - unsigned long remaining = (MESSAGE_DISPLAY_DURATION - elapsed) / 1000; - - // Display message (word wrap) - display.setTextSize(1); - int yPos = 15; - int lineHeight = 10; - String msg = currentMessage; - int maxCharsPerLine = 21; // 128 pixels / 6 pixels per char - - int startIdx = 0; - while (startIdx < msg.length() && yPos < 48) { - int endIdx = startIdx + maxCharsPerLine; - if (endIdx > msg.length()) endIdx = msg.length(); - - // Find last space before maxCharsPerLine - if (endIdx < msg.length() && msg.charAt(endIdx) != ' ') { - int lastSpace = msg.lastIndexOf(' ', endIdx); - if (lastSpace > startIdx) { - endIdx = lastSpace; - } - } - - String line = msg.substring(startIdx, endIdx); - display.setCursor(0, yPos); - display.println(line); - yPos += lineHeight; - startIdx = endIdx + 1; // Skip the space - } - - // Timer at bottom - display.drawLine(0, 52, 128, 52, SSD1306_WHITE); - display.setCursor(0, 55); - display.print("Returns in: "); - display.print(remaining); - display.println("s"); - - display.display(); -} - -void displayNoConnection() { - display.clearDisplay(); - display.setTextSize(1); - display.setTextColor(SSD1306_WHITE); - - display.setCursor(0, 0); - display.println("DISCONNECTED"); - display.drawLine(0, 10, 128, 10, SSD1306_WHITE); - - display.setTextSize(2); - display.setCursor(10, 25); - display.println("NO EDGE"); - display.setCursor(10, 43); - display.println("NODE"); - - display.display(); -} - -// ========================= Setup & Loop ========================= - -void setup() { - Serial.begin(115200); - while(!Serial) { - delay(10); - } - - delay(1000); - Serial.println("\n\n========================================"); - Serial.println("Wristband with ESP-NOW Communication"); - Serial.println("MAX30102 + OLED"); - Serial.println("========================================\n"); - - // Initialize I2C bus - Serial.println("Initializing I2C Bus..."); - Wire.begin(SDA_PIN, SCL_PIN); - Wire.setClock(400000); - delay(500); - Serial.println("✓ I2C initialized\n"); - - // Initialize ESP-NOW BEFORE I2C devices to ensure stable operation - initESPNOW(); - delay(500); - - // Scan bus - Serial.println("Scanning I2C bus..."); - scanBus(); - delay(500); - - // Initialize OLED - Serial.println("Initializing OLED at 0x3C..."); - if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { - Serial.println("ERROR: OLED not found!"); - while(1) { - Serial.println("OLED Error - Check wiring"); - delay(2000); - } - } - Serial.println("✓ OLED initialized!\n"); - - display.clearDisplay(); - display.setTextSize(1); - display.setTextColor(SSD1306_WHITE); - display.setCursor(0, 0); - display.println("Initializing..."); - display.setCursor(0, 15); - display.println("MAX30102 sensor"); - display.display(); - - delay(500); - - // Initialize MAX30102 - Serial.println("Initializing MAX30102 at 0x57..."); - if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) { - Serial.println("ERROR: MAX30102 not found!"); - display.clearDisplay(); - display.setTextSize(1); - display.setTextColor(SSD1306_WHITE); - display.setCursor(0, 0); - display.println("MAX30102 Error!"); - display.setCursor(0, 10); - display.println("Check wiring"); - display.display(); - - while(1) { - Serial.println("MAX30102 Error - Check wiring"); - delay(2000); - } - } - - Serial.println("✓ MAX30102 initialized!\n"); - - // Configure MAX30102 - particleSensor.setup(); - particleSensor.setPulseAmplitudeRed(0x1F); - particleSensor.setPulseAmplitudeGreen(0); - particleSensor.setPulseAmplitudeIR(0x33); - - Serial.println("✓ Setup complete!\n"); - Serial.println("System ready - Starting heart rate monitoring"); - Serial.println("Vitals will be sent to edge node every 25 seconds\n"); - - // Show welcome screen - display.clearDisplay(); - display.setTextSize(1); - display.setTextColor(SSD1306_WHITE); - display.setCursor(0, 0); - display.println("Wristband Ready"); - display.setCursor(0, 15); - display.println("Place finger on"); - display.setCursor(0, 25); - display.println("sensor..."); - display.setCursor(0, 45); - if (espnowReady) { - display.println("ESP-NOW: Connected"); - } else { - display.println("ESP-NOW: Failed"); - } - display.display(); - - delay(3000); -} - -void loop() { - // Check connection status - checkConnection(); - - // Read sensor values - long irValue = particleSensor.getIR(); - long redValue = particleSensor.getRed(); - bool fingerDetected = (irValue > 50000); - - // Detect heartbeat - if (checkForBeat(irValue) == true) { - long delta = millis() - lastBeat; - lastBeat = millis(); - - beatsPerMinute = 60 / (delta / 1000.0); - - if (beatsPerMinute < 255 && beatsPerMinute > 20) { - rates[rateSpot++] = (byte)beatsPerMinute; - rateSpot %= RATE_SIZE; - } - } - - // Calculate average BPM - float avgBPM = 0; - for (byte x = 0; x < RATE_SIZE; x++) - avgBPM += rates[x]; - avgBPM /= RATE_SIZE; - - // Calculate SpO2 - if (redValue > 0 && irValue > 0) { - float ratio = (float)redValue / irValue; - avgSpO2 = 110 - 25 * ratio; - - if (avgSpO2 < 80) avgSpO2 = 80; - if (avgSpO2 > 100) avgSpO2 = 100; - } - - // Send vitals to edge node every 25 seconds - if (espnowReady && (millis() - lastVitalsSent >= VITALS_SEND_INTERVAL)) { - int bpmToSend = fingerDetected ? (int)avgBPM : 0; - int spo2ToSend = fingerDetected ? (int)avgSpO2 : 0; - sendVitals(bpmToSend, spo2ToSend, fingerDetected); - lastVitalsSent = millis(); - } - - // Update display and serial every UPDATE_INTERVAL - if (millis() - lastUpdate > UPDATE_INTERVAL) { - lastUpdate = millis(); - - // Check if message display duration has expired - if (displayingMessage && (millis() - messageStartTime >= MESSAGE_DISPLAY_DURATION)) { - displayingMessage = false; - Serial.println("Message display timeout - returning to heart rate display"); - } - - // Update display based on state - if (displayingMessage) { - displayMessage(); - } else if (!edgeNodeConnected) { - displayNoConnection(); - } else { - displayHeartRate(irValue, avgBPM); - } - - // Serial output for debugging - if (!displayingMessage) { - Serial.print("BPM: "); - if (avgBPM > 0 && fingerDetected) { - Serial.print((int)avgBPM); - } else { - Serial.print("--"); - } - Serial.print(" | SpO2: "); - if (fingerDetected) { - Serial.print((int)avgSpO2); - Serial.print("%"); - } else { - Serial.print("--"); - } - Serial.print(" | IR: "); - Serial.print(irValue); - Serial.print(" | Edge: "); - Serial.println(edgeNodeConnected ? "Connected" : "Disconnected"); - } - } -} - -void scanBus() { - int foundCount = 0; - - for(byte i = 0; i < 128; i++) { - Wire.beginTransmission(i); - byte error = Wire.endTransmission(); - - if (error == 0) { - Serial.print(" Found device at: 0x"); - if (i < 16) Serial.print("0"); - Serial.println(i, HEX); - foundCount++; - } - } - - if(foundCount == 0) { - Serial.println(" No devices found!"); - } else { - Serial.print(" Total: "); - Serial.print(foundCount); - Serial.println(" device(s)\n"); - } -} diff --git a/wristband_updated_ESPNOW.ino b/wristband_updated_ESPNOW.ino deleted file mode 100644 index 2f75c99..0000000 --- a/wristband_updated_ESPNOW.ino +++ /dev/null @@ -1,539 +0,0 @@ -// wristband_espnow.ino -// Wristband with MAX30102 + SSD1306 + ESP-NOW integration -// FIXED: Message display timeout and auto-scrolling for long text - -#include -#include -#include -#include -#include -#include -#include - -// OLED Display -#define SCREEN_WIDTH 128 -#define SCREEN_HEIGHT 64 -#define OLED_RESET -1 -Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); - -// I2C Pins -#define SDA_PIN 4 -#define SCL_PIN 5 - -MAX30105 particleSensor; - -// Heart rate calculation -const byte RATE_SIZE = 4; -byte rates[RATE_SIZE]; -byte rateSpot = 0; -long lastBeat = 0; - -float beatsPerMinute = 0; -float avgSpO2 = 0; - -// Timing -unsigned long lastUpdate = 0; -const unsigned long UPDATE_INTERVAL = 100; // faster update for smooth scrolling - -// ESP-NOW message types -#define MSG_TYPE_VITALS 0x01 -#define MSG_TYPE_TEXT 0x02 -#define MSG_TYPE_ACK 0x03 -#define MAX_TEXT_LEN 128 - -typedef struct __attribute__((packed)) { - uint8_t msgType; - uint8_t bpm; - uint8_t spo2; - uint8_t finger; - uint32_t timestamp; -} espnow_vitals_t; - -typedef struct __attribute__((packed)) { - uint8_t msgType; - uint32_t messageId; - uint8_t length; - char text[MAX_TEXT_LEN]; -} espnow_text_t; - -typedef struct __attribute__((packed)) { - uint8_t msgType; - uint32_t messageId; - uint8_t success; -} espnow_ack_t; - -// Peer MAC -uint8_t edgeNodeMac[6] = {0x28, 0x56, 0x2F, 0x49, 0x56, 0xAC}; - -// Display state -bool displayingMessage = false; -String currentMessage = ""; -unsigned long messageStartTime = 0; -uint32_t currentMessageId = 0; -int scrollOffset = 0; -unsigned long lastScrollUpdate = 0; -const unsigned long SCROLL_SPEED = 150; // milliseconds per pixel scroll - -bool edgeNodeConnected = false; -unsigned long lastEdgeNodeContact = 0; -bool espnowReady = false; - -const unsigned long VITALS_INTERVAL = 25000UL; -unsigned long lastVitalsSent = 0; -const unsigned long MESSAGE_DISPLAY_MS = 15000UL; -const unsigned long EDGE_NODE_DISCONNECT_MS = 60000UL; - -// Forward declarations -void initESPNOW(); -void onDataRecv(const esp_now_recv_info_t *recv_info, const uint8_t *data, int len); -void onDataSent(const wifi_tx_info_t *tx_info, esp_now_send_status_t status); -void sendVitals(uint8_t bpm, uint8_t spo2, bool fingerDetected); -void sendAcknowledgment(uint32_t messageId, bool success); -void checkConnection(); -void displayMessageScreen(const String &msg, uint32_t messageId); -void displayVitalsScreen(uint8_t bpm, uint8_t spo2, bool finger, bool connected); -void scanBus(); -int calculateTextHeight(const String &text, int maxWidthChars, int lineHeight); - -void setup() { - Serial.begin(115200); - while(!Serial) { delay(10); } - delay(200); - - Serial.println("\n\nMAX30102 + OLED Heart Rate Monitor + ESP-NOW (with auto-scroll)"); - - WiFi.mode(WIFI_STA); - WiFi.disconnect(false); - delay(100); - WiFi.begin(); - delay(100); - - int channel = 1; - esp_err_t cherr = esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE); - if (cherr == ESP_OK) { - Serial.printf("✓ WiFi channel set to %d\n", channel); - } else { - Serial.printf("⚠ Failed to set WiFi channel err=%d\n", (int)cherr); - } - - initESPNOW(); - delay(200); - - Wire.begin(SDA_PIN, SCL_PIN); - Wire.setClock(400000); - delay(500); - - Serial.println("\n=== I2C Device Scan ==="); - scanBus(); - delay(500); - - if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { - Serial.println("ERROR: OLED not found!"); - while(1) { Serial.println("OLED Error - Check wiring"); delay(2000); } - } - - display.clearDisplay(); - display.setTextSize(1); - display.setTextColor(SSD1306_WHITE); - display.setCursor(0, 0); - display.println("Initializing MAX..."); - display.display(); - delay(500); - - if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) { - Serial.println("ERROR: MAX30102 not found!"); - display.clearDisplay(); - display.setTextSize(1); - display.setCursor(0, 0); - display.println("MAX30102 Error!"); - display.display(); - while(1) { - Serial.println("MAX30102 Error"); - delay(2000); - } - } - - particleSensor.setup(); - particleSensor.setPulseAmplitudeRed(0x1F); - particleSensor.setPulseAmplitudeGreen(0); - particleSensor.setPulseAmplitudeIR(0x33); - - display.clearDisplay(); - display.setTextSize(1); - display.setCursor(0, 0); - display.println("Heart Rate Monitor"); - display.setCursor(0, 15); - display.println("Place finger on sensor"); - display.display(); - delay(3000); - - lastVitalsSent = millis() - (VITALS_INTERVAL - 5000); - Serial.println("\n✓ Wristband ready"); -} - -void loop() { - long irValue = particleSensor.getIR(); - long redValue = particleSensor.getRed(); - - if (checkForBeat(irValue) == true) { - long delta = millis() - lastBeat; - lastBeat = millis(); - beatsPerMinute = 60 / (delta / 1000.0); - - if (beatsPerMinute < 255 && beatsPerMinute > 20) { - rates[rateSpot++] = (byte)beatsPerMinute; - rateSpot %= RATE_SIZE; - } - } - - float avgBPM = 0; - for (byte x = 0; x < RATE_SIZE; x++) - avgBPM += rates[x]; - avgBPM /= RATE_SIZE; - - if (redValue > 0 && irValue > 0) { - float ratio = (float)redValue / irValue; - avgSpO2 = 110 - 25 * ratio; - if (avgSpO2 < 80) avgSpO2 = 80; - if (avgSpO2 > 100) avgSpO2 = 100; - } - - if (millis() - lastUpdate > UPDATE_INTERVAL) { - lastUpdate = millis(); - - // Check if message display period has expired - if (displayingMessage) { - unsigned long elapsed = millis() - messageStartTime; - if (elapsed >= MESSAGE_DISPLAY_MS) { - // Time's up - return to vitals display - displayingMessage = false; - currentMessage = ""; - scrollOffset = 0; - Serial.println("Message display timeout - returning to vitals"); - } else { - // Continue displaying message with scrolling - displayMessageScreen(currentMessage, currentMessageId); - } - } else { - // Normal vitals display - bool fingerDetected = (irValue > 50000); - displayVitalsScreen((uint8_t)avgBPM, (uint8_t)avgSpO2, fingerDetected, edgeNodeConnected); - } - - Serial.print("BPM: "); - if (avgBPM > 0) Serial.print((int)avgBPM); else Serial.print("--"); - Serial.print(" | SpO2: "); - Serial.print((int)avgSpO2); - Serial.print("% | IR: "); - Serial.println(irValue); - } - - if (millis() - lastVitalsSent >= VITALS_INTERVAL) { - long irVal = particleSensor.getIR(); - bool fingerDetected = (irVal > 50000); - uint8_t bpmSend = 0; - uint8_t spo2Send = 0; - if (fingerDetected && beatsPerMinute > 0 && beatsPerMinute < 255) bpmSend = (uint8_t)beatsPerMinute; - if (fingerDetected && avgSpO2 >= 0 && avgSpO2 <= 100) spo2Send = (uint8_t)avgSpO2; - sendVitals(bpmSend, spo2Send, fingerDetected); - lastVitalsSent = millis(); - } - - checkConnection(); - delay(20); -} - -void scanBus() { - int foundCount = 0; - for(byte i = 0; i < 128; i++) { - Wire.beginTransmission(i); - byte error = Wire.endTransmission(); - if (error == 0) { - Serial.print(" Found device at: 0x"); - if (i < 16) Serial.print("0"); - Serial.println(i, HEX); - foundCount++; - } - } - Serial.print(" Total: "); - Serial.print(foundCount); - Serial.println(" device(s)\n"); -} - -void initESPNOW() { - if (WiFi.status() == WL_NO_SHIELD) { - Serial.println("⚠ WiFi not ready"); - WiFi.mode(WIFI_STA); - delay(100); - } - - if (esp_now_init() != ESP_OK) { - Serial.println("⚠ ESP-NOW init failed - retrying..."); - delay(500); - if (esp_now_init() != ESP_OK) { - Serial.println("✗ ESP-NOW init failed"); - espnowReady = false; - return; - } - } - espnowReady = true; - Serial.println("✓ ESP-NOW initialized"); - - esp_now_register_send_cb(onDataSent); - esp_now_register_recv_cb(onDataRecv); - delay(100); - - esp_now_peer_info_t peerInfo; - memset(&peerInfo, 0, sizeof(peerInfo)); - memcpy(peerInfo.peer_addr, edgeNodeMac, 6); - peerInfo.channel = 1; - - #if defined(ESP_IF_WIFI_STA) - peerInfo.ifidx = ESP_IF_WIFI_STA; - #elif defined(WIFI_IF_STA) - peerInfo.ifidx = WIFI_IF_STA; - #else - peerInfo.ifidx = (wifi_interface_t)0; - #endif - - peerInfo.encrypt = false; - - if (!esp_now_is_peer_exist(edgeNodeMac)) { - esp_err_t addStatus = esp_now_add_peer(&peerInfo); - if (addStatus != ESP_OK) { - Serial.printf("⚠ Failed to add peer - error: %d\n", addStatus); - } else { - Serial.println("✓ Edge node peer added"); - } - } - delay(100); -} - -void onDataRecv(const esp_now_recv_info_t *recv_info, const uint8_t *data, int len) { - if (data == NULL || len < 1) return; - uint8_t msgType = data[0]; - - lastEdgeNodeContact = millis(); - edgeNodeConnected = true; - - if (msgType == MSG_TYPE_TEXT) { - if (len < 6) return; - uint32_t msgId = 0; - uint8_t lengthField = 0; - memcpy(&msgId, &data[1], sizeof(msgId)); - memcpy(&lengthField, &data[5], 1); - if (lengthField > MAX_TEXT_LEN - 1) lengthField = MAX_TEXT_LEN - 1; - char txtbuf[MAX_TEXT_LEN + 1]; - memset(txtbuf, 0, sizeof(txtbuf)); - int copyLen = min((int)lengthField, len - 6); - if (copyLen > 0) memcpy(txtbuf, &data[6], copyLen); - txtbuf[copyLen] = '\0'; - - String receivedText = String(txtbuf); - Serial.printf("[ESP-NOW RX] TEXT msgId=%lu text='%s'\n", (unsigned long)msgId, receivedText.c_str()); - - // Reset scroll and start displaying - scrollOffset = 0; - lastScrollUpdate = millis(); - displayingMessage = true; - currentMessage = receivedText; - messageStartTime = millis(); - currentMessageId = msgId; - - displayMessageScreen(receivedText, msgId); - sendAcknowledgment(msgId, true); - } -} - -void onDataSent(const wifi_tx_info_t *tx_info, esp_now_send_status_t status) { - Serial.printf("[ESP-NOW TX] status=%s\n", (status == ESP_NOW_SEND_SUCCESS) ? "OK":"FAIL"); -} - -void sendVitals(uint8_t bpm, uint8_t spo2, bool fingerDetected) { - if (!espnowReady) return; - espnow_vitals_t pkt; - pkt.msgType = MSG_TYPE_VITALS; - pkt.bpm = bpm; - pkt.spo2 = spo2; - pkt.finger = fingerDetected ? 1 : 0; - pkt.timestamp = (uint32_t)millis(); - - esp_err_t rc = esp_now_send(edgeNodeMac, (uint8_t *)&pkt, sizeof(pkt)); - if (rc == ESP_OK) { - Serial.printf("Sent VITALS bpm=%u spo2=%u finger=%s\n", bpm, spo2, fingerDetected?"YES":"NO"); - } -} - -void sendAcknowledgment(uint32_t messageId, bool success) { - if (!espnowReady) return; - espnow_ack_t ack; - ack.msgType = MSG_TYPE_ACK; - ack.messageId = messageId; - ack.success = success ? 1 : 0; - esp_now_send(edgeNodeMac, (uint8_t *)&ack, sizeof(ack)); - Serial.printf("Sent ACK msgId=%lu\n", (unsigned long)messageId); -} - -void checkConnection() { - unsigned long now = millis(); - if (edgeNodeConnected && (now - lastEdgeNodeContact > EDGE_NODE_DISCONNECT_MS)) { - edgeNodeConnected = false; - Serial.println("Edge node timeout -> DISCONNECTED"); - display.clearDisplay(); - display.setTextSize(2); - display.setCursor(0, 10); - display.println("NO EDGE NODE"); - display.setTextSize(1); - display.setCursor(0, 40); - display.println("Reconnecting..."); - display.display(); - } -} - -// Calculate total height needed for wrapped text -int calculateTextHeight(const String &text, int maxWidthChars, int lineHeight) { - int start = 0; - int len = text.length(); - int lines = 0; - - while (start < len) { - int remaining = len - start; - int take = min(remaining, maxWidthChars); - - if (take == maxWidthChars) { - int lastSpace = -1; - for (int i = 0; i < take; i++) { - if (text.charAt(start + i) == ' ') lastSpace = i; - } - if (lastSpace > 0) take = lastSpace; - } - - lines++; - start += take; - while (start < len && text.charAt(start) == ' ') start++; - } - - return lines * lineHeight; -} - -void displayMessageScreen(const String &msg, uint32_t messageId) { - const int maxWidthChars = 21; - const int lineHeight = 10; - const int textStartY = 12; - const int displayHeight = 52; // available height for text (64 - 12 header) - - // Calculate total text height - int totalTextHeight = calculateTextHeight(msg, maxWidthChars, lineHeight); - - // Update scroll position if text is taller than display area - if (totalTextHeight > displayHeight) { - if (millis() - lastScrollUpdate > SCROLL_SPEED) { - scrollOffset++; - // Loop back to start when we've scrolled through everything - int maxScroll = totalTextHeight - displayHeight + lineHeight; - if (scrollOffset > maxScroll) { - scrollOffset = -20; // pause at top before repeating - } - lastScrollUpdate = millis(); - } - } else { - scrollOffset = 0; // no scrolling needed - } - - display.clearDisplay(); - display.setTextSize(1); - display.setTextColor(SSD1306_WHITE); - - // Header - display.setCursor(0, 0); - display.println("MSG:"); - display.drawLine(0, 10, 128, 10, SSD1306_WHITE); - - // Countdown timer - unsigned long elapsed = millis() - messageStartTime; - unsigned long remaining = 0; - if (elapsed < MESSAGE_DISPLAY_MS) { - remaining = (MESSAGE_DISPLAY_MS - elapsed) / 1000; - } - display.setCursor(90, 0); - display.printf("%2lus", remaining); - - // Draw text with scrolling - int start = 0; - int len = msg.length(); - int line = 0; - int currentY = textStartY - scrollOffset; - - while (start < len) { - int remaining = len - start; - int take = min(remaining, maxWidthChars); - - if (take == maxWidthChars) { - int lastSpace = -1; - for (int i = 0; i < take; i++) { - if (msg.charAt(start + i) == ' ') lastSpace = i; - } - if (lastSpace > 0) take = lastSpace; - } - - String part = msg.substring(start, start + take); - - // Only draw if line is visible - if (currentY >= textStartY - lineHeight && currentY < SCREEN_HEIGHT) { - display.setCursor(0, currentY); - display.println(part); - } - - line++; - currentY += lineHeight; - start += take; - while (start < len && msg.charAt(start) == ' ') start++; - } - - display.display(); -} - -void displayVitalsScreen(uint8_t bpm, uint8_t spo2, bool finger, bool connected) { - display.clearDisplay(); - display.setTextSize(1); - display.setTextColor(SSD1306_WHITE); - - // Connection indicator - display.setCursor(88, 0); - if (connected) display.print("[OK]"); - else display.print("[DISC]"); - - display.setCursor(0, 0); - display.println("HEART RATE MONITOR"); - display.drawLine(0, 10, 128, 10, SSD1306_WHITE); - - if (!finger) { - display.setTextSize(2); - display.setCursor(15, 25); - display.println("SIREN"); - display.setTextSize(1); - display.setCursor(10, 50); - display.println("Small Text"); - } else { - display.setTextSize(3); - display.setCursor(0, 18); - if (bpm > 0) display.print(bpm); else display.print("--"); - display.setTextSize(1); - display.setCursor(50, 25); - display.println("BPM"); - - display.setTextSize(2); - display.setCursor(0, 45); - display.print(spo2); - display.println("%"); - - display.setTextSize(1); - display.setCursor(50, 50); - display.println("SpO2"); - - display.setCursor(90, 50); - display.println("++"); - } - - display.display(); -} diff --git a/wristwatch.cpp b/wristwatch.cpp deleted file mode 100644 index 9df28c8..0000000 --- a/wristwatch.cpp +++ /dev/null @@ -1,246 +0,0 @@ -#include -#include -#include -#include - -// OLED Display -#define SCREEN_WIDTH 128 -#define SCREEN_HEIGHT 64 -#define OLED_RESET -1 -Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); - -// I2C Pins - Both devices on same bus (GPIO4=SDA, GPIO5=SCL) -#define SDA_PIN 4 -#define SCL_PIN 5 - -MAX30105 particleSensor; - -// Heart rate calculation -const byte RATE_SIZE = 4; -byte rates[RATE_SIZE]; -byte rateSpot = 0; -long lastBeat = 0; - -float beatsPerMinute = 0; -float avgSpO2 = 0; - -// Timing -unsigned long lastUpdate = 0; -const unsigned long UPDATE_INTERVAL = 500; - -void setup() { - Serial.begin(115200); - while(!Serial) { - delay(10); - } - - delay(1000); - Serial.println("\n\n========================================"); - Serial.println("MAX30102 + OLED Heart Rate Monitor"); - Serial.println("Shared I2C Bus (GPIO4=SDA, GPIO5=SCL)"); - Serial.println("========================================\n"); - - // Initialize I2C bus (both devices on same bus) - Serial.println("Initializing I2C Bus..."); - Wire.begin(SDA_PIN, SCL_PIN); - Wire.setClock(400000); - delay(500); - Serial.println("✓ I2C initialized\n"); - - // Scan bus - Serial.println("Scanning I2C bus..."); - scanBus(); - delay(500); - - // Initialize OLED - Serial.println("Initializing OLED at 0x3C..."); - if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { - Serial.println("ERROR: OLED not found!"); - while(1) { - Serial.println("OLED Error - Check wiring"); - delay(2000); - } - } - Serial.println("✓ OLED initialized!\n"); - - display.clearDisplay(); - display.setTextSize(1); - display.setTextColor(SSD1306_WHITE); - display.setCursor(0, 0); - display.println("Initializing MAX..."); - display.display(); - - delay(500); - - // Initialize MAX30102 - Serial.println("Initializing MAX30102 at 0x57..."); - if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) { - Serial.println("ERROR: MAX30102 not found!"); - display.clearDisplay(); - display.setTextSize(1); - display.setTextColor(SSD1306_WHITE); - display.setCursor(0, 0); - display.println("MAX30102 Error!"); - display.setCursor(0, 10); - display.println("Check wiring"); - display.display(); - - while(1) { - Serial.println("MAX30102 Error - Check wiring"); - delay(2000); - } - } - - Serial.println("✓ MAX30102 initialized!\n"); - - // Configure MAX30102 - particleSensor.setup(); - particleSensor.setPulseAmplitudeRed(0x1F); - particleSensor.setPulseAmplitudeGreen(0); - particleSensor.setPulseAmplitudeIR(0x33); - - Serial.println("✓ Setup complete!\n"); - - // Show welcome screen - display.clearDisplay(); - display.setTextSize(1); - display.setTextColor(SSD1306_WHITE); - display.setCursor(0, 0); - display.println("Heart Rate Monitor"); - display.setCursor(0, 15); - display.println("Place finger on sensor"); - display.setCursor(0, 30); - display.println("and keep it steady..."); - display.display(); - - delay(3000); -} - -void loop() { - // Read sensor values - long irValue = particleSensor.getIR(); - long redValue = particleSensor.getRed(); - - // Detect heartbeat - if (checkForBeat(irValue) == true) { - long delta = millis() - lastBeat; - lastBeat = millis(); - - beatsPerMinute = 60 / (delta / 1000.0); - - if (beatsPerMinute < 255 && beatsPerMinute > 20) { - rates[rateSpot++] = (byte)beatsPerMinute; - rateSpot %= RATE_SIZE; - } - } - - // Calculate average BPM - float avgBPM = 0; - for (byte x = 0; x < RATE_SIZE; x++) - avgBPM += rates[x]; - avgBPM /= RATE_SIZE; - - // Calculate SpO2 - if (redValue > 0 && irValue > 0) { - float ratio = (float)redValue / irValue; - avgSpO2 = 110 - 25 * ratio; - - if (avgSpO2 < 80) avgSpO2 = 80; - if (avgSpO2 > 100) avgSpO2 = 100; - } - - // Update display and serial every UPDATE_INTERVAL - if (millis() - lastUpdate > UPDATE_INTERVAL) { - lastUpdate = millis(); - - // Update OLED Display - display.clearDisplay(); - display.setTextSize(1); - display.setTextColor(SSD1306_WHITE); - - // Title - display.setCursor(0, 0); - display.println("HEART RATE MONITOR"); - display.drawLine(0, 10, 128, 10, SSD1306_WHITE); - - // Check if finger is detected - if (irValue < 50000) { - display.setTextSize(2); - display.setCursor(15, 25); - display.println("NO FINGER"); - display.setTextSize(1); - display.setCursor(10, 50); - display.println("Place finger on sensor"); - } else { - // BPM Display - display.setTextSize(3); - display.setCursor(0, 18); - if (avgBPM > 0) { - display.print((int)avgBPM); - } else { - display.print("--"); - } - display.setTextSize(1); - display.setCursor(50, 25); - display.println("BPM"); - - // SpO2 Display - display.setTextSize(2); - display.setCursor(0, 45); - display.print((int)avgSpO2); - display.println("%"); - - display.setTextSize(1); - display.setCursor(50, 50); - display.println("SpO2"); - - // Signal indicator - display.setCursor(90, 50); - if (irValue > 150000) { - display.println("+++"); - } else if (irValue > 100000) { - display.println("++"); - } else { - display.println("+"); - } - } - - display.display(); - - // Serial output for debugging - Serial.print("BPM: "); - if (avgBPM > 0) { - Serial.print((int)avgBPM); - } else { - Serial.print("--"); - } - Serial.print(" | SpO2: "); - Serial.print((int)avgSpO2); - Serial.print("% | IR: "); - Serial.println(irValue); - } -} - -void scanBus() { - int foundCount = 0; - - for(byte i = 0; i < 128; i++) { - Wire.beginTransmission(i); - byte error = Wire.endTransmission(); - - if (error == 0) { - Serial.print(" Found device at: 0x"); - if (i < 16) Serial.print("0"); - Serial.println(i, HEX); - foundCount++; - } - } - - if(foundCount == 0) { - Serial.println(" No devices found!"); - } else { - Serial.print(" Total: "); - Serial.print(foundCount); - Serial.println(" device(s)\n"); - } -}