Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .github/workflows/iot-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ on:
branches: [ "main", "develop" ]
paths:
- 'firmware/**'
- 'research/**'
- '.github/workflows/iot-ci.yml'
pull_request:
branches: [ "main" ]
paths:
- 'firmware/**'
- 'research/**'
- '.github/workflows/iot-ci.yml'

jobs:
Expand Down Expand Up @@ -55,4 +57,14 @@ jobs:
g++ -std=c++11 -I src test/test_ringbuffer.cpp -o /tmp/test_ringbuffer
/tmp/test_ringbuffer
g++ -std=c++11 -I src test/test_detection.cpp -lm -o /tmp/test_detection
/tmp/test_detection
/tmp/test_detection

- name: Build Host SIL CLI (same core as firmware)
run: |
cd firmware
g++ -std=c++11 -I src tools/detect_cli.cpp -lm -o /tmp/detect_cli

- name: SIL Smoke Test (synthetic dataset)
run: |
python research/synthetic.py research/out/synth_dataset --n-events 3
cd research && python calibrate.py out/synth_dataset --out out/calibration.json
11 changes: 10 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
.venv/
__pycache__/
*.py[cod]
*.py[cod]

# ------------------------------------------
# Node.js / React Native (Mobile)
Expand Down Expand Up @@ -50,4 +51,12 @@ ROADMAP.md
# ------------------------------------------
# Compiled Documents & Media
# ------------------------------------------
*.pdf
*.pdf

