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
36 changes: 36 additions & 0 deletions .github/workflows/backend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,42 @@ jobs:
export PYTHONPATH=$PYTHONPATH:.
python -m pytest tests/integration/ -v --tb=short -x || echo "No integration tests found"

- name: Run Geo Spatial Integration Tests (real PostGIS + Redis)
run: |
cd backend
docker run -d --name geo-test-pg \
-e POSTGRES_DB=quakeguard_test \
-e POSTGRES_USER=quakeguard \
-e POSTGRES_PASSWORD=ci-postgres-password \
-p 55432:5432 postgis/postgis:15-3.4-alpine
docker run -d --name geo-test-redis -p 56379:6379 redis:7-alpine
echo "Waiting for spatial test infrastructure readiness..."
timeout 60s bash -c "until nc -z localhost 55432 && nc -z localhost 56379; do sleep 2; done"
DATABASE_URL="postgresql://quakeguard:ci-postgres-password@localhost:55432/quakeguard_test" \
REDIS_URL="redis://localhost:56379/0" \
python -m pytest tests/integration/test_geo_integration.py -v --tb=short
docker rm -f geo-test-pg geo-test-redis

- name: Run TimescaleDB Integration Tests (real hypertable)
run: |
cd backend
DATABASE_URL="postgresql://quakeguard:ci-postgres-password@localhost:5432/quakeguard_test" \
python -m pytest tests/integration/test_timescale_integration.py -v --tb=short

- name: Run Zone Readings Integration Tests (real PostGIS)
run: |
cd backend
docker run -d --name zone-test-pg \
-e POSTGRES_DB=quakeguard_test \
-e POSTGRES_USER=quakeguard \
-e POSTGRES_PASSWORD=ci-postgres-password \
-p 55433:5432 postgis/postgis:15-3.4-alpine
echo "Waiting for zone test infrastructure readiness..."
timeout 60s bash -c "until nc -z localhost 55433; do sleep 2; done"
DATABASE_URL="postgresql://quakeguard:ci-postgres-password@localhost:55433/quakeguard_test" \
python -m pytest tests/integration/test_zone_readings.py -v --tb=short
docker rm -f zone-test-pg

- name: Execute Critical Stress Test
env:
API_URL: "http://localhost:8000"
Expand Down
18 changes: 17 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,23 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased] - (Target: v1.2.0)
## [1.2.1] - 2026-08-14
### Added
- **Geo-Zoning:** PostGIS zones as the source of truth (`Zone` model, `GET /zones`, `POST /zones/`), with a geohash-based Redis fast path for coordinate→zone lookup.
- **Zone Detection:** `GET /zones/locate` resolves a device's GPS position into a monitored polygon; Settings now ships "Detect my zone via GPS".
- **Per-Zone Seismograph:** `GET /zones/{zone_id}/readings` + `DELETE /zones/{zone_id}/readings`; the mobile dashboard renders a live seismograph per zone (horizontal zone strip) instead of mixing network-wide telemetry.
- **Per-Area Cooldown Fragmentation:** alert cooldown keys are now area-based (geohash region or zone), not global.
- **GNSS-Ready Data Model:** `Sensor.last_fix_at`, `Reading.lat/lon` captured at ingestion for area-fragmented cooldowns and future spatial correlation.
- **Per-Zone Alerts Feed:** `GET /zones/{zone_id}/alerts` retrieves the confirmed seismic alerts raised for a single area.
- **Live Chart Overhaul (mobile):** sliding window anchored to the wall clock (stale readings leave the window), linear MAG scale (MIN 3.5 / MED 4.0 / ALTO 4.5) with the axis pinned left, positive X seconds and a centered `TIME` label; the trace always renders inside the plot.
- **Settings Explore Section:** links to the GitHub repository (`GiZano/QuakeGuard`) and the QuakeGuard site; `QuakeGuard v1.2.1` footer.

