diff --git a/iot-data-harvester/esp32_code/platformio.ini b/iot-data-harvester/esp32_code/platformio.ini index 8deea0c..2c6fe55 100644 --- a/iot-data-harvester/esp32_code/platformio.ini +++ b/iot-data-harvester/esp32_code/platformio.ini @@ -29,4 +29,5 @@ extra_scripts = pre:extra_script.py lib_deps = adafruit/Adafruit ADXL345@^1.3.4 adafruit/Adafruit Unified Sensor@^1.1.14 - bblanchon/ArduinoJson@^7.0.3 \ No newline at end of file + bblanchon/ArduinoJson@^7.0.3 + tzapu/WiFiManager \ No newline at end of file diff --git a/iot-data-harvester/esp32_code/src/main.cpp b/iot-data-harvester/esp32_code/src/main.cpp index fa6fef8..5dd4d5a 100644 --- a/iot-data-harvester/esp32_code/src/main.cpp +++ b/iot-data-harvester/esp32_code/src/main.cpp @@ -1,18 +1,14 @@ /** - * Project: QuakeGuard - Electro-Domestic Earthquake Alarm System - * Version: 3.0.0-MASTER + * Project: QuakeGuard - Professional Seismic Node + * Version: 3.1.0-PRO * Target Hardware: ESP32-C3 SuperMini + ADXL345 * Author: GiZano * - * Description: - * Production-ready firmware for seismic event detection. - * Integrates real-time signal processing (STA/LTA) with cryptographic payload signing. - * - * Key Technical Features: - * - Hardware: I2C bus forced on GPIO 7 (SDA) and 8 (SCL) with dynamic object allocation. - * - DSP: High-Pass Filter + Noise Gate + Signal Dropout Protection. - * - Security: NIST256p ECDSA signature generation for data integrity. - * - Connectivity: JSON over HTTP POST with NTP synchronization. + * CHANGELOG v3.1.0: + * - CRITICAL: I2C Clock increased to 100kHz (Standard Mode) for timing compliance. + * - FEATURE: Integrated WiFiManager (No more hardcoded credentials). + * - STABILITY: Added 2000ms timeout to HTTP Client to prevent task blocking. + * - SYSTEM: Config Portal timeout set to 180s to allow offline sensor operation. */ #include @@ -20,11 +16,12 @@ #include #include #include +#include // Requires "tzapu/WiFiManager" library #include #include #include -// Cryptographic Libraries (MbedTLS) +// Cryptographic Libraries #include "mbedtls/entropy.h" #include "mbedtls/ctr_drbg.h" #include "mbedtls/ecdsa.h" @@ -32,28 +29,27 @@ #include "mbedtls/error.h" // -------------------------------------------------------------------------- -// HARDWARE PIN DEFINITIONS (ESP32-C3 SuperMini) +// HARDWARE PIN DEFINITIONS // -------------------------------------------------------------------------- -// Verified pinout for this specific hardware revision. #define I2C_SDA_PIN 7 #define I2C_SCL_PIN 8 +// I2C Frequency: 100kHz (Standard Mode) +// 10kHz was insufficient for 100Hz sampling rate loop times. +#define I2C_CLOCK_SPEED 100000 + // Global Sensor Pointer -// Initialized to NULL. Instantiated dynamically in setup() to prevent -// static initialization race conditions with the I2C bus. +// Dynamic allocation is strictly required here to delay constructor execution +// until AFTER Wire.setPins(7,8) is called. Adafruit_ADXL345_Unified *accel = NULL; // -------------------------------------------------------------------------- -// NETWORK & SERVER CONFIGURATION +// SERVER CONFIGURATION // -------------------------------------------------------------------------- -#ifndef WIFI_SSID - #define WIFI_SSID "YOUR_WIFI_SSID" // <--- UPDATE THIS -#endif -#ifndef WIFI_PASS - #define WIFI_PASS "YOUR_WIFI_PASS" // <--- UPDATE THIS -#endif +// WiFi Credentials are now handled by WiFiManager and stored in NVS. + #ifndef SERVER_HOST - #define SERVER_HOST "192.168.1.50" // <--- UPDATE YOUR SERVER IP + #define SERVER_HOST "192.168.1.50" #endif #ifndef SERVER_PORT #define SERVER_PORT 8000 @@ -65,44 +61,35 @@ Adafruit_ADXL345_Unified *accel = NULL; #define SENSOR_ID 101 #endif -// Constant mapping for type safety -const char* WIFI_SSID_CONF = WIFI_SSID; -const char* WIFI_PASS_CONF = WIFI_PASS; const char* SERVER_HOST_CONF = SERVER_HOST; const int SERVER_PORT_CONF = SERVER_PORT; const char* SERVER_PATH_CONF = SERVER_PATH; const int SENSOR_ID_CONF = SENSOR_ID; // -------------------------------------------------------------------------- -// RTOS HANDLES & DATA STRUCTURES +// RTOS HANDLES & STRUCTURES // -------------------------------------------------------------------------- QueueHandle_t eventQueue; struct SeismicEvent { - float magnitude; // Computed STA/LTA Ratio - unsigned long event_millis; // System timestamp at trigger time + float magnitude; + unsigned long event_millis; }; -// -------------------------------------------------------------------------- -// DSP ALGORITHM PARAMETERS -// -------------------------------------------------------------------------- -const float ALPHA_LTA = 0.05f; // Long Term Average smoothing factor -const float ALPHA_STA = 0.40f; // Short Term Average smoothing factor -const float TRIGGER_RATIO = 1.8f; // Threshold ratio for alarm triggering -const float NOISE_FLOOR = 0.04f; // Minimum G-force to consider valid signal (Noise Gate) +// DSP PARAMETERS +const float ALPHA_LTA = 0.05f; +const float ALPHA_STA = 0.40f; +const float TRIGGER_RATIO = 1.8f; +const float NOISE_FLOOR = 0.04f; // -------------------------------------------------------------------------- -// CRYPTOGRAPHY SUBSYSTEM +// CRYPTO SUBSYSTEM // -------------------------------------------------------------------------- Preferences preferences; mbedtls_entropy_context entropy; mbedtls_ctr_drbg_context ctr_drbg; mbedtls_pk_context pk_context; -/** - * @brief Initializes MbedTLS context and manages Device Identity. - * Generates a new ECDSA (SECP256R1) Key Pair if not present in Non-Volatile Storage. - */ void initCrypto() { mbedtls_entropy_init(&entropy); mbedtls_ctr_drbg_init(&ctr_drbg); @@ -115,26 +102,21 @@ void initCrypto() { if (!preferences.isKey("priv_key")) { Serial.println("[SEC] Generating New ECDSA Key Pair..."); - mbedtls_pk_setup(&pk_context, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY)); mbedtls_ecp_gen_key(MBEDTLS_ECP_DP_SECP256R1, mbedtls_pk_ec(pk_context), mbedtls_ctr_drbg_random, &ctr_drbg); - unsigned char priv_buf[128]; int ret = mbedtls_pk_write_key_der(&pk_context, priv_buf, sizeof(priv_buf)); - int key_len = ret; - - preferences.putBytes("priv_key", priv_buf + sizeof(priv_buf) - key_len, key_len); - Serial.println("[SEC] Keys Generated and Saved to NVS."); + preferences.putBytes("priv_key", priv_buf + sizeof(priv_buf) - ret, ret); + Serial.println("[SEC] Keys Generated."); } else { - Serial.println("[SEC] Loading Existing Keys from NVS..."); + Serial.println("[SEC] Loading Existing Keys..."); size_t len = preferences.getBytesLength("priv_key"); uint8_t buf[len]; preferences.getBytes("priv_key", buf, len); - mbedtls_pk_parse_key(&pk_context, buf, len, NULL, 0); } - // Export Public Key for Server Provisioning + // Export Public Key unsigned char pub_buf[128]; int ret_pub = mbedtls_pk_write_pubkey_der(&pk_context, pub_buf, sizeof(pub_buf)); int pub_len = ret_pub; @@ -146,17 +128,10 @@ void initCrypto() { Serial.println(); } -/** - * @brief Signs a payload string using the device's Private Key. - * @param message Input string to sign. - * @return Hexadecimal string of the ECDSA signature. - */ String signMessage(String message) { unsigned char hash[32]; unsigned char sig[MBEDTLS_ECDSA_MAX_LEN]; size_t sig_len = 0; - - // SHA-256 Hashing mbedtls_md_context_t ctx; mbedtls_md_init(&ctx); mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 0); @@ -164,169 +139,120 @@ String signMessage(String message) { mbedtls_md_update(&ctx, (const unsigned char*)message.c_str(), message.length()); mbedtls_md_finish(&ctx, hash); mbedtls_md_free(&ctx); - - // ECDSA Signing mbedtls_pk_sign(&pk_context, MBEDTLS_MD_SHA256, hash, 0, sig, &sig_len, mbedtls_ctr_drbg_random, &ctr_drbg); - - // Hex Encoding String hexSig = ""; - for(size_t i = 0; i < sig_len; i++) { - char buf[3]; - sprintf(buf, "%02x", sig[i]); - hexSig += buf; - } + for(size_t i = 0; i < sig_len; i++) { char buf[3]; sprintf(buf, "%02x", sig[i]); hexSig += buf; } return hexSig; } // -------------------------------------------------------------------------- -// TASK: SENSOR ACQUISITION & PROCESSING +// TASK: SENSOR (OPTIMIZED) // -------------------------------------------------------------------------- void sensorTask(void *pvParameters) { - float lta = 0.0f; - float sta = 0.0f; - float prev_raw_mag = 9.81f; // Assumes 1G start - float filtered_mag = 0.0f; + float lta = 0.0f, sta = 0.0f, prev_raw_mag = 9.81f, filtered_mag = 0.0f; sensors_event_t event; - // Block until the sensor object is allocated in setup() - while (accel == NULL) { - vTaskDelay(pdMS_TO_TICKS(100)); - } + while (accel == NULL) vTaskDelay(pdMS_TO_TICKS(100)); - Serial.println("[SENSOR] Task Active. Beginning Stabilization Phase..."); - - // Initial Stabilization Loop (Populate Filters) + Serial.println("[SENSOR] Task Active. Stabilizing..."); for(int i=0; i<20; i++) { if(accel->getEvent(&event)) { float mag = sqrt(pow(event.acceleration.x, 2) + pow(event.acceleration.y, 2) + pow(event.acceleration.z, 2)); - lta = mag; - sta = mag; - prev_raw_mag = mag; + lta = mag; sta = mag; prev_raw_mag = mag; } vTaskDelay(pdMS_TO_TICKS(50)); } - Serial.println("[SENSOR] Ready for detection."); + Serial.println("[SENSOR] Ready."); TickType_t xLastWakeTime = xTaskGetTickCount(); - const TickType_t xFrequency = pdMS_TO_TICKS(10); // 100Hz Sampling Rate + const TickType_t xFrequency = pdMS_TO_TICKS(10); bool inAlarm = false; unsigned long alarmStart = 0; for(;;) { - // Enforce strict timing vTaskDelayUntil(&xLastWakeTime, xFrequency); - // Retrieve Data (Access via pointer) - if (!accel->getEvent(&event)) { - continue; - } + if (!accel->getEvent(&event)) continue; float raw_mag = sqrt(pow(event.acceleration.x, 2) + pow(event.acceleration.y, 2) + pow(event.acceleration.z, 2)); - // --- SIGNAL DROPOUT PROTECTION --- - // If magnitude drops below 2.0 m/s^2 (~0.2G), it indicates a wiring failure or I2C bus error. - // We discard this frame to prevent the High Pass Filter from creating a false spike. - if (raw_mag < 2.0f) { - continue; - } + // Dropout Protection (< 0.2G) + if (raw_mag < 2.0f) continue; - // Digital High Pass Filter (Removes Gravity component) filtered_mag = 0.9f * (filtered_mag + raw_mag - prev_raw_mag); prev_raw_mag = raw_mag; float abs_signal = abs(filtered_mag); - // --- NOISE GATE --- - // Zero out signals below the hardware noise floor to prevent STA/LTA drift. - if (abs_signal < NOISE_FLOOR) { - abs_signal = 0.0f; - } + // Noise Gate + if (abs_signal < NOISE_FLOOR) abs_signal = 0.0f; - // STA/LTA Algorithm Update + // STA/LTA lta = (ALPHA_LTA * abs_signal) + ((1.0f - ALPHA_LTA) * lta); sta = (ALPHA_STA * abs_signal) + ((1.0f - ALPHA_STA) * sta); - // Safety floor for LTA to avoid division by zero or extreme ratios if (lta < 0.05f) lta = 0.05f; - float ratio = sta / lta; - // TRIGGER LOGIC - // Condition 1: Ratio exceeds threshold. - // Condition 2: Actual signal intensity exceeds noise floor (Real event verification). if (ratio >= TRIGGER_RATIO && sta > NOISE_FLOOR && !inAlarm) { - Serial.printf("[SENSOR] EARTHQUAKE DETECTED! Ratio: %.2f (Mag: %.3f G)\n", ratio, sta); - - SeismicEvent evt; - evt.magnitude = ratio; - evt.event_millis = millis(); + Serial.printf("[SENSOR] EARTHQUAKE! Ratio: %.2f (Mag: %.3f G)\n", ratio, sta); + SeismicEvent evt; evt.magnitude = ratio; evt.event_millis = millis(); xQueueSend(eventQueue, &evt, 0); - - inAlarm = true; - alarmStart = millis(); - } - - // Alarm Cooldown (5 Seconds) - if (inAlarm && (millis() - alarmStart > 5000)) { - inAlarm = false; + inAlarm = true; alarmStart = millis(); } + if (inAlarm && (millis() - alarmStart > 5000)) inAlarm = false; } } // -------------------------------------------------------------------------- -// TASK: NETWORK DISPATCHER +// TASK: NETWORK (WITH WATCHDOG & TIMEOUT) // -------------------------------------------------------------------------- void networkTask(void *pvParameters) { WiFiClient client; - Serial.printf("[NET] Connecting to Access Point: %s\n", WIFI_SSID_CONF); - WiFi.begin(WIFI_SSID_CONF, WIFI_PASS_CONF); - + // Explicit Timeout to prevent hanging on unresponsive servers + client.setTimeout(2000); + + // Wait for WiFiManager to handle initial connection in setup + // or connecting via saved credentials while (WiFi.status() != WL_CONNECTED) { - vTaskDelay(pdMS_TO_TICKS(500)); - Serial.print("."); + vTaskDelay(pdMS_TO_TICKS(1000)); } - Serial.println("\n[NET] WiFi Connected."); - - // NTP Time Synchronization (Critical for signature validity) + configTime(0, 0, "pool.ntp.org", "time.nist.gov"); SeismicEvent receivedEvt; for(;;) { - // Block until event is received from Sensor Task if (xQueueReceive(eventQueue, &receivedEvt, portMAX_DELAY) == pdTRUE) { // Connection Watchdog if (WiFi.status() != WL_CONNECTED) { - WiFi.disconnect(); - WiFi.reconnect(); - vTaskDelay(pdMS_TO_TICKS(1000)); - continue; + Serial.println("[NET] WiFi Lost. Attempting Reconnect..."); + WiFi.reconnect(); + // Give it some time to reconnect + int retry = 0; + while(WiFi.status() != WL_CONNECTED && retry < 5) { + vTaskDelay(pdMS_TO_TICKS(1000)); + retry++; + } + if(WiFi.status() != WL_CONNECTED) continue; // Skip this event if no net } - // Timestamp Reconstruction - time_t now_unix; - time(&now_unix); + time_t now_unix; time(&now_unix); unsigned long age_ms = millis() - receivedEvt.event_millis; time_t evt_time = now_unix - (age_ms / 1000); - // Payload Construction int val = (int)(receivedEvt.magnitude * 100); String payload = String(val) + ":" + String(evt_time); - - // Cryptographic Signing String sig = signMessage(payload); - // JSON Serialization JsonDocument doc; doc["value"] = val; doc["misurator_id"] = SENSOR_ID_CONF; doc["device_timestamp"] = evt_time; doc["signature_hex"] = sig; - String json; - serializeJson(doc, json); + String json; serializeJson(doc, json); - // HTTP POST Transmission - Serial.println("[NET] Transmitting Event to Server..."); + Serial.println("[NET] Sending..."); if (client.connect(SERVER_HOST_CONF, SERVER_PORT_CONF)) { client.println(String("POST ") + SERVER_PATH_CONF + " HTTP/1.1"); client.println(String("Host: ") + SERVER_HOST_CONF); @@ -336,12 +262,12 @@ void networkTask(void *pvParameters) { client.println(); client.println(json); - // Flush Response Buffer + // Read response with timeout protection while(client.connected() || client.available()) { if(client.available()) client.readStringUntil('\n'); } client.stop(); - Serial.println("[NET] Transmission Successful."); + Serial.println("[NET] Sent OK."); } else { Serial.println("[NET] Connection Failed."); } @@ -350,37 +276,39 @@ void networkTask(void *pvParameters) { } // -------------------------------------------------------------------------- -// SYSTEM INITIALIZATION +// SETUP (WIFIMANAGER + HARDWARE INIT) // -------------------------------------------------------------------------- void setup() { Serial.begin(115200); - // Wait for the Serial Monitor to initialize while(!Serial) delay(10); delay(2000); - Serial.println("\n\n=================================================="); - Serial.println("[BOOT] QuakeGuard Security System First..."); - Serial.println("=================================================="); - - // 1. INITIALIZE CRYPTO SUBSYSTEM & DISPLAY KEY (PRIORITY OVER SENSOR) + Serial.println("\n\n[BOOT] QuakeGuard v3.1 PRO"); + + // 1. CRYPTO INIT initCrypto(); - - Serial.println("\n⚠️ WARNING: You have 10 seconds to copy the Public Key above!"); - Serial.println(" Register it in the server database to prevent 403 Forbidden errors."); - Serial.println(" Sensor initialization will commence shortly..."); - // Visual Countdown - for(int i=10; i>0; i--) { - Serial.printf(" %d...", i); - delay(1000); + Serial.println("\n--- 10 SECONDS TO COPY PUBLIC KEY ---"); + for(int i=10; i>0; i--) { Serial.printf(" %d...", i); delay(1000); } + Serial.println("\n"); + + // 2. WIFIMANAGER (BLOCKING PORTAL) + // Create Config Portal "QuakeGuard-Setup". + // If no saved creds, user must connect to this AP and configure WiFi. + WiFiManager wm; + wm.setConfigPortalTimeout(180); // 3 minutes timeout, then boot anyway (offline mode) + + Serial.println("[NET] Initializing WiFiManager..."); + if (!wm.autoConnect("QuakeGuard-Setup")) { + Serial.println("[NET] Failed to connect or timeout. Booting in Offline Mode."); + } else { + Serial.println("[NET] WiFi Connected via WiFiManager."); } - Serial.println("\n\n[BOOT] Starting Hardware Initialization..."); - // 2. HARDWARE INIT (I2C PINS 7 & 8) - Serial.printf("[HARDWARE] Configuring I2C Bus on SDA=%d, SCL=%d\n", I2C_SDA_PIN, I2C_SCL_PIN); + // 3. HARDWARE INIT + Serial.printf("[HARDWARE] I2C Init: SDA=%d, SCL=%d @ %dHz\n", I2C_SDA_PIN, I2C_SCL_PIN, I2C_CLOCK_SPEED); - // Bus Recovery Sequence: Manually toggles pins to unlatch the sensor - // if it is stuck in a "zombie" state holding the line low. + // Bus Recovery pinMode(I2C_SDA_PIN, INPUT_PULLUP); pinMode(I2C_SCL_PIN, INPUT_PULLUP); digitalWrite(I2C_SDA_PIN, HIGH); @@ -390,21 +318,17 @@ void setup() { Wire.end(); Wire.setPins(I2C_SDA_PIN, I2C_SCL_PIN); Wire.begin(); - Wire.setClock(10000); // 10kHz stability clock + Wire.setClock(I2C_CLOCK_SPEED); // 100kHz Standard Mode delay(100); - // 3. DYNAMIC MEMORY ALLOCATION - Serial.println("[HARDWARE] Allocating Sensor Object..."); + Serial.println("[HARDWARE] Allocating Sensor..."); if (accel != NULL) delete accel; accel = new Adafruit_ADXL345_Unified(12345); - // 4. SENSOR STARTUP if(!accel->begin(0x53)) { - Serial.println("[WARN] Not found at 0x53. Trying 0x1D..."); + Serial.println("[WARN] Try 0x1D..."); if(!accel->begin(0x1D)) { - Serial.println("[FATAL] Sensor Check Failed. (Did you copy the key?)"); - // We do not block execution in an infinite loop here, otherwise the Key - // would scroll off-screen. The sensorTask will handle hardware absence gracefully. + Serial.println("[FATAL] Sensor Hardware Error."); } } else { accel->setDataRate(ADXL345_DATARATE_100_HZ); @@ -412,7 +336,7 @@ void setup() { Serial.println("[SYS] Sensor OK."); } - // 5. TASK CREATION + // 4. RTOS eventQueue = xQueueCreate(20, sizeof(SeismicEvent)); xTaskCreate(sensorTask, "SensorTask", 4096, NULL, 5, NULL); xTaskCreate(networkTask, "NetworkTask", 8192, NULL, 1, NULL); @@ -421,6 +345,5 @@ void setup() { } void loop() { - // Main loop delegates to FreeRTOS tasks vTaskDelay(pdMS_TO_TICKS(1000)); } \ No newline at end of file