# ------------------------------------------
# Research (SIL validation, ROADMAP R1)
# ------------------------------------------
research/data/
research/out/
research/*.png
firmware/tools/detect_cli
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,12 +407,14 @@ QuakeGuard/
| **v1.3** | GNSS sync — accurate node timestamps, GPS coordinate resolution, ADXL345 calibration |
| **v2.0** | Triangulation — multi-node spatial correlation + AI reports for epicenter calculation |
| **v2.1** | Data Dashboards — Grafana dashboards for real-time visualization of seismic telemetry |
| **v2.2** | Heterogeneous Edge Intelligence — hybrid Tier A (STA/LTA) + Tier B (quantized CNN) decision fusion |
| **Future** | Cloud IaC — Kubernetes + Terraform auto-scaling platform (see [ROADMAP.md](ROADMAP.md)) |

### #Research — Scientific Validation (SIL)

| Node | Focus |
|------|-------|
| **#Research** | Parallel node (starts after v2.1) — SIL cross-validation: ground-truth INGV replay of the exact production C++ STA/LTA core, Python-only as orchestrator, ROC metrics + AI benchmarking (latency P50/P99, hallucination rate). See [ROADMAP.md](ROADMAP.md) |
| **#Research** | Parallel ongoing node — SIL cross-validation: replay of the exact production C++ STA/LTA core (`DetectionCore.h`) on the host via the same C++ source, Python-only as orchestrator, ROC metrics + AI benchmarking (latency P50/P99, hallucination rate). R1 pipeline (core isolation, host CLI, orchestrator, metrics, calibration) implemented; see [ROADMAP.md](ROADMAP.md) |

---

Expand Down
62 changes: 54 additions & 8 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,21 +70,42 @@ Grafana dashboards for real-time visualization of seismic telemetry.

---

## v2.2.0 — Heterogeneous Edge Intelligence

Crowning of the engineering phase. Two-tier edge cluster where TinyML is **not** a simple STA/LTA replacement, but a hierarchical Decision Fusion between cheap ubiquitous sensors and intelligent confirmation gates.

> **Blocking prerequisite:** the STA/LTA parameter calibration from **R1** must be completed before drafting/training the v2.2.0 models. Calibration is urgent and runs in parallel with v1.3.

**Tier A — Ubiquitous sensors (ESP32-C3):**
- Low-cost, installable anywhere; STA/LTA + ECDSA signing, unchanged from v1.x
- Produce the **proprietary MEMS dataset** (fills the domain gap vs INGV professional seismometers)

**Tier B — Intelligent confirmation gates (ESP32-S3):**
- Hybrid quantized CNN (INT8) via ESP-DL / TensorFlow Lite Micro
- Activated **only on STA/LTA triggers** to compute the local event probability
- Emits a confidence score that confirms or discards Tier A triggers (Decision Fusion)

---

## #Research — Scientific Validation (SIL)

Parallel node (non-semantic): started after reaching **v2.1** at a minimum.
Parallel node (non-semantic). **R1 is the Foundation**: it starts immediately, in parallel with v1.3 (GNSS), and its calibration is urgent because it is the blocking prerequisite for v2.2.0 and the paper.

Software-in-the-Loop (SIL) cross-validation: no logic duplication. It uses **100% of the production C++ code** on both the firmware and the host, guaranteeing numerical equivalence for the IEEE paper.

### R1 — STA/LTA detection cross-validation (High priority)
### R1 — STA/LTA detection cross-validation via SIL (Foundation)

> Status: **implemented (Fase 0–4).** The STA/LTA core is isolated in pure C++, compiled natively in CI, and driven by the Python orchestrator with zero logic duplication.
>
> Scheduling: **in parallel with v1.3 (GNSS)**. The calibration of the trigger parameters is **urgent and blocking**: it gates the start of v2.2.0 model drafting/training.

> Status: **Not implemented.** Only a partial baseline exists today: the pure, shared `RingBuffer.h` (compiled natively in CI) and host unit tests of the detection logic (`test_detection.cpp`, which currently re-implements rather than reuses the firmware core). The STA/LTA core is still inline in `main.cpp` and the Python/INGV orchestrator is missing.
- [x] Isolation of the STA/LTA algorithmic core in pure C++, fully decoupled from the ESP32 hardware (no I2C/WiFi/FreeRTOS calls in the algorithmic core) — `firmware/src/DetectionCore.h`
- [x] Native host compilation of the C++ core (same source as the firmware) — `detect_cli.cpp` + CI build
- [x] Python as the **sole orchestrator**: reading the public INGV dataset (accelerograms), passing data to the C++ binary via `subprocess`, collecting trigger points and tracing ROC curves — `research/`
- [x] Metrics: Sensitivity/Recall, False-Alarm Rate, response latency — `research/metrics.py`
- [x] Calibration of the trigger parameters (`TRIGGER_RATIO`, `NOISE_FLOOR`, `HPF_ALPHA`) against ground-truth — `research/calibrate.py`; **real ITACA download pending** (`ITICA_TOKEN`, see `research/README.md`)

- [ ] Isolation of the STA/LTA algorithmic core in pure C++, fully decoupled from the ESP32 hardware (no I2C/WiFi/FreeRTOS calls in the algorithmic core)
- [ ] Native host compilation of the C++ core (same source as the firmware)
- [ ] Python as the **sole orchestrator**: reading the public INGV dataset (accelerograms), passing data to the C++ binary via `ctypes`/`subprocess`/`pybind11`, collecting trigger points and tracing ROC curves
- [ ] Metrics: Sensitivity/Recall, False-Alarm Rate, response latency
- [ ] Calibration of the trigger parameters (`TRIGGER_RATIO`, `NOISE_FLOOR`, `HPF_ALPHA`) against INGV ground-truth
> **Remaining for full R1 closure:** real ITACA/INGV ground-truth validation once the ITACA token portal is reachable; the calibration currently runs on the realistic synthetic fallback (unit-tested, CI-covered).

### R2 — AI Benchmarking (this is the paper's primary novelty contribution)

Expand All @@ -100,3 +121,28 @@ Software-in-the-Loop (SIL) cross-validation: no logic duplication. It uses **100

- [ ] Publish open validation dataset (Zenodo DOI, separate from software)
- [ ] Draft technical paper / preprint (arXiv)

---

## Future Horizon — Cloud Infrastructure & Real-Time Auto-Scaling

Production-grade cloud platform behind the alert pipeline: the MQTT/REST/AI stack of v1.x–v2.2 runs as containerized workloads on Kubernetes, fully provisioned as **Infrastructure-as-Code** with Terraform. The control plane elastically scales with the number of deployed sensors and with real-time alert bursts.

> Post-research horizon (after v2.2.0 / paper). Not blocking for the thesis; it targets the operational release of the system.

**Infrastructure-as-Code (Terraform):**
- Declarative provisioning of the cloud provider resources (managed Kubernetes cluster, VPC, node pools, networking) in versioned modules
- State management and drift detection for reproducible, auditable deployments

**Orchestration (Kubernetes):**
- Containerized deployment of the MQTT broker, AI report worker, REST control plane and dashboard
- Native autoscaling (Horizontal Pod Autoscaler / cluster autoscaler) driven by MQTT ingestion rate and CPU/memory
- Real-time elastic burst handling: alert spikes scale up workers (AI reports) and event queues; quiet periods scale to zero
- Rolling updates, health probes and self-healing for continuous availability

**Delivery & observability:**
- GitOps / CI/CD pipeline applying Terraform and Helm charts
- Monitoring and alerting for the cluster itself (resource saturation, autoscaling events)

---

111 changes: 111 additions & 0 deletions firmware/src/DetectionCore.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#pragma once

#include <cmath>
#include "RingBuffer.h"

/**
* SeismicDetector — pure-C++ STA/LTA detection core.
*
* Fully decoupled from the ESP32 hardware: no Arduino, FreeRTOS, I2C or WiFi
* calls in this translation unit. This is the single source of truth used both
* by the firmware (sensorTask) and by the host SIL validation (detect_cli),
* guaranteeing numerical equivalence.
*
* The detector is clock-injected: callers pass an absolute "now" in
* milliseconds. The firmware passes millis(); the host passes sample_index * 10
* at 100 Hz. This keeps the detection decision identical on both targets.
*/
class SeismicDetector {
public:
static constexpr float DEFAULT_HPF_ALPHA = 0.9f;
static constexpr float DEFAULT_TRIGGER_RATIO = 1.8f;
static constexpr float DEFAULT_NOISE_FLOOR = 0.04f;
static constexpr float DEFAULT_LTA_FLOOR = 0.01f;
static constexpr size_t STA_WINDOW = 100; // 1 s @ 100 Hz
static constexpr size_t LTA_WINDOW = 1000; // 10 s @ 100 Hz
static constexpr unsigned long COOLDOWN_MS = 5000;
static constexpr float INITIAL_RAW = 9.81f; // gravity baseline

explicit SeismicDetector(float hpfAlpha = DEFAULT_HPF_ALPHA,
float triggerRatio = DEFAULT_TRIGGER_RATIO,
float noiseFloor = DEFAULT_NOISE_FLOOR);

/**
* Euclidean norm of a 3-axis acceleration sample (the single shared
* definition of "magnitude" used by firmware and host).
*/
static float norm3(float x, float y, float z) {
return std::sqrt(x * x + y * y + z * z);
}

/**
* Feed one raw acceleration magnitude (norm of the 3 axes).
* Returns true when a seismic event is detected.
*/
bool push(float rawMag, unsigned long nowMs);

// Ratio of the last evaluated sample (STA/LTA).
float lastRatio() const { return ratio_; }
// STA of the last evaluated sample.
float lastSTA() const { return sta_; }

private:
float hpfAlpha_;
float triggerRatio_;
float noiseFloor_;

float filtered_ = 0.0f; // HPF state
float prevRaw_ = INITIAL_RAW;

RingBuffer<STA_WINDOW> staBuf_;
RingBuffer<LTA_WINDOW> ltaBuf_;

bool inAlarm_ = false;
unsigned long alarmStartMs_ = 0;

float sta_ = 0.0f;
float ratio_ = 0.0f;
};