### Changed
- Worker alert pipeline uses per-area cooldown keys and rounds-trip normalization (magnitude estimation shared with the mobile client).
- Peripheral MQTT subscriber wiring and `scripts/simulate_zone.py` updated for the per-zone stream flow.
- Version artifacts bumped to v1.2.1 (CITATION.cff, mobile footer, README roadmap).

## [1.2.0] - 2026-08-06
### Added
- **On-Premise AI Emergency Reports:** New AI layer generates human-readable emergency reports from confirmed seismic alerts via a local Ollama LLM (`llama3.2:1b` default), keeping telemetry on the host.
- **`ollama_client.py`:** Deterministic report generation (`temperature 0.0`, `top_k 1`, streaming disabled) with a strict system prompt ("Only use the provided JSON telemetry. Do not invent data.") and explicit `"AI report unavailable."` fallback on failure.
Expand Down
6 changes: 3 additions & 3 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ authors:
given-names: "Riccardo"
alias: "riccardo0731"
title: "QuakeGuard"
version: 1.2.0
version: 1.2.1
identifiers:
- type: doi
value: "10.5281/zenodo.21814890"
description: "Software archive (v1.2.0)"
date-released: 2026-08-06
description: "Software archive (v1.2.1)"
date-released: 2026-08-14
url: "https://giovanni-zanotti.is-a.dev/Pages/Works/quakeguard/"
repository-code: "https://github.com/GiZano/QuakeGuard"
license: "AGPL-3.0"
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -416,8 +416,9 @@ QuakeGuard/
|---------|-------|
| **v1.0** | ✅ Released — edge seismic detection on ESP32, local alerts |
| **v1.1** | ✅ Released — HiveMQ Cloud MQTT (TLS), ngrok HTTPS tunnel, security hardening |
| **v1.2** | ✅ Current — On-Premise AI Worker (Local Ollama / Llama 3.2) for privacy-preserving emergency reports |
| **v1.2.1** | Zero-Trust Serial Fallback — signed telemetry over USB CDC (serial) when MQTT is unreachable |
| **v1.2** | ✅ Released — On-Premise AI Worker (Local Ollama / Llama 3.2) for privacy-preserving emergency reports |
| **v1.2.1** | ✅ Current — Geo-Zoning & Cooldown Fragmentation — geohash Redis zone index (FastAPI/PostGIS source of truth), per-area cooldown, GNSS-ready data model (`Sensor.last_fix_at`, `Reading.lat/lon`), per-zone live seismograph |
| **v1.2.2** | Zero-Trust Serial Fallback — signed telemetry over USB CDC (serial) when MQTT is unreachable |
| **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 |
Expand Down
60 changes: 48 additions & 12 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Data Plane migration to MQTT Cloud (HiveMQ), REST Control Plane (HTTPS) and TLS

---

## v1.2.0 — On-Premise AI Reports (Current)
## v1.2.0 — On-Premise AI Reports (Released)

On-premise AI integration (LLM in the backend) to generate textual emergency reports from MQTT data.

Expand All @@ -39,7 +39,23 @@ On-premise AI integration (LLM in the backend) to generate textual emergency rep

---

## v1.2.1 — Zero-Trust Serial Fallback
## v1.2.1 — Geo-Zoning & Cooldown Fragmentation (GNSS-ready) (Current)

Geographic zone division designed so the system is ready for the GNSS upgrade (v1.3). The alert cooldown is fragmented from the coarse macro-region level down to a per-area geohash granularity, and the zone-assignment hot path is offloaded from PostGIS queries to a Redis geohash index — with PostGIS kept as the single source of truth.

> **Blocking prerequisite for v1.3 (GNSS):** real GNSS coordinates replace the hardcoded Rome fix in the firmware (`main.cpp`), so the geo layer must already resolve zones and fragment cooldowns from raw coordinates — not from a fixed registration-time zone.

