สถาปัตยกรรม Gunshot Detection แบบ Open Design เพื่อสนับสนุนความโปร่งใสในพื้นที่ชายแดน · 4-Microphone TDOA · LED Direction Indicator · GPS Timestamp
| อุปกรณ์ | Signal | GPIO | หน้าที่ |
|---|---|---|---|
| LED N | Anode | GPIO 4 | ติดเมื่อเสียงวิ่งไปทิศเหนือ |
| LED S | Anode | GPIO 5 | ติดเมื่อเสียงวิ่งไปทิศใต้ |
| LED E | Anode | GPIO 6 | ติดเมื่อเสียงวิ่งไปทิศตะวันออก |
| LED W | Anode | GPIO 7 | ติดเมื่อเสียงวิ่งไปทิศตะวันตก |
| LED CTR | Anode | GPIO 11 | Heartbeat / Alert Lock |
| INMP441 ×4 | I2S DATA | GPIO 8 | Shared Data Line |
| INMP441 ×4 | I2S CLK | GPIO 9 | Bit Clock (BCK) |
| MIC 1 (N) | WS/LRCK | GPIO 15 | Left-Right Clock ช่อง N |
| MIC 2 (S) | WS/LRCK | GPIO 16 | Left-Right Clock ช่อง S |
| MIC 3 (E) | WS/LRCK | GPIO 17 | Left-Right Clock ช่อง E |
| MIC 4 (W) | WS/LRCK | GPIO 18 | Left-Right Clock ช่อง W |
| GPS Neo-M8N | TX→RX | GPIO 43 | UART2 receive from GPS |
| GPS Neo-M8N | RX←TX | GPIO 44 | UART2 send to GPS |
| รายการ | ฿/ชิ้น | จำนวน | รวม ฿ |
|---|---|---|---|
| ESP32-S3 Dev Board | 220 | 220 | |
| INMP441 I2S Microphone | 45 | 180 | |
| LED 5mm + R220Ω (set 5) | 5 | 25 | |
| GPS Neo-M8N Module | 240 | 240 | |
| กล่อง IP66 (ABS) | 80 | 80 | |
| แผง Solar 6V 2W + TP4056 | 120 | 120 | |
| แบต 18650 LiFePO4 3.2V | 45 | 45 | |
| สายไฟ + PCB Veroboard | 15 | 15 | |
| ต้นทุนรวมต่อสถานี | 925 | ||
คำนวณความละเอียดในการระบุทิศทาง (Angular Resolution) จากระยะห่างไมค์และ Sample Rate
สำหรับ TDOA ที่ต้องการ Multi-channel I2S DMA พร้อมกัน 4 ช่อง ESP32-S3 คือตัวเลือกที่ดีที่สุด ราคาไม่แพงกว่า S2 มาก แต่ได้ Dual-Core + AI Accelerator เพิ่มมา
| รุ่น | Core / MHz | I2S Ports | AI Accel | Wi-Fi/BLE | ราคา Dev Board | เหมาะสม? |
|---|---|---|---|---|---|---|
| ESP32 Classic ESP32-D0WDQ6 |
Dual · 240 | 2 ports | — | Wi-Fi + BLE | ~฿80-120 | พอใช้ได้ I2S 2 ports ต้อง multiplex ไมค์ |
| ESP32-S2 | Single · 240 | 1 port | — | Wi-Fi only | ~฿100-150 | ไม่แนะนำ Single core + I2S น้อย |
| ESP32-S3 ⭐ แนะนำสำหรับ Project นี้ |
Dual · 240 | 2 ports (รองรับ 4 MIC ด้วย L/R chaining) |
Vector DSP | Wi-Fi + BLE | ~฿180-250 | ดีที่สุด Dual-core + DSP + RAM เยอะ |
| ESP32-C3 | Single · 160 RISC-V |
1 port | — | Wi-Fi + BLE | ~฿60-90 | ไม่พอ Single core + I2S 1 port |
| ESP32-H2 | Single · 96 RISC-V |
1 port | — | BLE + 802.15.4 | ~฿80-120 | ไม่เหมาะ ไม่มี Wi-Fi, CPU ช้า |
จำลองทิศทางเสียงและดูว่า LED ไหนจะติด พร้อม TDOA delay ที่คำนวณได้จริง
| ทิศทาง: | 000° (N) |
| ระยะทาง: | 200 m |
| TDOA N-S: | 0.00 μs |
| TDOA E-W: | 0.00 μs |
| LED Active: | CTR |
โค้ด C++ สมบูรณ์ รัน TDOA แบบ GCC-PHAT บน Dual Core · Core 0 รับ I2S / Core 1 คำนวณ Cross-Correlation + ควบคุม LED
/* * Gunshot Detection & Direction Vector Indicator System v2.0 * Target: ESP32-S3 · 4x INMP441 I2S Microphone Array · 5x LED Direction Indicators * Algorithm: GCC-PHAT Time Difference of Arrival (TDOA) * Published: e25vbe.radio · MIT License · Open Design for Border Monitoring */ #include <Arduino.h> #include <driver/i2s.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "tdoa.h" // ── GPIO Pin Definitions ────────────────────────────────────── #define LED_N 4 // North LED → GPIO 4 #define LED_S 5 // South LED → GPIO 5 #define LED_E 6 // East LED → GPIO 6 #define LED_W 7 // West LED → GPIO 7 #define LED_CTR 11 // Status LED → GPIO 11 #define I2S_BCK 9 // Bit Clock (shared all mics) #define I2S_DATA 8 // Data line (shared all mics) #define I2S_WS_MIC1 15 // Word Select · MIC 1 North #define I2S_WS_MIC2 16 // Word Select · MIC 2 South #define I2S_WS_MIC3 17 // Word Select · MIC 3 East #define I2S_WS_MIC4 18 // Word Select · MIC 4 West // ── Signal Detection Parameters ────────────────────────────── #define SAMPLE_RATE 44100 // Hz #define BUFFER_SIZE 512 // samples per DMA block #define AMPLITUDE_THRESH 180000 // ADC counts - calibrate on site #define INDICATOR_HOLD_MS 3000 // LED lock time after detection (ms) #define MIC_SPACING_M 1.0f // meters between opposing microphones #define SPEED_SOUND_MS 0.344f // m per millisecond @ 20°C // ── Shared buffers between cores ───────────────────────────── volatile int32_t buf_n[BUFFER_SIZE]; volatile int32_t buf_s[BUFFER_SIZE]; volatile int32_t buf_e[BUFFER_SIZE]; volatile int32_t buf_w[BUFFER_SIZE]; volatile bool data_ready = false; volatile bool event_detected = false; volatile float last_bearing_deg = 0; // ── I2S driver setup ───────────────────────────────────────── void i2s_install(i2s_port_t port, int ws_pin) { i2s_config_t cfg = { .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX), .sample_rate = SAMPLE_RATE, .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT, .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, .communication_format = I2S_COMM_FORMAT_STAND_I2S, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 4, .dma_buf_len = BUFFER_SIZE, .use_apll = true }; i2s_pin_config_t pins = { .bck_io_num = I2S_BCK, .ws_io_num = ws_pin, .data_out_num = I2S_PIN_NO_CHANGE, .data_in_num = I2S_DATA }; i2s_driver_install(port, &cfg, 0, NULL); i2s_set_pin(port, &pins); } // ── LED helpers ─────────────────────────────────────────────── void led_setup() { for (int p : {LED_N, LED_S, LED_E, LED_W, LED_CTR}) pinMode(p, OUTPUT); // Boot flash sequence for (int p : {LED_N, LED_E, LED_S, LED_W, LED_CTR}) { digitalWrite(p, HIGH); delay(80); digitalWrite(p, LOW); } } void led_clear() { digitalWrite(LED_N, LOW); digitalWrite(LED_S, LOW); digitalWrite(LED_E, LOW); digitalWrite(LED_W, LOW); } void led_direction(float bearing) { led_clear(); // Light the LED(s) corresponding to the arrival direction quadrant if (bearing >= 315 || bearing < 45) digitalWrite(LED_N, HIGH); if (bearing >= 45 && bearing < 135) digitalWrite(LED_E, HIGH); if (bearing >= 135 && bearing < 225) digitalWrite(LED_S, HIGH); if (bearing >= 225 && bearing < 315) digitalWrite(LED_W, HIGH); } // ── Core 0 Task: I2S Capture ────────────────────────────────── void taskCapture(void* param) { size_t bytes; while (true) { i2s_read(I2S_NUM_0, (void*)buf_n, BUFFER_SIZE*4, &bytes, portMAX_DELAY); i2s_read(I2S_NUM_1, (void*)buf_s, BUFFER_SIZE*4, &bytes, portMAX_DELAY); // MIC3/4 share I2S_NUM_0 via L/R channel split in production data_ready = true; vTaskDelay(1 / portTICK_PERIOD_MS); } } // ── Core 1 Task: TDOA + LED ─────────────────────────────────── void taskProcess(void* param) { while (true) { if (!data_ready) { vTaskDelay(2 / portTICK_PERIOD_MS); continue; } data_ready = false; // Peak amplitude check (fast path) int32_t peak = peak_amplitude(buf_n, BUFFER_SIZE); if (peak < AMPLITUDE_THRESH) { // Heartbeat blink digitalWrite(LED_CTR, HIGH); vTaskDelay(20/portTICK_PERIOD_MS); digitalWrite(LED_CTR, LOW); continue; } // ── Gunshot Detected! ────────────────────────────────── digitalWrite(LED_CTR, HIGH); Serial.printf("[ALERT] Peak=%d · running TDOA\n", peak); float dt_ns = gcc_phat_delay(buf_n, buf_s, BUFFER_SIZE, SAMPLE_RATE); float dt_ew = gcc_phat_delay(buf_e, buf_w, BUFFER_SIZE, SAMPLE_RATE); // Convert delay to bearing angle float sin_el = (dt_ns * SPEED_SOUND_MS * 1000) / MIC_SPACING_M; float sin_az = (dt_ew * SPEED_SOUND_MS * 1000) / MIC_SPACING_M; sin_el = constrain(sin_el, -1.0f, 1.0f); sin_az = constrain(sin_az, -1.0f, 1.0f); float bearing = atan2f(sin_az, sin_el) * 180.0f / PI; if (bearing < 0) bearing += 360; last_bearing_deg = bearing; Serial.printf("[DIRECTION] Bearing=%.1f° dt_NS=%.2fms dt_EW=%.2fms\n", bearing, dt_ns*1000, dt_ew*1000); led_direction(bearing); vTaskDelay(INDICATOR_HOLD_MS / portTICK_PERIOD_MS); led_clear(); digitalWrite(LED_CTR, LOW); } } // ── Setup ───────────────────────────────────────────────────── void setup() { Serial.begin(115200); led_setup(); i2s_install(I2S_NUM_0, I2S_WS_MIC1); i2s_install(I2S_NUM_1, I2S_WS_MIC2); Serial.println("[INIT] Gunshot Detection Array v2.0 standing by..."); xTaskCreatePinnedToCore(taskCapture, "I2S_CAP", 4096, NULL, 2, NULL, 0); xTaskCreatePinnedToCore(taskProcess, "TDOA_PROC", 8192, NULL, 1, NULL, 1); } void loop() { vTaskDelay(100 / portTICK_PERIOD_MS); // Unused; logic runs on FreeRTOS tasks }
/* * tdoa.h — GCC-PHAT Time Difference of Arrival helper * Generalized Cross-Correlation with Phase Transform * Returns delay in seconds between two audio buffers */ #pragma once #include <math.h> #include <stdint.h> // Simple peak amplitude scanner int32_t peak_amplitude(volatile int32_t* buf, int N) { int32_t peak = 0; for (int i = 0; i < N; i++) { int32_t v = abs(buf[i]); if (v > peak) peak = v; } return peak; } /* * gcc_phat_delay() — Estimate time delay between two signals * Simplified integer cross-correlation (no FFT, sufficient for N=512) * Returns delay in SECONDS (positive = signal1 arrived first) * * For production: replace with ESP-DSP FFT-based GCC-PHAT * Library: https://github.com/espressif/esp-dsp */ float gcc_phat_delay(volatile int32_t* sig1, volatile int32_t* sig2, int N, int fs) { int max_lag = N / 4; // search window long best_corr = LONG_MIN; int best_lag = 0; for (int lag = -max_lag; lag <= max_lag; lag++) { long corr = 0; for (int i = 0; i < N; i++) { int j = i + lag; if (j >= 0 && j < N) corr += ((long)sig1[i] >> 14) * ((long)sig2[j] >> 14); } if (corr > best_corr) { best_corr = corr; best_lag = lag; } } return (float)best_lag / (float)fs; // seconds }
GUNSHOT DETECTION ARRAY · WIRING NOTES
e25vbe.radio · Open Design · MIT License
════════════════════════════════════════
INMP441 I2S MICROPHONE WIRING (×4)
───────────────────────────────────
All 4 mics share the same BCK and DATA lines.
Each mic gets its own WS (Word Select / LRCK) pin.
INMP441 pin → ESP32-S3 GPIO
──────────────────────────────
VDD (3.3V) → 3.3V
GND → GND
SD (Data Out) → GPIO 8 (shared)
SCK (Bit Clk) → GPIO 9 (shared)
WS MIC1 (N) → GPIO 15
WS MIC2 (S) → GPIO 16
WS MIC3 (E) → GPIO 17
WS MIC4 (W) → GPIO 18
L/R SEL → GND = Left channel
PHYSICAL ARRAY PLACEMENT
─────────────────────────
Mount microphones at corners of a 1m × 1m cross:
MIC1 (N)
│ 1 m
MIC4(W) ─── ESP32 ─── MIC3(E)
│ 1 m
MIC2 (S)
→ Align MIC1 to TRUE NORTH using compass before deployment
→ Mount 2m+ above ground to reduce ground reflection noise
→ Weatherproof mic capsule with foam windscreen (50 PPI)
LED INDICATORS (5× LED + 220Ω resistor each)
──────────────────────────────────────────────
LED N → GPIO 4 → 220Ω → GND
LED S → GPIO 5 → 220Ω → GND
LED E → GPIO 6 → 220Ω → GND
LED W → GPIO 7 → 220Ω → GND
LED CTR → GPIO 11 → 220Ω → GND
GPS NEO-M8N
────────────
VCC → 3.3V
GND → GND
TX → GPIO 43 (ESP32-S3 UART2 RX)
RX → GPIO 44 (ESP32-S3 UART2 TX)
PPS → GPIO 2 (optional precision timing)
POWER SYSTEM
─────────────
Solar panel 6V 2W → TP4056 charger → 18650 LiFePO4 3.2V → AMS1117 3.3V → ESP32-S3
Runtime estimate: ~36h on 3000mAh battery without solar top-up