// ---------------------------------------------------------------------------
// Implementation (header-only)
// ---------------------------------------------------------------------------

inline SeismicDetector::SeismicDetector(float hpfAlpha, float triggerRatio, float noiseFloor)
: hpfAlpha_(hpfAlpha), triggerRatio_(triggerRatio), noiseFloor_(noiseFloor) {}

inline bool SeismicDetector::push(float rawMag, unsigned long nowMs) {
// High-pass filter to remove gravity. NOTE: uses std::abs; on-firmware the
// same expression is used so results are bit-identical on host vs ESP32.
filtered_ = hpfAlpha_ * (filtered_ + rawMag - prevRaw_);
prevRaw_ = rawMag;
float abs_signal = filtered_ < 0.0f ? -filtered_ : filtered_;

// Noise gate.
if (abs_signal < noiseFloor_) abs_signal = 0.0f;

staBuf_.push(abs_signal);
ltaBuf_.push(abs_signal);

// Do not trigger until the full LTA window is populated.
if (!ltaBuf_.isFull()) return false;

sta_ = staBuf_.average();
float lta = ltaBuf_.average();
if (lta < DEFAULT_LTA_FLOOR) lta = DEFAULT_LTA_FLOOR; // division-by-zero guard

ratio_ = sta_ / lta;

bool triggered = false;
if (ratio_ >= triggerRatio_ && sta_ > noiseFloor_ && !inAlarm_) {
inAlarm_ = true;
alarmStartMs_ = nowMs;
triggered = true;
}

if (inAlarm_ && (nowMs - alarmStartMs_ > COOLDOWN_MS)) {
inAlarm_ = false;
}

return triggered;
}
55 changes: 8 additions & 47 deletions firmware/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
#include <array>
#include <vector>
#include <chrono>
#include "RingBuffer.h"
#include "DetectionCore.h"