- ✅ **Geohash zone index (Redis fast path)** — `backend/src/geo.py`: at seed time every zone polygon is decomposed into the set of geohash cells (prec 3, ~156 km) it intersects; `resolve_zone()` (in `main.py`) resolves a coordinate from the Redis SET without a DB round-trip. Redis miss or ambiguous multi-zone match falls back to the authoritative PostGIS `ST_Contains` + `ST_Area ASC` query, so the cache can never assign a wrong zone
- ✅ **Pure-Python geohash encoder** matching PostGIS `ST_GeoHash` — zero new dependencies, deterministic keys shared by seed-time index and runtime lookup
- ✅ **Fragmented cooldown lock** — `alert_cooldown:<geohash>` (prec 4, ~50 km) instead of `alert_cooldown:<zone_id>`: two independent events inside the same overlapping macro-polygon no longer silence each other. Legacy `alert_cooldown:zone:<id>` retained for coordinates-less sensors
- ✅ **GNSS-ready data model** — `Sensor.last_fix_at`, `Reading.latitude`/`longitude`; the ingestion payload carries the sensor's fix + geohash; `/devices/register` re-resolves the zone if a relocated node reports a changed fix
- ✅ **Real spatial tests** — integration tests assert resolution against the seeded PostGIS polygons (Milan, Madrid, Tokyo, unknown point), and `point_to_geohash` is cross-validated against PostGIS `ST_GeoHash`

> **Future hardening (for administrative polygons):** `ST_SimplifyPreserveTopology` + GiST tuning, sizing zones so an event cannot physically reach the adjacent zone (~50–100 km for destructive surface-wave propagation).

---

## v1.2.2 — Zero-Trust Serial Fallback

Signed telemetry over a serial link (USB CDC) when MQTT/WiFi connectivity is lost, so the host still receives data during offline simulations.

Expand Down Expand Up @@ -68,6 +84,7 @@ Triangulation algorithm. Multi-node spatial correlation combined with AI reports
- Multi-node spatial and temporal correlation
- Internal epicenter computation
- AI + triangulation data fusion for precise alerts
- KiCad schematics and Gerber files of the node PCB (hardware blueprints for the triangulation node) — already designed; fabrication order + bring-up pending

---

Expand All @@ -81,14 +98,14 @@ Grafana dashboards for real-time visualization of seismic telemetry.
---

