/* * SHIELD/ATLAS — ESP32 Acoustic Sensor Node * Captures audio from analog microphone, runs FFT, sends detections to cloud. * * Hardware: * - ELEGOO ESP32 (or any ESP32 dev board) * - Electret microphone module (e.g., MAX9814 or KY-037) on GPIO 34 (ADC1_CH6) * - Optional: status LED on GPIO 2 (built-in on most ESP32 boards) * * Libraries Required (install via Arduino Library Manager): * - WiFi (built-in) * - HTTPClient (built-in) * - ArduinoJson by Benoit Blanchon * - arduinoFFT by kosme * * Configuration: * 1. Set WIFI_SSID and WIFI_PASSWORD below * 2. Set SERVER_URL to your SHIELD/ATLAS instance * 3. Connect microphone OUT to GPIO 34 * 4. Upload and monitor via Serial (115200 baud) * * Copyright 2026 Integrated Security Solutions (ISS) — SDVOSB * FOR DISPLAY PURPOSES ONLY — NOTIONAL TEMPLATE */ #include #include #include #include #include // ============= CONFIGURATION — EDIT THESE ============= const char* WIFI_SSID = "YOUR_WIFI_SSID"; const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD"; const char* SERVER_URL = "https://shield-atlas-production.up.railway.app"; const char* SENSOR_ID = "ESP32-ACOUSTIC-001"; // ======================================================= #define MIC_PIN 34 #define LED_PIN 2 #define SAMPLE_RATE 8000 #define FFT_SIZE 1024 #define CLASSIFY_INTERVAL_MS 2000 #define HEARTBEAT_INTERVAL_MS 30000 double vReal[FFT_SIZE]; double vImag[FFT_SIZE]; ArduinoFFT FFT = ArduinoFFT(vReal, vImag, FFT_SIZE, SAMPLE_RATE); unsigned long lastClassify = 0; unsigned long lastHeartbeat = 0; unsigned long bootTime = 0; int detectionCount = 0; int matchCount = 0; String classifyEndpoint; String statusEndpoint; void setup() { Serial.begin(115200); pinMode(LED_PIN, OUTPUT); pinMode(MIC_PIN, INPUT); analogReadResolution(12); Serial.println(); Serial.println("============================================"); Serial.println(" SHIELD/ATLAS ESP32 Acoustic Sensor Node"); Serial.println(" Integrated Security Solutions — SDVOSB"); Serial.println("============================================"); Serial.println(); // Connect to WiFi Serial.printf("[WIFI] Connecting to %s", WIFI_SSID); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); int attempts = 0; while (WiFi.status() != WL_CONNECTED && attempts < 30) { delay(500); Serial.print("."); attempts++; digitalWrite(LED_PIN, !digitalRead(LED_PIN)); } if (WiFi.status() == WL_CONNECTED) { Serial.println(" CONNECTED"); Serial.printf("[WIFI] IP: %s\n", WiFi.localIP().toString().c_str()); Serial.printf("[WIFI] RSSI: %d dBm\n", WiFi.RSSI()); digitalWrite(LED_PIN, HIGH); } else { Serial.println(" FAILED"); Serial.println("[WIFI] Operating in offline mode — will retry periodically"); digitalWrite(LED_PIN, LOW); } // Build endpoint URLs String base = String(SERVER_URL); classifyEndpoint = base + "/api/acoustic/classify"; statusEndpoint = base + "/api/cram/status"; bootTime = millis(); // Test server connection if (WiFi.status() == WL_CONNECTED) { testServerConnection(); } Serial.println(); Serial.printf("[SENSOR] Microphone on GPIO %d\n", MIC_PIN); Serial.printf("[SENSOR] Sample rate: %d Hz\n", SAMPLE_RATE); Serial.printf("[SENSOR] FFT size: %d bins\n", FFT_SIZE); Serial.printf("[SENSOR] Classify interval: %d ms\n", CLASSIFY_INTERVAL_MS); Serial.printf("[SENSOR] Sensor ID: %s\n", SENSOR_ID); Serial.println("[SENSOR] Node ACTIVE — listening for acoustic events"); Serial.println(); } void testServerConnection() { HTTPClient http; http.begin(statusEndpoint); http.setTimeout(10000); int code = http.GET(); if (code == 200) { Serial.printf("[SERVER] Connected to SHIELD/ATLAS at %s\n", SERVER_URL); } else { Serial.printf("[SERVER] Server returned %d — check URL\n", code); } http.end(); } void captureSamples() { unsigned long sampleInterval = 1000000 / SAMPLE_RATE; // microseconds for (int i = 0; i < FFT_SIZE; i++) { unsigned long t = micros(); int raw = analogRead(MIC_PIN); vReal[i] = (double)(raw - 2048) / 2048.0; // Center around 0, normalize vImag[i] = 0.0; // Wait for next sample time while (micros() - t < sampleInterval) { // busy wait for timing accuracy } } } void analyzeAndClassify() { // Apply Hamming window FFT.windowing(FFT_WIN_TYP_HAMMING, FFT_FORWARD); // Compute FFT FFT.compute(FFT_FORWARD); FFT.complexToMagnitude(); // Find peak frequency double peakMagnitude = 0; int peakIndex = 0; double totalEnergy = 0; double weightedFreqSum = 0; for (int i = 1; i < FFT_SIZE / 2; i++) { double mag = vReal[i]; double freq = (double)i * SAMPLE_RATE / FFT_SIZE; totalEnergy += mag * mag; weightedFreqSum += freq * mag * mag; if (mag > peakMagnitude) { peakMagnitude = mag; peakIndex = i; } } double peakFreq = (double)peakIndex * SAMPLE_RATE / FFT_SIZE; double rmsDb = 20.0 * log10(max(peakMagnitude, 0.0001)); // Spectral centroid and spread double centroid = (totalEnergy > 0) ? weightedFreqSum / totalEnergy : 0; double spreadSum = 0; for (int i = 1; i < FFT_SIZE / 2; i++) { double freq = (double)i * SAMPLE_RATE / FFT_SIZE; double mag = vReal[i]; spreadSum += (freq - centroid) * (freq - centroid) * mag * mag; } double spread = (totalEnergy > 0) ? sqrt(spreadSum / totalEnergy) : 0; // Classify spectral shape const char* spectralShape; if (spread < 200) { spectralShape = "IMPULSE"; } else if (spread > 2000) { spectralShape = "BROADBAND"; } else if (peakFreq > 3000) { spectralShape = "SUPERSONIC_CRACK"; } else { spectralShape = "SUSTAINED_ROAR"; } detectionCount++; // Print local analysis Serial.printf("[DETECT #%d] peak=%.0fHz rms=%.1fdB shape=%s centroid=%.0f spread=%.0f\n", detectionCount, peakFreq, rmsDb, spectralShape, centroid, spread); // LED blink on detection digitalWrite(LED_PIN, LOW); delay(50); digitalWrite(LED_PIN, HIGH); // Send to server if (WiFi.status() == WL_CONNECTED) { sendClassification(peakFreq, rmsDb, spectralShape, centroid, spread); } else { Serial.println("[WIFI] Offline — detection logged locally only"); // Attempt reconnect WiFi.reconnect(); } } void sendClassification(double peakFreq, double rmsDb, const char* spectralShape, double centroid, double spread) { HTTPClient http; http.begin(classifyEndpoint); http.addHeader("Content-Type", "application/json"); http.setTimeout(5000); // Build JSON payload StaticJsonDocument<512> doc; doc["peakFrequencyHz"] = round(peakFreq * 10) / 10.0; doc["rmsDb"] = round(rmsDb * 10) / 10.0; doc["spectralShape"] = spectralShape; doc["spectralCentroid"] = round(centroid * 10) / 10.0; doc["spectralSpread"] = round(spread * 10) / 10.0; doc["sensorType"] = "ESP32_MICROPHONE"; doc["sensorId"] = SENSOR_ID; doc["sensorModel"] = "ELEGOO_ESP32"; doc["uptimeMs"] = millis() - bootTime; String payload; serializeJson(doc, payload); int httpCode = http.POST(payload); if (httpCode == 200) { String response = http.getString(); StaticJsonDocument<1024> respDoc; deserializeJson(respDoc, response); if (respDoc["matched"].as()) { matchCount++; const char* threatName = respDoc["topMatch"]["name"] | "Unknown"; float confidence = respDoc["topMatch"]["confidence"] | 0.0; Serial.printf("[MATCH] >>> %s (confidence: %.0f%%) <<<\n", threatName, confidence * 100); // Rapid LED blink on match for (int i = 0; i < 5; i++) { digitalWrite(LED_PIN, LOW); delay(100); digitalWrite(LED_PIN, HIGH); delay(100); } } } else { Serial.printf("[SERVER] POST failed: %d\n", httpCode); } http.end(); } void sendHeartbeat() { if (WiFi.status() != WL_CONNECTED) return; Serial.printf("[HEARTBEAT] uptime=%lus, detections=%d, matches=%d, RSSI=%d\n", (millis() - bootTime) / 1000, detectionCount, matchCount, WiFi.RSSI()); } void loop() { unsigned long now = millis(); // Capture and classify on interval if (now - lastClassify >= CLASSIFY_INTERVAL_MS) { lastClassify = now; captureSamples(); analyzeAndClassify(); } // Periodic heartbeat if (now - lastHeartbeat >= HEARTBEAT_INTERVAL_MS) { lastHeartbeat = now; sendHeartbeat(); } }