// --------------------------------------------------------------------------
// HARDWARE & SERVER CONFIGURATION
Expand Down Expand Up @@ -87,8 +87,6 @@ struct SeismicEvent {
unsigned long event_millis;
};

constexpr float ALPHA_LTA = 0.05f;
constexpr float ALPHA_STA = 0.40f;
constexpr float TRIGGER_RATIO = 1.8f;
constexpr float NOISE_FLOOR = 0.04f;
constexpr float HPF_ALPHA = 0.9f;
Expand Down Expand Up @@ -248,64 +246,27 @@ bool performProvisioning() {
// TASK 1: SENSOR ACQUISITION
// --------------------------------------------------------------------------
void sensorTask(void *pvParameters) { // NOSONAR
float prev_raw_mag = 9.81f;
float filtered_mag = 0.0f;
sensors_event_t event;

// Instantiate our strict rolling window buffers!
// At 100Hz: STA = 1 second, LTA = 10 seconds
RingBuffer<100> staBuffer;
RingBuffer<1000> ltaBuffer;
// Pure-C++ STA/LTA core, shared with the host SIL validation (same source).
SeismicDetector detector(HPF_ALPHA, TRIGGER_RATIO, NOISE_FLOOR);

Serial.println("[SENSOR] Task Active. Stabilizing and filling buffers...");

TickType_t xLastWakeTime = xTaskGetTickCount();
const TickType_t xFrequency = pdMS_TO_TICKS(10); // Exactly 100Hz

bool inAlarm = false;
unsigned long alarmStart = 0;

for(;;) {
vTaskDelayUntil(&xLastWakeTime, xFrequency);
accel.getEvent(&event);
float raw_mag = sqrt(pow(event.acceleration.x, 2) + pow(event.acceleration.y, 2) + pow(event.acceleration.z, 2));

// High Pass Filter to remove gravity
filtered_mag = HPF_ALPHA * (filtered_mag + raw_mag - prev_raw_mag);
prev_raw_mag = raw_mag;
float abs_signal = abs(filtered_mag);

if (abs_signal < NOISE_FLOOR) abs_signal = 0.0f;

// Push the clean signal into our circular buffers
staBuffer.push(abs_signal);
ltaBuffer.push(abs_signal);

// Wait until the Long-Term window is fully populated before triggering alarms
if (!ltaBuffer.isFull()) {
continue;
}
float raw_mag = SeismicDetector::norm3(event.acceleration.x, event.acceleration.y, event.acceleration.z);

float sta = staBuffer.average();
float lta = ltaBuffer.average();

// Prevent division by zero if LTA drops too low
if (lta < 0.01f) lta = 0.01f;

float ratio = sta / lta;

// DEBUG: Uncomment this line to view the rolling windows in the Serial Plotter!
// Serial.printf("Signal:%.3f,STA:%.3f,LTA:%.3f,Ratio:%.2f\n", abs_signal, sta, lta, ratio);

if (ratio >= TRIGGER_RATIO && sta > NOISE_FLOOR && !inAlarm) {
Serial.printf("[SENSOR] EARTHQUAKE! Ratio: %.2f (Mag: %.3f G)\n", ratio, sta);
SeismicEvent evt = { ratio, millis() };
// Clock-injected: the detector receives the same millis() the firmware would use.
if (detector.push(raw_mag, millis())) {
Serial.printf("[SENSOR] EARTHQUAKE! Ratio: %.2f (Mag: %.3f G)\n", detector.lastRatio(), detector.lastSTA());
SeismicEvent evt = { detector.lastRatio(), millis() };
xQueueSend(eventQueue, &evt, 0);
inAlarm = true;
alarmStart = millis();
}

if (inAlarm && (millis() - alarmStart > 5000)) inAlarm = false;
}
}

Expand Down
Loading
Loading