> **Geo-zoning & cooldown-lock design (relevant for the paper's System-Engineering claim):**
> - The per-zone Redis cooldown lock (`lock:cooldown:<zone>`) can *silence independent
> earthquakes* inside a single macro-region (e.g. two events in the same "North America"
> polygon trigger one lock and the second alert is dropped as a duplicate).
> - Plan fragmentation to Geohash/H3 keys (`lock:cooldown:<geohash>`) — this offloads zone
> assignment from PostGIS `ST_Contains` to fast Redis lookups.
> - For real administrative polygons: apply `ST_SimplifyPreserveTopology` + a GiST index;
> size zones so an event cannot physically reach the adjacent zone (~50–100 km for
> destructive surface-wave propagation).
> - **Implemented in v1.2.1:** fragmentation to Geohash keys (`alert_cooldown:<geohash>`),
> which offloads zone assignment from PostGIS `ST_Contains` to fast Redis lookups
> and stops overlapping macro-regions from silencing independent earthquakes.
> - **Future:** H3 hex-grid reindexing is re-evaluated when the v2.0 triangulation
> clustering is designed; daily H3 resolution can replace the coarsen geohash grid
> with no zone-model change. For real administrative polygons: apply
> `ST_SimplifyPreserveTopology` + a GiST index; size zones so an event cannot
> physically reach the adjacent zone (~50–100 km for destructive surface-wave propagation).

---

Expand All @@ -109,6 +126,22 @@ Crowning of the engineering phase. Two-tier edge cluster where TinyML is **not**

---

## v2.3.0 — Right-Sized Ingestion at Scale (Redis Streams + TimescaleDB)

Backend ingestion redesigned so the control plane sustains tens of thousands of sensors on a small footprint instead of degrading into a single-queue toy.

- ✅ **Redis Streams replaces the single-consumer list queue** — producers XADD to `readings:stream` (O(1) append); N worker processes drain via consumer groups (`docker compose scale worker=N`); `XAUTOCLAIM` recovers pending entries across worker restarts (at-least-once delivery); poisoned heartbeats park on the `readings:dlq` stream so they never stall the group — `backend/src/ingest.py`
- ✅ **Batched DB commits** — a stream batch (default 64) is written in one transaction instead of one commit per heartbeat
- ✅ **TimescaleDB hypertable on `readings`** — chunked on `recorded_at`; continuous aggregate `readings_minute` serves the dashboard rollups; compression + retention policies. Migration (`backend/src/timescale.py`) is idempotent and **fails closed** on plain PostGIS (dev/CI)
- ✅ **Statistics fast-path** — `/sensors/{id}/statistics` reads the continuous aggregate when present, falls back to a COUNT otherwise
- ✅ **Single TimescaleDB+PostGIS image** — `backend/docker/postgres-timescale.Dockerfile`, wired into `docker-compose.yml`
- ✅ **Real migration coverage** — dedicated CI job runs the hypertable + aggregate tests against the actual deployment image
- ✅ **Load generator** `backend/scripts/load_test.py` — N sensors at H Hz (default matches the 150-sensor CI requirement), stream or HTTP transport

> **Scale math (design target):** 150 sensors @ 1/5s ≈ 30 msg/s (trivial today); 10k sensors @ 1 Hz ≈ 2k msg/s (bounded by worker count + hypertable inserts, still 1 Postgres node). The MQTT transport already exists (firmware → broker → bridge → API); the bridge stays HTTP-proxying by design — direct MQTT→stream is the documented next step only if the broker becomes the bottleneck.

---

## #Research — Scientific Validation (SIL)

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.
Expand Down Expand Up @@ -190,9 +223,12 @@ Production-grade cloud platform behind the alert pipeline: the MQTT/REST/AI stac

### Performance & scaling engineering (post-paper, not needed at current scale)

- **Kafka / Redpanda as the central ingestion buffer (millions-class)** — replaces Redis Streams as the durable, replayable backbone once sustained ingestion exceeds what a single Redis node can buffer. The v2.3.0 consumer interface (`src/ingest.py`) is deliberately transport-agnostic: `enqueue_reading` / `read_batch` / `ack` / `recover_pending` are re-pointable so a Kafka-backed implementation can slot in without touching the worker. Also unlocks partitions-per-sensor ordering and backfill reprocessing for the triangulation engine (v2.0).
- **ClickHouse for cold-path analytics** — move long-range dashboards / multi-node correlation queries (epicenter triangulation, swarm clustering) off the operational Postgres node onto a columnar store with a Kafka connector. Cold reads never contend with the ingestion hot path; TimescaleDB continuous aggregates keep serving the real-time dashboard.
- **Non-blocking MQTT-Bridge refactor** — `aiomqtt` + async push to Redis (or `httpx`/`aiohttp`)
to make the bridge relay fully non-blocking. *Parked: the current synchronous bridge is not
the bottleneck at sandbox scale.*
to make the bridge relay fully non-blocking. *Partially superseded by v2.3.0: the ingestion
endpoint is now an O(1) stream append, so the HTTP-proxying bridge is no longer the DB
bottleneck; direct MQTT→stream still removes the HTTP hop and is the documented next step.*
- **Rust ingestion microservice (Axum) + ECDSA verification via PyO3** — the hybrid path:
keep FastAPI/PostGIS/Ollama, move only the CPU-bound signature verification (P-256/SHA-256)
to native speed; a dedicated Axum ingestion endpoint can later absorb `POST /readings/`.
Expand Down
Loading
Loading