diff --git a/docs/web/quakeguard.html b/docs/web/quakeguard.html index f1c1908..4f62bba 100644 --- a/docs/web/quakeguard.html +++ b/docs/web/quakeguard.html @@ -473,20 +473,38 @@

Geographic Zones

-
-
+
+
Where it’s going

Roadmap

-
-
v1.0 · Released

Edge seismic detection on ESP32-C3 and local alerts.

-
v1.1 · Released

HiveMQ Cloud MQTT (TLS), ngrok HTTPS tunnel, security hardening.

-
v1.2.1 · Current

Geo-Zoning & GNSS-Ready: Redis Streams ingestion, TimescaleDB persistence, per-area dedup cooldown and on-premise AI emergency reports.

-
v1.2.2 · Cleanup

SonarCloud cleanup and quality-gate fixes (PR #368).

-
v1.3 · GNSS (in progress)

GNSS module already done (NEO-6M/M8N, NVS last-known fix); NTP + PPS time sync still pending.

-
v2.0 · Triangulation

Multi-node spatial correlation + AI reports for epicenter calculation.

-
v2.1 · Dashboards

Grafana live visualization of seismic telemetry.

+ +
+
+

Development

+
+
v1.0 · Released

Edge seismic detection on ESP32-C3 and local alerts.

+
v1.1 · Released

HiveMQ Cloud MQTT (TLS), ngrok HTTPS tunnel, security hardening.

+
v1.2.0 · Released

On-Premise AI Reports — Ollama generates emergency reports on-premise, pushed over WebSocket.

+
v1.2.1 · Current

Geo-Zoning & Cooldown Fragmentation (GNSS-ready) — geohash Redis fast-path zone resolution, per-area cooldown locks, PostGIS ST_Contains as source of truth.

+
v1.2.2 · Serial fallback

Zero-Trust Serial Fallback — ECDSA-signed telemetry over USB CDC when MQTT/WiFi is unreachable.

+
v1.3 · Synchronized GNSS

Optional GNSS module (NEO-6M/M8N), NTP + PPS timestamps, real coordinates replacing the hardcoded fix, ADXL345 calibration.

+
v2.0 · Triangulation

Multi-node spatial correlation + AI reports for epicenter calculation.

+
v2.1 · Dashboards

Grafana live visualization of seismic telemetry.

+
+
+
+

Research

+
+
R1 · Done

SIL STA/LTA cross-validation — pure C++ core, host orchestrator, metrics and trigger calibration on the synthetic fallback. Real ESM ground-truth validation remains.

+
R2 · Next

AI benchmarking — P50/P99 latency of the local Ollama worker, hallucination rate, privacy/latency vs cloud baseline.

+
R3 · Planned

Dissemination — open validation dataset (Zenodo DOI) + technical paper / preprint.

+
+

R1 is the blocking prerequisite for the v2.2.0 Edge AI tier and runs in parallel with v1.3 (GNSS).

+
diff --git a/docs/web/quakeguard.js b/docs/web/quakeguard.js index 9ecbaa6..75e0067 100644 --- a/docs/web/quakeguard.js +++ b/docs/web/quakeguard.js @@ -1,252 +1,195 @@ /* QuakeGuard — EN/IT dictionary (migrated from the old quakeguard-i18n.js engine) */ -window.I18N_DATA = { - en: { - navbar_sections: 'Sections', navbar_demo: 'Live Demo', navbar_mission: 'Mission', - navbar_architecture: 'Architecture', navbar_resilience: 'Resilience', - navbar_security: 'Security', navbar_zones: 'Geographic Zones', navbar_roadmap: 'Roadmap', - navbar_health: 'Project Health', navbar_mentors: 'Mentors', navbar_github: 'GitHub', navbar_home: 'Home', - - hero_sub: 'Electro-Domestic Seismic Alarm System', - hero_lead: 'A full-stack IoT architecture for real-time detection, analysis and reporting of seismic events. Everyday household appliances — washing machines, TVs, refrigerators — become a distributed earthquake early-warning network that alerts you before the shaking arrives. v1.2.1 Geo-Zoning & GNSS-Ready — Redis Streams ingestion, per-area cooldown, TimescaleDB persistence and on-premise AI emergency reports with Ollama — privacy-first, seismic telemetry never leaves the machine.', - hero_trigger: 'Trigger an earthquake', hero_github: 'View on GitHub', - hero_whitepaper: 'Whitepaper', hero_wiki: 'Wiki', - - mission_eyebrow: 'Mission', mission_title: 'Detection for everyone', - mission_lead: 'We want to make earthquake detection available all around the world, with zero cost for the people and low cost for the enterprises. By embedding miniature IoT sensors inside the objects we already live with, we can grant P-wave detection without anyone even knowing — and give people time to evacuate or reach a safe location before it is too late.', - stat_sample: 'Edge sampling', stat_mag: 'Magnitude threshold', - stat_dedup: 'Per-area dedup / anti-replay', stat_rate: 'Rate limit per IP', - - demo_eyebrow: 'Live Demo', demo_title: 'Trigger an earthquake', - demo_sub: 'A faithful, client-only echo of the backend flow: pick one of the 8 geographic zones, set a magnitude >= 4.5 and watch the pipeline run from edge to mobile alert + AI report.', - demo_notice: 'This is a client-side simulation — everything runs in your browser, no real request is sent. Try it below.', - demo_zone: 'Geographic zone', demo_mag: 'Estimated magnitude', - demo_mag_hint: 'M >= 4.5 triggers an alert', demo_msg: 'Message', - demo_msg_ph: 'Simulated Critical Event', demo_trigger: 'Trigger Earthquake', - demo_historic: 'Historically the backend bypasses the IoT pipeline and pushes straight to the Redis quake_alerts channel — the mobile app vibrates and fires a push notification within milliseconds.', - demo_console_heading: 'Ingestion → Alert pipeline', - demo_console_ready: 'Ready — select a zone and trigger an earthquake to watch the pipeline.', - demo_feed_heading: 'Recent critical events (last 10)', demo_ai_heading: 'AI Emergency Report', - - arch_eyebrow: 'From vibration to alert', arch_title: 'System Architecture', - arch_iot: 'IoT Edge Sensors', - arch_iot_p: 'ESP32-C3 (RISC-V) + ADXL345 accelerometer. Runs a FreeRTOS DSP pipeline with ring-buffer STA/LTA, ECDSA signing on every payload and an optional GNSS subsystem (NEO-6M/M8N) with NVS last-known-fix.', - arch_api: 'FastAPI Cloud', - arch_api_p: 'Fully async Python 3.11 gateway. Sliding-window rate limiting, API-key auth, ECDSA verification and anti-replay.', - arch_redis: 'Redis Streams + Pub/Sub', - arch_redis_p: 'Decouples ingestion from processing. Redis Streams offloading, per-area dedup locks, a Dead Letter Queue on failure, plus a dedicated ai_report_queue for ai_report_worker.py decoupled from the alert engine.', - arch_mqtt: 'MQTT Data Plane', - arch_mqtt_p: 'HiveMQ Cloud broker over TLS. A dedicated bridge microservice forwards telemetry to the secure HTTP pipeline.', - arch_pg: 'PostgreSQL + TimescaleDB + PostGIS', - arch_pg_p: 'Time-series + spatial persistence. Sensor data lands in a TimescaleDB hypertable; zones resolve through a geohash fast-path Redis index, with PostGIS ST_Contains as the authoritative fallback.', - arch_app: 'React Native App', - arch_app_p: 'Three-tab app (Monitor, Sensor Map, Settings). Per-zone live seismograph with a horizontal zone strip, GPS "Detect my zone" via GET /zones/locate, WebSocket alerts with SOS haptics & push, siren audio (expo-audio), MIC/RESEARCH dual theme, offline mode, last-10 alert history and AI report cards.', - - pipe_1_t: 'Detect', pipe_1_d: 'ADXL345 at 100 Hz → HPF removes gravity → STA/LTA ratio > 1.8 flags an event.', - pipe_2_t: 'Sign', pipe_2_d: 'Firmware signs the payload with ECDSA NIST P-256 — the private key never leaves the device NVS.', - pipe_3_t: 'Publish', pipe_3_d: 'MQTT publish to quakeguard/telemetry on HiveMQ Cloud (TLS, port 8883).', - pipe_4_t: 'Verify', pipe_4_d: 'Bridge → POST /readings/. API key, sensor status, anti-replay (60 s) and signature are all validated.', - pipe_5_t: 'Queue', pipe_5_d: 'Rate limit (50 req/s per IP) passes → event appended to Redis Streams (readings:stream); the API replies 202 instantly.', - pipe_6_t: 'Process', pipe_6_d: 'Worker estimates M = log10(PGA × 1.6) + 3.0; if M ≥ 4.5 an Alert is triggered.', - pipe_7_t: 'Persist & dedup', pipe_7_d: 'Atomically saved to TimescaleDB (hypertable) + PostGIS; a per-area Redis cooldown (geohash region or zone) prevents alert storms (outbox pattern).', - pipe_8_t: 'Alert', pipe_8_d: 'WebSocket broadcast → mobile SOS haptic vibration + high-priority push notification, sub-second delivery.', - pipe_9_t: 'AI Report', pipe_9_d: 'ai_report_queue in Redis → worker → Ollama generates the emergency report on-premise; persisted and pushed on the ai_reports WebSocket channel.', - - dp_1: 'Reading captured at 100 Hz (ADXL345 ±16G)', - dp_2: 'STA/LTA = 2.11 > 1.8 → seismic event', - dp_3: 'Payload signed with ECDSA NIST P-256', - dp_4: 'MQTT publish → quakeguard/telemetry (HiveMQ TLS)', - dp_5: 'Bridge → POST /readings/ → signature & replay verified', - dp_6: 'Rate limit OK → Redis Streams (202 Accepted)', - dp_7: 'Worker: Magnitude estimated (M = log10(PGA × 1.6) + 3.0)', - dp_8: 'Per-area cooldown acquired (dedup outbox)', - dp_9: 'PostGIS zone assign → WebSocket broadcast', - dp_10: 'Mobile: SOS haptic + push notification', - dp_11: 'AI Report: ai_report_queue → Ollama → report on ai_reports WS channel', - - res_eyebrow: 'Built for the crowd', res_title: 'The Thundering Herd', - res_card_title: 'How the spike is absorbed', - res_lead1: 'Earthquakes do not knock on one door. When hundreds of sensors in the same zone detect the same event at once, the backend is hit by a massive synchronous traffic spike.', - res_lead2: 'QuakeGuard is engineered for exactly that moment: ingestion is decoupled from processing, requests return 202 in milliseconds, connection pooling absorbs the load, and a per-area Redis cooldown fires one clean alert — not a storm of duplicates.', - res_queue_t: 'Producer-consumer queue', res_queue_p: 'ingestion never waits on the database; events offload to Redis Streams instantly.', - res_rate_t: 'Control-plane rate limit', res_rate_p: '50 req/s per IP, backs off buggy nodes early.', - res_pool_t: 'Connection pooling', - res_pool_p: 'Aggressive SQLAlchemy pool for high concurrency.', - res_cooldown_t: 'Per-area cooldown', res_cooldown_p: 'Dedup lock per geohash region or zone; only the winning worker publishes the Alert (outbox pattern).', - - sec_eyebrow: 'Zero-Trust Edge', sec_title: 'Cryptographic Security', - sec_lead: 'Data integrity is paramount in an emergency system. Every telemetry packet is cryptographically secured end-to-end against spoofing, replay and tampering.', - sec_gates_title: 'The 4 validation gates', - sec_gate_key_t: 'API key', sec_gate_key_p: 'Constant-time comparison to resist timing attacks.', - sec_gate_status_t: 'Sensor status', sec_gate_status_p: 'Confirms the sensor exists and is active.', - sec_gate_replay_t: 'Anti-replay', sec_gate_replay_p: 'Timestamps older than 60 s are rejected with 403.', - sec_gate_sig_t: 'ECDSA signature', sec_gate_sig_p: 'Payload verified against the device public key (DER and r||s).', - sec_identity_title: 'Identity & threat coverage', - sec_identity_p: 'Each ESP32-C3 generates its own NIST P-256 ECDSA keypair; the private key is sealed and never leaves the device. The public key becomes the commitment identity during /devices/register.', - sec_mitm: 'MitM protected', sec_spoof: 'Spoofing blocked', sec_replay: 'Replay blocked', - sec_brute: 'Brute force limited', sec_unauth: 'Unauthorized access blocked (fail-fast)', - - zones_eyebrow: 'Spatial intelligence', zones_title: 'Geographic Zones', - zones_lead: '8 global regions are pre-seeded: zone resolution uses a geohash Redis fast path, with PostGIS ST_Contains as the authoritative fallback and an Unknown Region fallback for unmapped coordinates. The mobile app can detect your zone with one tap via GET /zones/locate.', - zones_unknown_sub: 'Fallback for unmapped coordinates', - zone_1: 'Italy - North', zone_2: 'Italy - Center', zone_3: 'Italy - South & Islands', - zone_4: 'Western Europe', zone_5: 'North America', zone_6: 'South America', - zone_7: 'East Asia', zone_8: 'Unknown Region', - - roadmap_eyebrow: 'Where it is going', roadmap: 'Roadmap', - road_1_label: 'v1.0 · Released', road_1: 'Edge seismic detection on ESP32-C3 and local alerts.', - road_11_label: 'v1.1 · Released', road_11: 'HiveMQ Cloud MQTT (TLS), ngrok HTTPS tunnel, security hardening.', - road_12_label: 'v1.2.1 · Current', road_12: 'Geo-Zoning & GNSS-Ready: Redis Streams ingestion, TimescaleDB persistence, per-area dedup cooldown and on-premise AI emergency reports.', - road_122_label: 'v1.2.2 · Cleanup', road_122: 'SonarCloud cleanup and quality-gate fixes (PR #368).', - road_13_label: 'v1.3 · GNSS (in progress)', road_13: 'GNSS module already done (NEO-6M/M8N, NVS last-known fix); NTP + PPS time sync still pending.', - road_20_label: 'v2.0 · Triangulation', road_20: 'Multi-node spatial correlation + AI reports for epicenter calculation.', - road_21_label: 'v2.1 · Dashboards', road_21: 'Grafana live visualization of seismic telemetry.', - - health_eyebrow: 'Engineering rigor', health_title: 'Project Health', - health_cd_title: 'CI/CD', health_cd_p: 'Five automated pipelines gate every change:', - health_stress_title: 'Stress test', - health_stress_p: 'Validates the full ingestion pipeline against a simulated massive event.', - health_stress_cert: 'SYSTEM CERTIFIED', - health_docs_title: 'Deep documentation', - health_docs_p: 'A Typst-compiled whitepaper walks through architecture, hardware, security, backend, mobile and deployment.', - health_docs_os: 'Open source under AGPL-3.0, with a DOI, contributing guide and security policy.', - health_docs_read: 'Read the whitepaper', health_docs_star: 'Star on GitHub', - - exp_eyebrow: 'See it in action', exp_title: 'The experience', - exp_note: 'Watch the demo with captions available in English and Italian.', - - contact_title: 'Contribute or collaborate', - contact_p: 'QuakeGuard is open source and community-driven. Ideas, issues and pull requests are always welcome.', - contact_open: 'Open the repository', contact_email: 'Email me', - footer_ttl: 'Get in touch', footer_email: 'Email Me', - mentors_eyebrow: 'Milestone', mentors_title: 'Mentorship & Recognition', - mentors_p1: 'Developed as a school-contest project for Hackersgen by Sorint.lab and the GF Marilli competition — in collaboration with riccardo0731.', - mentors_p2: 'We were honored to work with Francesco Finazzi — UniBG professor and founder of Earthquake Network, the largest community seismic network in the world. After the conference, our collaboration with him is exactly where the project’s story began.', - mentor_1: 'Sorint.lab — Hackersgen', mentor_2: 'University of Bergamo', - contact_eyebrow: 'Get in touch', - footer_tagline: 'BSc Computer Science Student at UniPi | Specialized in Edge Computing, Cloud Architecture, and IoT.', - video_fallback: 'Your browser does not support the video tag.' - }, - it: { - navbar_sections: 'Sezioni', navbar_demo: 'Demo', navbar_mission: 'Missione', - navbar_architecture: 'Architettura', navbar_resilience: 'Resilienza', - navbar_security: 'Sicurezza', navbar_zones: 'Zone Geografiche', navbar_roadmap: 'Roadmap', - navbar_health: 'Stato del progetto', navbar_mentors: 'Mentor', navbar_github: 'GitHub', navbar_home: 'Home', - - hero_sub: 'Sistema di allarme sismico elettro-domestico', - hero_lead: 'Un\u2019architettura IoT full-stack per la rilevazione, l\u2019analisi e il reporting in tempo reale di eventi sismici. Gli elettrodomestici di tutti i giorni — lavatrici, TV, frigoriferi — diventano una rete distribuita di early warning che ti avvisa prima dell\u2019arrivo della scossa. v1.2.1 Geo-Zoning & GNSS-Ready — ingestione Redis Streams, cooldown per area, persistenza TimescaleDB e report AI di emergenza on-premise con Ollama — privacy-first, la telemetria non lascia mai la macchina.', - hero_trigger: 'Genera un terremoto', hero_github: 'Vedi su GitHub', - hero_whitepaper: 'Whitepaper', hero_wiki: 'Wiki', - - mission_eyebrow: 'Missione', mission_title: 'Rilevazione per tutti', - mission_lead: 'Vogliamo rendere la rilevazione dei terremoti disponibile in tutto il mondo, con costo zero per le persone e costo contenuto per le aziende. Grazie ai mini-sensori IoT integrati negli oggetti di uso quotidiano possiamo garantire la rilevazione delle onde P senza che nessuno se ne accorga, e dare alle persone il tempo di evacuare o mettersi in salvo prima che sia troppo tardi.', - stat_sample: 'Campionamento edge', stat_mag: 'Soglia di magnitudo', - stat_dedup: 'Dedup per area / anti-replay', stat_rate: 'Limite richieste per IP', - - demo_eyebrow: 'Live Demo', demo_title: 'Genera un terremoto', - demo_sub: 'Un eco fedele, solo client-side, del flusso del backend: scegli una delle 8 zone geografiche, imposta una magnitudo e guarda la pipeline scorrere dall\u2019edge fino all\u2019allerta mobile + report AI.', - demo_notice: 'Questa è una simulazione lato client: tutto avviene nel tuo browser, nessuna richiesta viene inviata. Provala qui sotto.', - demo_zone: 'Zona geografica', demo_mag: 'Magnitudo stimata', demo_mag_hint: 'M >= 4.5 attiva un allerta', - demo_msg: 'Messaggio', demo_msg_ph: 'Evento critico simulato', demo_trigger: 'Genera Terremoto', - demo_historic: 'Storicamente il backend bypassa la pipeline IoT e pubblica direttamente sul canale Redis quake_alerts: l\u2019app mobile vibra e invia una notifica push in pochi millisecondi.', - demo_console_heading: 'Ingestione \u2192 pipeline di allerta', - demo_console_ready: 'Pronto: seleziona una zona e genera un terremoto per vedere la pipeline.', - demo_feed_heading: 'Eventi critici recenti (ultimi 10)', demo_ai_heading: 'Report AI di emergenza', - - arch_eyebrow: 'Dalla vibrazione all\u2019allerta', arch_title: 'Architettura del sistema', - arch_iot: 'Sensori IoT Edge', - arch_iot_p: 'ESP32-C3 (RISC-V) + accelerometro ADXL345. Pipeline DSP FreeRTOS con STA/LTA a ring-buffer, firma ECDSA su ogni payload e un sottosistema GNSS opzionale (NEO-6M/M8N) con l\u2019ultimo fix salvato in NVS.', - arch_api: 'FastAPI Cloud', - arch_api_p: 'Gateway asincrono Python 3.11 con rate limiting a finestra scorrevole, autenticazione con API key, verifica ECDSA e anti-replay.', - arch_redis: 'Redis Streams + Pub/Sub', - arch_redis_p: 'Disaccoppia l\u2019ingestione dall\u2019elaborazione, con offloading su Redis Streams, lock di dedup per area, Dead Letter Queue sui guasti e una ai_report_queue dedicata per ai_report_worker.py, disaccoppiato dal motore degli alert.', - arch_mqtt: 'Data plane MQTT', - arch_mqtt_p: 'Broker HiveMQ Cloud su TLS. Un bridge dedicato inoltra la telemetria alla pipeline HTTP sicura.', - arch_pg: 'PostgreSQL + TimescaleDB + PostGIS', - arch_pg_p: 'Persistenza time-series e spaziale: i dati dei sensori finiscono in un hypertable TimescaleDB; le zone si risolvono tramite l\u2019indice geohash fast-path su Redis, con PostGIS ST_Contains come fallback autorevole.', - arch_app: 'App React Native', - arch_app_p: 'App a tre tab (Monitoraggio, Mappa sensori, Impostazioni). Sismografo live per zona con strip orizzontale, GPS "Rileva la mia zona" via GET /zones/locate, alert WebSocket con vibrazione SOS e push, sirena audio (expo-audio), doppio tema MIC/RESEARCH, modalità offline, cronologia degli ultimi 10 eventi e card dei Report AI.', - - pipe_1_t: 'Rileva', pipe_1_d: 'ADXL345 a 100 Hz → HPF elimina la gravità → la soglia STA/LTA > 1.8 segnala un evento.', - pipe_2_t: 'Firma', pipe_2_d: 'Il firmware firma il payload con ECDSA NIST P-256 — la chiave privata non lascia mai la NVS del dispositivo.', - pipe_3_t: 'Pubblica', pipe_3_d: 'Pubblicazione MQTT su quakeguard/telemetry su HiveMQ Cloud (TLS, porta 8883).', - pipe_4_t: 'Verifica', pipe_4_d: 'Bridge → POST /readings/. API key, stato del sensore, anti-replay (60 s) e firma vengono tutti validati.', - pipe_5_t: 'Coda', pipe_5_d: 'Il rate limit (50 req/s per IP) passa → l\u2019evento viene accodato su Redis Streams (readings:stream); l\u2019API risponde 202 istantaneamente.', - pipe_6_t: 'Elabora', pipe_6_d: 'Il worker stima M = log10(PGA × 1.6) + 3.0; se M ≥ 4,5 viene attivato un Alert.', - pipe_7_t: 'Salva & dedup', pipe_7_d: 'Salvato atomicamente su TimescaleDB (hypertable) + PostGIS; un cooldown Redis per area (regione geohash o zona) previene le tempeste di alert (pattern outbox).', - pipe_8_t: 'Allerta', pipe_8_d: 'Broadcast WebSocket → vibrazione aptica SOS + notifica push ad alta priorità sul mobile, consegna in meno di un secondo.', - pipe_9_t: 'Report AI', pipe_9_d: 'ai_report_queue su Redis → worker → Ollama genera il report di emergenza on-premise; persistito e inviato sul canale WebSocket ai_reports.', - - dp_1: 'Lettura catturata a 100 Hz (ADXL345 ±16G)', - dp_2: 'STA/LTA = 2.11 > 1.8 → evento sismico', - dp_3: 'Payload firmato con ECDSA NIST P-256', - dp_4: 'Pubblicazione MQTT → quakeguard/telemetry (HiveMQ TLS)', - dp_5: 'Bridge → POST /readings/ → firma & replay verificati', - dp_6: 'Rate limit OK → Redis Streams (202 Accepted)', - dp_7: 'Worker: Magnitudo stimata (M = log10(PGA × 1.6) + 3.0)', - dp_8: 'Cooldown per area acquisito (dedup outbox)', - dp_9: 'Assegnazione zona PostGIS → broadcast WebSocket', - dp_10: 'Mobile: vibrazione SOS + notifica push', - dp_11: 'Report AI: ai_report_queue → Ollama → report sul canale WS ai_reports', - - res_eyebrow: 'Pensato per la folla', res_title: 'La Mandria Tonante', - res_card_title: 'Come viene assorbito il picco', - res_lead1: 'I terremoti non bussano a una sola porta. Quando centinaia di sensori nella stessa zona rilevano lo stesso evento nello stesso istante, il backend viene colpito da un picco di traffico massiccio e sincrono.', - res_lead2: 'QuakeGuard è progettato per quel momento: l\u2019ingestione è disaccoppiata dall\u2019elaborazione, le richieste rispondono 202 in millisecondi, il connection pooling assorbe il carico e un cooldown Redis per area garantisce una sola allerta pulita — non una tempesta di duplicati.', - res_queue_t: 'Coda producer-consumer', res_queue_p: 'L\u2019ingestione non aspetta mai il database: gli eventi scaricano su Redis Streams istantaneamente.', - res_rate_t: 'Rate limit control-plane', res_rate_p: '50 req/s per IP, blocca presto i nodi difettosi o malevoli.', - res_pool_t: 'Connection pooling', res_pool_p: 'Pool SQLAlchemy aggressivo per alta concorrenza.', - res_cooldown_t: 'Cooldown per area', res_cooldown_p: 'Lock di dedup per regione geohash o zona; solo il worker vincitore pubblica l\u2019Alert (pattern outbox).', - - sec_eyebrow: 'Zero-Trust Edge', sec_title: 'Sicurezza Crittografica', - sec_lead: 'L\u2019integrità dei dati è fondamentale in un sistema di emergenza. Ogni pacchetto di telemetria è protetto crittograficamente end-to-end contro spoofing, replay e manomissione.', - sec_gates_title: 'Le 4 porte di validazione', - sec_gate_key_t: 'API key', sec_gate_key_p: 'Confronto a tempo costante per resistere agli attacchi temporali.', - sec_gate_status_t: 'Stato del sensore', sec_gate_status_p: 'Verifica che il sensore esista e sia attivo.', - sec_gate_replay_t: 'Anti-replay', sec_gate_replay_p: 'Timestamp più vecchi di 60s respinti con 403.', - sec_gate_sig_t: 'Firma ECDSA', sec_gate_sig_p: 'Payload verificato contro la chiave pubblica del dispositivo (DER e r||s).', - sec_identity_title: 'Identità e copertura delle minacce', - sec_identity_p: 'Ogni ESP32-C3 genera la propria coppia di chiavi NIST P-256 ECDSA; la chiave privata è sigillata e non lascia mai il dispositivo. La chiave pubblica diventa l\u2019identità inamovibile durante /devices/register.', - sec_mitm: 'MitM protetto', sec_spoof: 'Spoofing bloccato', sec_replay: 'Replay bloccato', - sec_brute: 'Brute force limitata', sec_unauth: 'Accesso non autorizzato bloccato (fail-fast)', - - zones_eyebrow: 'Intelligenza spaziale', zones_title: 'Zone Geografiche', - zones_lead: '8 regioni globali pre-caricate: la risoluzione delle zone usa il fast path geohash su Redis, con PostGIS ST_Contains come fallback autorevole e il fallback Unknown Region per le coordinate non mappate. L\u2019app mobile può rilevare la tua zona con un tocco tramite GET /zones/locate.', - zones_unknown_sub: 'Fallback per coordinate non mappate', - zone_1: 'Italia - Nord', zone_2: 'Italia - Centro', zone_3: 'Italia - Sud & Isole', - zone_4: 'Europa Occidentale', zone_5: 'Nord America', zone_6: 'Sud America', - zone_7: 'Asia Orientale', zone_8: 'Regione Sconosciuta', - - roadmap_eyebrow: 'Dove sta andando', roadmap: 'Roadmap', - road_1_label: 'v1.0 · Rilasciata', road_1: 'Rilevazione sismica edge su ESP32-C3 e allarmi locali.', - road_11_label: 'v1.1 · Rilasciata', road_11: 'HiveMQ Cloud MQTT (TLS), tunnel HTTPS ngrok, hardening della sicurezza.', - road_12_label: 'v1.2.1 · Attuale', road_12: 'Geo-Zoning & GNSS-Ready: ingestione Redis Streams, persistenza TimescaleDB, cooldown di dedup per area e report AI di emergenza on-premise.', - road_122_label: 'v1.2.2 · Pulizia', road_122: 'Pulizia SonarCloud e fix del quality gate (PR #368).', - road_13_label: 'v1.3 · GNSS (in corso)', road_13: 'Modulo GNSS già pronto (NEO-6M/M8N, ultimo fix in NVS); sincronizzazione NTP + PPS ancora da fare.', - road_20_label: 'v2.0 · Triangolazione', road_20: 'Correlazione spaziale multi-nodo + report AI per il calcolo dell\u2019epicentro.', - road_21_label: 'v2.1 · Dashboard', road_21: 'Grafana: visualizzazione live della telemetria sismica.', - health_eyebrow: 'Rigore ingegneristico', health_title: 'Stato del progetto', - health_cd_title: 'CI/CD', health_cd_p: 'Cinque pipeline automatiche verificano ogni modifica:', - health_stress_title: 'Stress test', - health_stress_p: 'Valida l\u2019intera pipeline di ingestione contro un evento massivo simulato.', - health_stress_cert: 'SISTEMA CERTIFICATO', - health_docs_title: 'Documentazione approfondita', - health_docs_p: 'Un whitepaper compilato con Typst illustra architettura, hardware, sicurezza, backend, mobile e deployment.', - health_docs_os: 'Open source sotto AGPL-3.0, con DOI, guida ai contributi e security policy.', - health_docs_read: 'Leggi il whitepaper', health_docs_star: 'Metti una stella su GitHub', - - exp_eyebrow: 'Vedilo in azione', exp_title: 'L\u2019esperienza', - exp_note: 'Guarda la demo con i sottotitoli disponibili in italiano e inglese.', - - contact_title: 'Contribuisci o collabora', - contact_p: 'QuakeGuard è open source e guidato dalla community: idee, segnalazioni e pull request sono sempre benvenute.', - contact_open: 'Apri il repository', contact_email: 'Scrivimi', - footer_ttl: 'Contattaci', footer_email: 'Email', - mentors_eyebrow: 'Traguardo', mentors_title: 'Mentorship e riconoscimenti', - mentors_p1: 'Sviluppato come progetto scolastico per Hackersgen di Sorint.lab e per la competizione GF Marilli — in collaborazione con riccardo0731.', - mentors_p2: 'Abbiamo avuto l’onore di lavorare con Francesco Finazzi — professore UniBG e fondatore di Earthquake Network, la più grande rete sismica comunitaria al mondo. Dopo la conferenza, la nostra collaborazione con lui è esattamente il punto da cui è iniziata la storia del progetto.', - mentor_1: 'Sorint.lab — Hackersgen', mentor_2: 'Università di Bergamo', - contact_eyebrow: 'Contattaci', - footer_tagline: 'Studente di Computer Science (BSc) presso UniPi | Specializzato in Edge Computing, Cloud Architecture e IoT.', - video_fallback: 'Il tuo browser non supporta il tag video.' - } +/* CPD-safe form: each key stores its {en, it} pair once, then the dict is exposed + * to the i18n engine as I18N_DATA.en / I18N_DATA.it (same interface as before). */ +window.I18N_DATA = (function () { + var T = { + navbar_sections: { en: 'Sections', it: 'Sezioni' }, + navbar_demo: { en: 'Live Demo', it: 'Demo' }, + navbar_mission: { en: 'Mission', it: 'Missione' }, + navbar_architecture: { en: 'Architecture', it: 'Architettura' }, + navbar_resilience: { en: 'Resilience', it: 'Resilienza' }, + navbar_security: { en: 'Security', it: 'Sicurezza' }, + navbar_zones: { en: 'Geographic Zones', it: 'Zone Geografiche' }, + navbar_roadmap: { en: 'Roadmap', it: 'Roadmap' }, + navbar_health: { en: 'Project Health', it: 'Stato del progetto' }, + navbar_mentors: { en: 'Mentors', it: 'Mentor' }, + navbar_github: { en: 'GitHub', it: 'GitHub' }, + navbar_home: { en: 'Home', it: 'Home' }, + hero_sub: { en: 'Electro-Domestic Seismic Alarm System', it: 'Sistema di allarme sismico elettro-domestico' }, + hero_lead: { en: 'A full-stack IoT architecture for real-time detection, analysis and reporting of seismic events. Everyday household appliances — washing machines, TVs, refrigerators — become a distributed earthquake early-warning network that alerts you before the shaking arrives. v1.2.1 Geo-Zoning & GNSS-Ready — Redis Streams ingestion, per-area cooldown, TimescaleDB persistence and on-premise AI emergency reports with Ollama — privacy-first, seismic telemetry never leaves the machine.', it: 'Un\u2019architettura IoT full-stack per la rilevazione, l\u2019analisi e il reporting in tempo reale di eventi sismici. Gli elettrodomestici di tutti i giorni — lavatrici, TV, frigoriferi — diventano una rete distribuita di early warning che ti avvisa prima dell\u2019arrivo della scossa. v1.2.1 Geo-Zoning & GNSS-Ready — ingestione Redis Streams, cooldown per area, persistenza TimescaleDB e report AI di emergenza on-premise con Ollama — privacy-first, la telemetria non lascia mai la macchina.' }, + hero_trigger: { en: 'Trigger an earthquake', it: 'Genera un terremoto' }, + hero_github: { en: 'View on GitHub', it: 'Vedi su GitHub' }, + hero_whitepaper: { en: 'Whitepaper', it: 'Whitepaper' }, + hero_wiki: { en: 'Wiki', it: 'Wiki' }, + mission_eyebrow: { en: 'Mission', it: 'Missione' }, + mission_title: { en: 'Detection for everyone', it: 'Rilevazione per tutti' }, + mission_lead: { en: 'We want to make earthquake detection available all around the world, with zero cost for the people and low cost for the enterprises. By embedding miniature IoT sensors inside the objects we already live with, we can grant P-wave detection without anyone even knowing — and give people time to evacuate or reach a safe location before it is too late.', it: 'Vogliamo rendere la rilevazione dei terremoti disponibile in tutto il mondo, con costo zero per le persone e costo contenuto per le aziende. Grazie ai mini-sensori IoT integrati negli oggetti di uso quotidiano possiamo garantire la rilevazione delle onde P senza che nessuno se ne accorga, e dare alle persone il tempo di evacuare o mettersi in salvo prima che sia troppo tardi.' }, + stat_sample: { en: 'Edge sampling', it: 'Campionamento edge' }, + stat_mag: { en: 'Magnitude threshold', it: 'Soglia di magnitudo' }, + stat_dedup: { en: 'Per-area dedup / anti-replay', it: 'Dedup per area / anti-replay' }, + stat_rate: { en: 'Rate limit per IP', it: 'Limite richieste per IP' }, + demo_eyebrow: { en: 'Live Demo', it: 'Live Demo' }, + demo_title: { en: 'Trigger an earthquake', it: 'Genera un terremoto' }, + demo_sub: { en: 'A faithful, client-only echo of the backend flow: pick one of the 8 geographic zones, set a magnitude >= 4.5 and watch the pipeline run from edge to mobile alert + AI report.', it: 'Un eco fedele, solo client-side, del flusso del backend: scegli una delle 8 zone geografiche, imposta una magnitudo e guarda la pipeline scorrere dall\u2019edge fino all\u2019allerta mobile + report AI.' }, + demo_notice: { en: 'This is a client-side simulation — everything runs in your browser, no real request is sent. Try it below.', it: 'Questa è una simulazione lato client: tutto avviene nel tuo browser, nessuna richiesta viene inviata. Provala qui sotto.' }, + demo_zone: { en: 'Geographic zone', it: 'Zona geografica' }, + demo_mag: { en: 'Estimated magnitude', it: 'Magnitudo stimata' }, + demo_mag_hint: { en: 'M >= 4.5 triggers an alert', it: 'M >= 4.5 attiva un allerta' }, + demo_msg: { en: 'Message', it: 'Messaggio' }, + demo_msg_ph: { en: 'Simulated Critical Event', it: 'Evento critico simulato' }, + demo_trigger: { en: 'Trigger Earthquake', it: 'Genera Terremoto' }, + demo_historic: { en: 'Historically the backend bypasses the IoT pipeline and pushes straight to the Redis quake_alerts channel — the mobile app vibrates and fires a push notification within milliseconds.', it: 'Storicamente il backend bypassa la pipeline IoT e pubblica direttamente sul canale Redis quake_alerts: l\u2019app mobile vibra e invia una notifica push in pochi millisecondi.' }, + demo_console_heading: { en: 'Ingestion → Alert pipeline', it: 'Ingestione \u2192 pipeline di allerta' }, + demo_console_ready: { en: 'Ready — select a zone and trigger an earthquake to watch the pipeline.', it: 'Pronto: seleziona una zona e genera un terremoto per vedere la pipeline.' }, + demo_feed_heading: { en: 'Recent critical events (last 10)', it: 'Eventi critici recenti (ultimi 10)' }, + demo_ai_heading: { en: 'AI Emergency Report', it: 'Report AI di emergenza' }, + arch_eyebrow: { en: 'From vibration to alert', it: 'Dalla vibrazione all\u2019allerta' }, + arch_title: { en: 'System Architecture', it: 'Architettura del sistema' }, + arch_iot: { en: 'IoT Edge Sensors', it: 'Sensori IoT Edge' }, + arch_iot_p: { en: 'ESP32-C3 (RISC-V) + ADXL345 accelerometer. Runs a FreeRTOS DSP pipeline with ring-buffer STA/LTA, ECDSA signing on every payload and an optional GNSS subsystem (NEO-6M/M8N) with NVS last-known-fix.', it: 'ESP32-C3 (RISC-V) + accelerometro ADXL345. Pipeline DSP FreeRTOS con STA/LTA a ring-buffer, firma ECDSA su ogni payload e un sottosistema GNSS opzionale (NEO-6M/M8N) con l\u2019ultimo fix salvato in NVS.' }, + arch_api: { en: 'FastAPI Cloud', it: 'FastAPI Cloud' }, + arch_api_p: { en: 'Fully async Python 3.11 gateway. Sliding-window rate limiting, API-key auth, ECDSA verification and anti-replay.', it: 'Gateway asincrono Python 3.11 con rate limiting a finestra scorrevole, autenticazione con API key, verifica ECDSA e anti-replay.' }, + arch_redis: { en: 'Redis Streams + Pub/Sub', it: 'Redis Streams + Pub/Sub' }, + arch_redis_p: { en: 'Decouples ingestion from processing. Redis Streams offloading, per-area dedup locks, a Dead Letter Queue on failure, plus a dedicated ai_report_queue for ai_report_worker.py decoupled from the alert engine.', it: 'Disaccoppia l\u2019ingestione dall\u2019elaborazione, con offloading su Redis Streams, lock di dedup per area, Dead Letter Queue sui guasti e una ai_report_queue dedicata per ai_report_worker.py, disaccoppiato dal motore degli alert.' }, + arch_mqtt: { en: 'MQTT Data Plane', it: 'Data plane MQTT' }, + arch_mqtt_p: { en: 'HiveMQ Cloud broker over TLS. A dedicated bridge microservice forwards telemetry to the secure HTTP pipeline.', it: 'Broker HiveMQ Cloud su TLS. Un bridge dedicato inoltra la telemetria alla pipeline HTTP sicura.' }, + arch_pg: { en: 'PostgreSQL + TimescaleDB + PostGIS', it: 'PostgreSQL + TimescaleDB + PostGIS' }, + arch_pg_p: { en: 'Time-series + spatial persistence. Sensor data lands in a TimescaleDB hypertable; zones resolve through a geohash fast-path Redis index, with PostGIS ST_Contains as the authoritative fallback.', it: 'Persistenza time-series e spaziale: i dati dei sensori finiscono in un hypertable TimescaleDB; le zone si risolvono tramite l\u2019indice geohash fast-path su Redis, con PostGIS ST_Contains come fallback autorevole.' }, + arch_app: { en: 'React Native App', it: 'App React Native' }, + arch_app_p: { en: 'Three-tab app (Monitor, Sensor Map, Settings). Per-zone live seismograph with a horizontal zone strip, GPS "Detect my zone" via GET /zones/locate, WebSocket alerts with SOS haptics & push, siren audio (expo-audio), MIC/RESEARCH dual theme, offline mode, last-10 alert history and AI report cards.', it: 'App a tre tab (Monitoraggio, Mappa sensori, Impostazioni). Sismografo live per zona con strip orizzontale, GPS "Rileva la mia zona" via GET /zones/locate, alert WebSocket con vibrazione SOS e push, sirena audio (expo-audio), doppio tema MIC/RESEARCH, modalità offline, cronologia degli ultimi 10 eventi e card dei Report AI.' }, + pipe_1_t: { en: 'Detect', it: 'Rileva' }, + pipe_1_d: { en: 'ADXL345 at 100 Hz → HPF removes gravity → STA/LTA ratio > 1.8 flags an event.', it: 'ADXL345 a 100 Hz → HPF elimina la gravità → la soglia STA/LTA > 1.8 segnala un evento.' }, + pipe_2_t: { en: 'Sign', it: 'Firma' }, + pipe_2_d: { en: 'Firmware signs the payload with ECDSA NIST P-256 — the private key never leaves the device NVS.', it: 'Il firmware firma il payload con ECDSA NIST P-256 — la chiave privata non lascia mai la NVS del dispositivo.' }, + pipe_3_t: { en: 'Publish', it: 'Pubblica' }, + pipe_3_d: { en: 'MQTT publish to quakeguard/telemetry on HiveMQ Cloud (TLS, port 8883).', it: 'Pubblicazione MQTT su quakeguard/telemetry su HiveMQ Cloud (TLS, porta 8883).' }, + pipe_4_t: { en: 'Verify', it: 'Verifica' }, + pipe_4_d: { en: 'Bridge → POST /readings/. API key, sensor status, anti-replay (60 s) and signature are all validated.', it: 'Bridge → POST /readings/. API key, stato del sensore, anti-replay (60 s) e firma vengono tutti validati.' }, + pipe_5_t: { en: 'Queue', it: 'Coda' }, + pipe_5_d: { en: 'Rate limit (50 req/s per IP) passes → event appended to Redis Streams (readings:stream); the API replies 202 instantly.', it: 'Il rate limit (50 req/s per IP) passa → l\u2019evento viene accodato su Redis Streams (readings:stream); l\u2019API risponde 202 istantaneamente.' }, + pipe_6_t: { en: 'Process', it: 'Elabora' }, + pipe_6_d: { en: 'Worker estimates M = log10(PGA × 1.6) + 3.0; if M ≥ 4.5 an Alert is triggered.', it: 'Il worker stima M = log10(PGA × 1.6) + 3.0; se M ≥ 4,5 viene attivato un Alert.' }, + pipe_7_t: { en: 'Persist & dedup', it: 'Salva & dedup' }, + pipe_7_d: { en: 'Atomically saved to TimescaleDB (hypertable) + PostGIS; a per-area Redis cooldown (geohash region or zone) prevents alert storms (outbox pattern).', it: 'Salvato atomicamente su TimescaleDB (hypertable) + PostGIS; un cooldown Redis per area (regione geohash o zona) previene le tempeste di alert (pattern outbox).' }, + pipe_8_t: { en: 'Alert', it: 'Allerta' }, + pipe_8_d: { en: 'WebSocket broadcast → mobile SOS haptic vibration + high-priority push notification, sub-second delivery.', it: 'Broadcast WebSocket → vibrazione aptica SOS + notifica push ad alta priorità sul mobile, consegna in meno di un secondo.' }, + pipe_9_t: { en: 'AI Report', it: 'Report AI' }, + pipe_9_d: { en: 'ai_report_queue in Redis → worker → Ollama generates the emergency report on-premise; persisted and pushed on the ai_reports WebSocket channel.', it: 'ai_report_queue su Redis → worker → Ollama genera il report di emergenza on-premise; persistito e inviato sul canale WebSocket ai_reports.' }, + dp_1: { en: 'Reading captured at 100 Hz (ADXL345 ±16G)', it: 'Lettura catturata a 100 Hz (ADXL345 ±16G)' }, + dp_2: { en: 'STA/LTA = 2.11 > 1.8 → seismic event', it: 'STA/LTA = 2.11 > 1.8 → evento sismico' }, + dp_3: { en: 'Payload signed with ECDSA NIST P-256', it: 'Payload firmato con ECDSA NIST P-256' }, + dp_4: { en: 'MQTT publish → quakeguard/telemetry (HiveMQ TLS)', it: 'Pubblicazione MQTT → quakeguard/telemetry (HiveMQ TLS)' }, + dp_5: { en: 'Bridge → POST /readings/ → signature & replay verified', it: 'Bridge → POST /readings/ → firma & replay verificati' }, + dp_6: { en: 'Rate limit OK → Redis Streams (202 Accepted)', it: 'Rate limit OK → Redis Streams (202 Accepted)' }, + dp_7: { en: 'Worker: Magnitude estimated (M = log10(PGA × 1.6) + 3.0)', it: 'Worker: Magnitudo stimata (M = log10(PGA × 1.6) + 3.0)' }, + dp_8: { en: 'Per-area cooldown acquired (dedup outbox)', it: 'Cooldown per area acquisito (dedup outbox)' }, + dp_9: { en: 'PostGIS zone assign → WebSocket broadcast', it: 'Assegnazione zona PostGIS → broadcast WebSocket' }, + dp_10: { en: 'Mobile: SOS haptic + push notification', it: 'Mobile: vibrazione SOS + notifica push' }, + dp_11: { en: 'AI Report: ai_report_queue → Ollama → report on ai_reports WS channel', it: 'Report AI: ai_report_queue → Ollama → report sul canale WS ai_reports' }, + res_eyebrow: { en: 'Built for the crowd', it: 'Pensato per la folla' }, + res_title: { en: 'The Thundering Herd', it: 'La Mandria Tonante' }, + res_card_title: { en: 'How the spike is absorbed', it: 'Come viene assorbito il picco' }, + res_lead1: { en: 'Earthquakes do not knock on one door. When hundreds of sensors in the same zone detect the same event at once, the backend is hit by a massive synchronous traffic spike.', it: 'I terremoti non bussano a una sola porta. Quando centinaia di sensori nella stessa zona rilevano lo stesso evento nello stesso istante, il backend viene colpito da un picco di traffico massiccio e sincrono.' }, + res_lead2: { en: 'QuakeGuard is engineered for exactly that moment: ingestion is decoupled from processing, requests return 202 in milliseconds, connection pooling absorbs the load, and a per-area Redis cooldown fires one clean alert — not a storm of duplicates.', it: 'QuakeGuard è progettato per quel momento: l\u2019ingestione è disaccoppiata dall\u2019elaborazione, le richieste rispondono 202 in millisecondi, il connection pooling assorbe il carico e un cooldown Redis per area garantisce una sola allerta pulita — non una tempesta di duplicati.' }, + res_queue_t: { en: 'Producer-consumer queue', it: 'Coda producer-consumer' }, + res_queue_p: { en: 'ingestion never waits on the database; events offload to Redis Streams instantly.', it: 'L\u2019ingestione non aspetta mai il database: gli eventi scaricano su Redis Streams istantaneamente.' }, + res_rate_t: { en: 'Control-plane rate limit', it: 'Rate limit control-plane' }, + res_rate_p: { en: '50 req/s per IP, backs off buggy nodes early.', it: '50 req/s per IP, blocca presto i nodi difettosi o malevoli.' }, + res_pool_t: { en: 'Connection pooling', it: 'Connection pooling' }, + res_pool_p: { en: 'Aggressive SQLAlchemy pool for high concurrency.', it: 'Pool SQLAlchemy aggressivo per alta concorrenza.' }, + res_cooldown_t: { en: 'Per-area cooldown', it: 'Cooldown per area' }, + res_cooldown_p: { en: 'Dedup lock per geohash region or zone; only the winning worker publishes the Alert (outbox pattern).', it: 'Lock di dedup per regione geohash o zona; solo il worker vincitore pubblica l\u2019Alert (pattern outbox).' }, + sec_eyebrow: { en: 'Zero-Trust Edge', it: 'Zero-Trust Edge' }, + sec_title: { en: 'Cryptographic Security', it: 'Sicurezza Crittografica' }, + sec_lead: { en: 'Data integrity is paramount in an emergency system. Every telemetry packet is cryptographically secured end-to-end against spoofing, replay and tampering.', it: 'L\u2019integrità dei dati è fondamentale in un sistema di emergenza. Ogni pacchetto di telemetria è protetto crittograficamente end-to-end contro spoofing, replay e manomissione.' }, + sec_gates_title: { en: 'The 4 validation gates', it: 'Le 4 porte di validazione' }, + sec_gate_key_t: { en: 'API key', it: 'API key' }, + sec_gate_key_p: { en: 'Constant-time comparison to resist timing attacks.', it: 'Confronto a tempo costante per resistere agli attacchi temporali.' }, + sec_gate_status_t: { en: 'Sensor status', it: 'Stato del sensore' }, + sec_gate_status_p: { en: 'Confirms the sensor exists and is active.', it: 'Verifica che il sensore esista e sia attivo.' }, + sec_gate_replay_t: { en: 'Anti-replay', it: 'Anti-replay' }, + sec_gate_replay_p: { en: 'Timestamps older than 60 s are rejected with 403.', it: 'Timestamp più vecchi di 60s respinti con 403.' }, + sec_gate_sig_t: { en: 'ECDSA signature', it: 'Firma ECDSA' }, + sec_gate_sig_p: { en: 'Payload verified against the device public key (DER and r||s).', it: 'Payload verificato contro la chiave pubblica del dispositivo (DER e r||s).' }, + sec_identity_title: { en: 'Identity & threat coverage', it: 'Identità e copertura delle minacce' }, + sec_identity_p: { en: 'Each ESP32-C3 generates its own NIST P-256 ECDSA keypair; the private key is sealed and never leaves the device. The public key becomes the commitment identity during /devices/register.', it: 'Ogni ESP32-C3 genera la propria coppia di chiavi NIST P-256 ECDSA; la chiave privata è sigillata e non lascia mai il dispositivo. La chiave pubblica diventa l\u2019identità inamovibile durante /devices/register.' }, + sec_mitm: { en: 'MitM protected', it: 'MitM protetto' }, + sec_spoof: { en: 'Spoofing blocked', it: 'Spoofing bloccato' }, + sec_replay: { en: 'Replay blocked', it: 'Replay bloccato' }, + sec_brute: { en: 'Brute force limited', it: 'Brute force limitata' }, + sec_unauth: { en: 'Unauthorized access blocked (fail-fast)', it: 'Accesso non autorizzato bloccato (fail-fast)' }, + zones_eyebrow: { en: 'Spatial intelligence', it: 'Intelligenza spaziale' }, + zones_title: { en: 'Geographic Zones', it: 'Zone Geografiche' }, + zones_lead: { en: '8 global regions are pre-seeded: zone resolution uses a geohash Redis fast path, with PostGIS ST_Contains as the authoritative fallback and an Unknown Region fallback for unmapped coordinates. The mobile app can detect your zone with one tap via GET /zones/locate.', it: '8 regioni globali pre-caricate: la risoluzione delle zone usa il fast path geohash su Redis, con PostGIS ST_Contains come fallback autorevole e il fallback Unknown Region per le coordinate non mappate. L\u2019app mobile può rilevare la tua zona con un tocco tramite GET /zones/locate.' }, + zones_unknown_sub: { en: 'Fallback for unmapped coordinates', it: 'Fallback per coordinate non mappate' }, + zone_1: { en: 'Italy - North', it: 'Italia - Nord' }, + zone_2: { en: 'Italy - Center', it: 'Italia - Centro' }, + zone_3: { en: 'Italy - South & Islands', it: 'Italia - Sud & Isole' }, + zone_4: { en: 'Western Europe', it: 'Europa Occidentale' }, + zone_5: { en: 'North America', it: 'Nord America' }, + zone_6: { en: 'South America', it: 'Sud America' }, + zone_7: { en: 'East Asia', it: 'Asia Orientale' }, + zone_8: { en: 'Unknown Region', it: 'Regione Sconosciuta' }, + roadmap_eyebrow: { en: 'Where it is going', it: 'Dove sta andando' }, + roadmap: { en: 'Roadmap', it: 'Roadmap' }, + road_1_label: { en: 'v1.0 · Released', it: 'v1.0 · Rilasciata' }, + road_1: { en: 'Edge seismic detection on ESP32-C3 and local alerts.', it: 'Rilevazione sismica edge su ESP32-C3 e allarmi locali.' }, + road_11_label: { en: 'v1.1 · Released', it: 'v1.1 · Rilasciata' }, + road_11: { en: 'HiveMQ Cloud MQTT (TLS), ngrok HTTPS tunnel, security hardening.', it: 'HiveMQ Cloud MQTT (TLS), tunnel HTTPS ngrok, hardening della sicurezza.' }, + road_120_label: { en: 'v1.2.0 · Released', it: 'v1.2.0 · Rilasciata' }, + road_120: { en: 'On-Premise AI Reports — Ollama generates emergency reports on-premise, pushed over WebSocket.', it: 'Report AI On-Premise — Ollama genera i report di emergenza on-premise, inviati via WebSocket.' }, + road_12_label: { en: 'v1.2.1 · Current', it: 'v1.2.1 · Attuale' }, + road_12: { en: 'Geo-Zoning & Cooldown Fragmentation (GNSS-ready) — geohash Redis fast-path zone resolution, per-area cooldown locks, PostGIS ST_Contains as source of truth.', it: 'Geo-Zoning e frammentazione del cooldown (GNSS-ready) — risoluzione zona via indice geohash Redis fast-path, lock di cooldown per area, PostGIS ST_Contains come fonte di verità.' }, + road_122_label: { en: 'v1.2.2 · Serial fallback', it: 'v1.2.2 · Fallback seriale' }, + road_122: { en: 'Zero-Trust Serial Fallback — ECDSA-signed telemetry over USB CDC when MQTT/WiFi is unreachable.', it: 'Zero-Trust Serial Fallback — telemetria firmata ECDSA su USB CDC quando MQTT/WiFi non è raggiungibile.' }, + road_13_label: { en: 'v1.3 · Synchronized GNSS', it: 'v1.3 · GNSS sincronizzato' }, + road_13: { en: 'Optional GNSS module (NEO-6M/M8N), NTP + PPS timestamps, real coordinates replacing the hardcoded fix, ADXL345 calibration.', it: 'Modulo GNSS opzionale (NEO-6M/M8N), timestamp NTP + PPS, coordinate reali al posto del fix hardcoded, calibrazione ADXL345.' }, + road_20_label: { en: 'v2.0 · Triangulation', it: 'v2.0 · Triangolazione' }, + road_20: { en: 'Multi-node spatial correlation + AI reports for epicenter calculation.', it: 'Correlazione spaziale multi-nodo + report AI per il calcolo dell\u2019epicentro.' }, + road_21_label: { en: 'v2.1 · Dashboards', it: 'v2.1 · Dashboard' }, + road_21: { en: 'Grafana live visualization of seismic telemetry.', it: 'Grafana: visualizzazione live della telemetria sismica.' }, + roadmap_details: { en: 'See the detailed roadmap', it: 'Vedi la roadmap dettagliata' }, + roadmap_dev: { en: 'Development', it: 'Sviluppo' }, + roadmap_res: { en: 'Research', it: 'Ricerca' }, + r1_label: { en: 'R1 · Done', it: 'R1 · Completata' }, + r1: { en: 'SIL STA/LTA cross-validation — pure C++ core, host orchestrator, metrics and trigger calibration on the synthetic fallback. Real ESM ground-truth validation remains.', it: 'Cross-validazione SIL STA/LTA — core C++ puro, orchestrator host, metriche e calibrazione dei trigger sul fallback sintetico. Resta la validazione su dati reali ESM.' }, + r2_label: { en: 'R2 · Next', it: 'R2 · Prossima' }, + r2: { en: 'AI benchmarking — P50/P99 latency of the local Ollama worker, hallucination rate, privacy/latency vs cloud baseline.', it: 'Benchmark AI — latenza P50/P99 del worker Ollama locale, tasso di allucinazione, privacy/latenza rispetto al cloud.' }, + r3_label: { en: 'R3 · Planned', it: 'R3 · Pianificata' }, + r3: { en: 'Dissemination — open validation dataset (Zenodo DOI) + technical paper / preprint.', it: 'Disseminazione — dataset di validazione aperto (DOI Zenodo) + paper tecnico / preprint.' }, + roadmap_res_note: { en: 'R1 is the blocking prerequisite for the v2.2.0 Edge AI tier and runs in parallel with v1.3 (GNSS).', it: 'R1 è il prerequisito bloccante del tier Edge AI v2.2.0 e corre in parallelo con v1.3 (GNSS).' }, + health_eyebrow: { en: 'Engineering rigor', it: 'Rigore ingegneristico' }, + health_title: { en: 'Project Health', it: 'Stato del progetto' }, + health_cd_title: { en: 'CI/CD', it: 'CI/CD' }, + health_cd_p: { en: 'Five automated pipelines gate every change:', it: 'Cinque pipeline automatiche verificano ogni modifica:' }, + health_stress_title: { en: 'Stress test', it: 'Stress test' }, + health_stress_p: { en: 'Validates the full ingestion pipeline against a simulated massive event.', it: 'Valida l\u2019intera pipeline di ingestione contro un evento massivo simulato.' }, + health_stress_cert: { en: 'SYSTEM CERTIFIED', it: 'SISTEMA CERTIFICATO' }, + health_docs_title: { en: 'Deep documentation', it: 'Documentazione approfondita' }, + health_docs_p: { en: 'A Typst-compiled whitepaper walks through architecture, hardware, security, backend, mobile and deployment.', it: 'Un whitepaper compilato con Typst illustra architettura, hardware, sicurezza, backend, mobile e deployment.' }, + health_docs_os: { en: 'Open source under AGPL-3.0, with a DOI, contributing guide and security policy.', it: 'Open source sotto AGPL-3.0, con DOI, guida ai contributi e security policy.' }, + health_docs_read: { en: 'Read the whitepaper', it: 'Leggi il whitepaper' }, + health_docs_star: { en: 'Star on GitHub', it: 'Metti una stella su GitHub' }, + exp_eyebrow: { en: 'See it in action', it: 'Vedilo in azione' }, + exp_title: { en: 'The experience', it: 'L\u2019esperienza' }, + exp_note: { en: 'Watch the demo with captions available in English and Italian.', it: 'Guarda la demo con i sottotitoli disponibili in italiano e inglese.' }, + contact_title: { en: 'Contribute or collaborate', it: 'Contribuisci o collabora' }, + contact_p: { en: 'QuakeGuard is open source and community-driven. Ideas, issues and pull requests are always welcome.', it: 'QuakeGuard è open source e guidato dalla community: idee, segnalazioni e pull request sono sempre benvenute.' }, + contact_open: { en: 'Open the repository', it: 'Apri il repository' }, + contact_email: { en: 'Email me', it: 'Scrivimi' }, + footer_ttl: { en: 'Get in touch', it: 'Contattaci' }, + footer_email: { en: 'Email Me', it: 'Email' }, + mentors_eyebrow: { en: 'Milestone', it: 'Traguardo' }, + mentors_title: { en: 'Mentorship & Recognition', it: 'Mentorship e riconoscimenti' }, + mentors_p1: { en: 'Developed as a school-contest project for Hackersgen by Sorint.lab and the GF Marilli competition — in collaboration with riccardo0731.', it: 'Sviluppato come progetto scolastico per Hackersgen di Sorint.lab e per la competizione GF Marilli — in collaborazione con riccardo0731.' }, + mentors_p2: { en: 'We were honored to work with Francesco Finazzi — UniBG professor and founder of Earthquake Network, the largest community seismic network in the world. After the conference, our collaboration with him is exactly where the project’s story began.', it: 'Abbiamo avuto l’onore di lavorare con Francesco Finazzi — professore UniBG e fondatore di Earthquake Network, la più grande rete sismica comunitaria al mondo. Dopo la conferenza, la nostra collaborazione con lui è esattamente il punto da cui è iniziata la storia del progetto.' }, + mentor_1: { en: 'Sorint.lab — Hackersgen', it: 'Sorint.lab — Hackersgen' }, + mentor_2: { en: 'University of Bergamo', it: 'Università di Bergamo' }, + contact_eyebrow: { en: 'Get in touch', it: 'Contattaci' }, + footer_tagline: { en: 'BSc Computer Science Student at UniPi | Specialized in Edge Computing, Cloud Architecture, and IoT.', it: 'Studente di Computer Science (BSc) presso UniPi | Specializzato in Edge Computing, Cloud Architecture e IoT.' }, + video_fallback: { en: 'Your browser does not support the video tag.', it: 'Il tuo browser non supporta il tag video.' }, }; + var en = {}, it = {}; + for (var k in T) { en[k] = T[k].en; it[k] = T[k].it; } + return { en: en, it: it }; +})(); diff --git a/research/README.md b/research/README.md index 6e14b29..7f9a1da 100644 --- a/research/README.md +++ b/research/README.md @@ -6,11 +6,8 @@ code** as the ESP32 firmware (`firmware/src/DetectionCore.h`), guaranteeing numerical equivalence for the IEEE paper. ``` -┌─ fetch_itaca.py ─── download accelerograms + P-arrival ground truth -│ (ESM public FDSN API via ObsPy, no token; -│ realistic synthetic fallback otherwise) -│ -├─ synthetic.py ─────── generate a synthetic dataset (no network) for CI / smoke tests +┌─ synthetic.py ─── generate a realistic synthetic dataset (no network) for CI / smoke tests +│ (canonical fallback provider; same layout as the future ESM path) │ ├─ calibrate_io.py ── load events/ + ground_truth.json │ @@ -26,27 +23,28 @@ numerical equivalence for the IEEE paper. ## Graceful degradation (I/O contract) -`fetch_itaca.py` emits a **fixed** dataset layout. Downstream modules +`synthetic.py` emits a **fixed** dataset layout. Downstream modules (`metrics.py`, `calibrate.py`, the C++ core) never know whether they process a real earthquake or a locally generated mock — that is the point. Resolution: -- **Real path (ESM).** The script queries the **ESM (European Strong-Motion) database** - through its **public FDSN web-service** using **ObsPy** (`obspy.fdsn`), downloads the - parametric flatfiles and accelerograms, and converts them into the shared layout. - No registration token is required, so the real path works headlessly in CI/CD. This - replaces the previous ITACA DYNA-1.2 parser (custom code, token-gated portal). -- **Synthetic fallback (default).** When no network/ESM is available the script - generates *realistic* accelerometer-like mocks: white background noise, a - high-frequency P impulse, a larger/lower-frequency S arrival, and a 1 G - gravity offset (matching the firmware's `sensors_event_t`). The exact +- **Real path (ESM, to be implemented).** The planned ESM (European + Strong-Motion) downloader queries the **public FDSN web-service** using + **ObsPy** (`obspy.fdsn`), downloads the parametric flatfiles and accelerograms, + and converts them into the shared layout. No registration token is required, + so the real path can work headlessly in CI/CD. This replaces the previous + ITACA DYNA-1.2 parser (custom code, token-gated portal). +- **Synthetic fallback (default).** When no network/ESM is available, + `synthetic.py` generates *realistic* accelerometer-like mocks: white background + noise, a high-frequency P impulse, a larger/lower-frequency S arrival, and a + 1 G gravity offset (matching the firmware's `sensors_event_t`). The exact P-arrival is written to `ground_truth.json` as the reference. ``` ┌────────────────────────────────────────────────────────────────────────┐ -│ fetch_itaca.py │ -│ ESM reachable? ──yes──▶ ESM FDSN (ObsPy) → flatfiles → t,ax,ay,az │ -│ │no (m/s²) │ -│ └──────────▶ realistic synthetic generator ──── P known │ +│ synthetic.py │ +│ (fallback) ───────▶ realistic synthetic generator ──── P known │ +│ │ +│ ESM downloader (future, ObsPy public FDSN) ───▶ t,ax,ay,az (m/s²) │ └──────────────────────────────▶ (identical layout below) ────────────────┘ ``` @@ -56,11 +54,10 @@ The dataset is **not** committed to git (see `research/README.md` re: license and weight). Generate it with: ```bash -# real ESM (public FDSN API, no token required) -python research/fetch_itaca.py research/data -# or, for a realistic local / CI mock: -python research/fetch_itaca.py research/data # same command, no network -python research/synthetic.py research/data_synth +# realistic local / CI mock (no network required) +python research/synthetic.py research/data_synth +# real ESM (public FDSN API, no token) — to be implemented (R1 closure) +# python research/fetch_esm.py research/data ``` Layout produced by every path (units **m/s^2**, the same as the firmware diff --git a/research/__init__.py b/research/__init__.py index 05730d0..6890ebc 100644 --- a/research/__init__.py +++ b/research/__init__.py @@ -4,10 +4,11 @@ (R1 of the ROADMAP #Research node). Pipeline: - fetch_itaca.py -> download INGV/ITACA accelerograms + ground truth - orchestrator.py -> run the host-compiled detect_cli (same C++ as firmware) - metrics.py -> Sensitivity/Recall, False-Alarm Rate, latency, ROC - calibrate.py -> sweep TRIGGER_RATIO x NOISE_FLOOR against ground truth + synthetic.py -> generate realistic synthetic dataset (no network, CI) + + ESM real-downloader to be implemented (R1) + orchestrator.py -> run the host-compiled detect_cli (same C++ as firmware) + metrics.py -> Sensitivity/Recall, False-Alarm Rate, latency, ROC + calibrate.py -> sweep TRIGGER_RATIO x NOISE_FLOOR against ground truth """ __version__ = "0.1.0" diff --git a/research/fetch_itaca.py b/research/fetch_itaca.py deleted file mode 100644 index da96cdd..0000000 --- a/research/fetch_itaca.py +++ /dev/null @@ -1,255 +0,0 @@ -"""Download accelerograms + ground truth from the ITACA strong-motion portal. - -Graceful-degradation fetcher used as the single entry point for real-world -validation data for the SIL pipeline (ROADMAP R1). The script emits a *fixed* -dataset layout (documented in research/README.md) so downstream modules -(metrics.py, calibrate.py, the C++ core) never know whether they process a real -earthquake or a locally generated mock -- that is the I/O contract. - -Resolution strategy: - 1. If the ``ITACA_TOKEN`` environment variable is set (e.g. from a .env - file), fetch the real accelerogram from the ITACA/ESM ``eventdata`` - web-service and parse the returned DYNA 1.2 ASCII archive. - 2. Otherwise, gracefully degrade to a *realistic synthetic* accelerogram - (white background noise + high-frequency P impulse + larger S arrival), - whose exact P-arrival is known and written to ground_truth.json. - -Output: - / - events/.csv # t,ax,ay,az (100 Hz, G) - ground_truth.json # [{event_id, p_arrival_s}] -""" - -from __future__ import annotations - -import argparse -import csv -import json -import math -import os -import random -import sys -from pathlib import Path - -from calibrate_io import resolve_within_root - -SAMPLING_HZ = 100 - -# The gravity constant is the *standard* one used in geophysics, NOT the 9.81 -# approximation from the ADXL345 datasheet. ITACA/ESM DYNA 1.2 files carry -# accelerations in Gal (cm/s^2); 1 G = 980.665 Gal exactly. Keeping this -# constant lets the C++ NOISE_FLOOR react to the same physical scale both -# locally and on the real MEMS node. -G_TO_MS2 = 9.80665 -GAL_PER_G = 1e2 / G_TO_MS2 # = 980.665 Gal per G -GRAVITY = G_TO_MS2 # m/s^2, vertical baseline for the synthetic mock - - -class ItacaDataError(RuntimeError): - """Raised when the ITACA portal does not return usable data.""" - - -class ItacaFetcher: - """Downloads real ITACA data when a token is available. - - Uses the ITACA strong-motion web-services: - eventdata WS : /itaca40ws/eventdata/1/query - auth : a signed-message token from - /itaca40ws/generate-signed-message/1/ - - The exact HTTP auth handshake changes across portal revisions and is - therefore isolated here so the rest of the pipeline never changes. - """ - - def __init__(self, base_url: str = "https://itaca.mi.ingv.it", token: str | None = None) -> None: - self.base_url = base_url.rstrip("/") - self.token = token if token is not None else os.environ.get("ITACA_TOKEN") - - @property - def available(self) -> bool: - """Whether a real download is possible (a token is configured).""" - return bool(self.token) - - def list_events(self, min_magnitude: float = 4.0) -> list[dict]: - """Return [{event_id, p_arrival_s}] for catalog events above magnitude. - - Queries the ITACA *flatfile* web-service (publicly accessible, no token - required) for candidate events and their P-arrival metadata. The - flatfile currently does NOT expose phase arrivals, so this is a hook - for the future schema mapping. - """ - raise NotImplementedError( - "Real ITACA event catalogue not bound to the live flatfile schema; " - "use the synthetic fallback (no ITACA_TOKEN)." - ) - - def download(self, event: dict, out_dir: Path) -> None: - """Download one real event's accelerogram and write the CSV (G units). - - Adapt `_build_request` / `_parse_dyna` to the live portal format. - """ - raise NotImplementedError( - "Real ITACA waveform parsing is token-gated and not implemented; " - "use the synthetic fallback (no ITACA_TOKEN)." - ) - - -class RealisticSynthetic: - """Realistic accelerometer-like mock: noise + high-frequency P + larger S. - - The waveform mimics what a MEMS node resting under gravity records. Because - the detector's feature is the *magnitude* norm(x,y,z) over a ~9.8 m/s^2 - gravity baseline, only the VERTICAL (Z) component changes that norm in - first order; horizontal shaking perturbs it to second order and stays below - the noise floor. The synthetic therefore puts both the P and S energy on - the vertical axis (as a vertical-component accelerogram does), while the - measured axes still carry a realistic 9.8 m/s^2 gravity offset: - - - continuous white background noise (well below NOISE_FLOOR); - - a short, high-frequency P impulse arriving at ``p_arrival_s``; - - a stronger, lower-frequency S arrival ~1.8 s later; - - a constant GRAVITY vertical offset (the node "sits" under gravity). - - Units: m/s^2, matching the Adafruit `sensors_event_t` API the firmware - feeds into the core and the 9.8 m/s^2 gravity baseline of the C++ detector. - """ - - def __init__(self, fs: int = SAMPLING_HZ) -> None: - self.fs = fs - - def synthesize( - self, - event_id: str, - seed: int = 0, - pga_ms2: float = 2.0, - p_arrival_s: float = 7.0, - ) -> tuple[list[float], list[tuple[float, float, float]], float]: - """Return (times, axes_in_m/s2, exact_p_arrival_s).""" - del event_id - rng = random.Random(seed) - fs = self.fs - - s_offset_s = 1.8 # S follows P by ~1.8 s (near-source delay) - s_arrival_s = p_arrival_s + s_offset_s - duration_s = p_arrival_s + 15.0 - n = int(duration_s * fs) - times = [i / fs for i in range(n)] - - noise_sigma = 0.02 # ~0.02 m/s^2 background gaussian noise (<= 2e-3 G) - axes: list[tuple[float, float, float]] = [] - - for i in range(n): - t = times[i] - ax = rng.gauss(0.0, noise_sigma) - ay = rng.gauss(0.0, noise_sigma) - raw_z = rng.gauss(0.0, noise_sigma) - - # P-wave impulse: short high-frequency burst on the vertical axis - dt_p = t - p_arrival_s - if 0.0 <= dt_p < 0.5: - f_p = 10.0 - az_p = 0.30 * pga_ms2 * math.sin(2 * math.pi * f_p * dt_p) * math.exp(-25.0 * dt_p) - raw_z += az_p - ax += 0.15 * az_p - ay += 0.15 * az_p - - # S-wave arrival: larger, lower-frequency energy on the vertical axis - dt_s = t - s_arrival_s - if 0.0 <= dt_s < 5.0: - f_s = 2.5 - as_ = pga_ms2 * math.sin(2 * math.pi * f_s * dt_s) * math.exp(-1.2 * dt_s) - raw_z += as_ - - # Horizontal projection is kept near-zero: with a gravity baseline the - # magnitude is dominated by the vertical, so horizontal-only motion is - # invisible to norm3 (as on the real node). - axes.append((ax, ay, raw_z + GRAVITY)) - - return times, axes, p_arrival_s - - -def write_accelerogram_csv( - path: Path, times: list[float], axes: list[tuple[float, float, float]] -) -> None: - """Write an accelerogram in the shared t,ax,ay,az CSV format (G units).""" - with path.open("w", newline="") as f: - writer = csv.writer(f) - writer.writerow(["# t,ax,ay,az"]) # header (ignored by parser) - for t, (ax, ay, az) in zip(times, axes): - writer.writerow([f"{t:.6f}", f"{ax:.6f}", f"{ay:.6f}", f"{az:.6f}"]) - - -def build_synthetic_dataset(out_dir: Path, n_events: int = 5, seed: int = 42) -> None: - """Generate a realistic synthetic validation dataset (fallback mode).""" - safe_dir = resolve_within_root(out_dir) - events_dir = safe_dir / "events" - events_dir.mkdir(parents=True, exist_ok=True) - - gen = RealisticSynthetic() - ground_truth: list[dict] = [] - for i in range(n_events): - event_id = f"synth_{i:03d}" - times, axes, p_arrival = gen.synthesize(event_id, seed=seed + i, pga_ms2=1.0 + 0.6 * i) - write_accelerogram_csv(events_dir / f"{event_id}.csv", times, axes) - ground_truth.append({"event_id": event_id, "p_arrival_s": p_arrival}) - - with (safe_dir / "ground_truth.json").open("w") as f: - json.dump(ground_truth, f, indent=2) - - -def resolve_mode(fetcher: ItacaFetcher | None = None) -> str: - """Return ``real`` or ``synthetic`` depending on the available token.""" - fetcher = fetcher or ItacaFetcher() - return "real" if fetcher.available else "synthetic" - - -def download_catalog( - out_dir: Path, - n_events: int = 5, - seed: int = 42, - fetcher: ItacaFetcher | None = None, -) -> tuple[list[Path], list[dict], str]: - """Download the catalog. Returns (written_paths, ground_truth, mode). - - The output contract (layout + units) is identical in both modes. - """ - mode = resolve_mode(fetcher) - if mode == "real": - # The real ITACA path requires a token and a bound DYNA 1.2 parser. - # Until then it refuses to run instead of emitting a mock under a - # misleading name (see research/README.md). - raise NotImplementedError( - "ITACA real download is not implemented yet. " - "Run without ITACA_TOKEN to use the synthetic fallback." - ) - - build_synthetic_dataset(Path(out_dir), n_events=n_events, seed=seed) - written = sorted((Path(out_dir) / "events").glob("*.csv")) - with (Path(out_dir) / "ground_truth.json").open() as f: - ground_truth = json.load(f) - return written, ground_truth, mode - - -def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("out_dir", type=Path, help="output validation dataset dir") - parser.add_argument("--min-magnitude", type=float, default=4.0) - parser.add_argument("--n-events", type=int, default=5) - parser.add_argument("--seed", type=int, default=42) - args = parser.parse_args(argv) - - try: - written, ground_truth, mode = download_catalog( - args.out_dir, n_events=args.n_events, seed=args.seed - ) - except NotImplementedError as exc: - print(f"ITACA download not yet integrated ({exc}).", file=sys.stderr) - return 1 - - print(f"[{mode}] Wrote {len(written)} accelerograms to {args.out_dir / 'events'}") - print(f"[{mode}] Ground truth: {len(ground_truth)} events") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/research/synthetic.py b/research/synthetic.py index 5797728..b7ca574 100644 --- a/research/synthetic.py +++ b/research/synthetic.py @@ -1,21 +1,134 @@ """Generate a synthetic validation dataset for local smoke tests / CI. -Thin wrapper over the realistic synthetic generator in fetch_itaca.py, so the -fallback (no-token) path and this generator stay exactly in sync. Produces the -same layout as the ITACA path: +Canonical provider of the realistic synthetic fallback for the SIL pipeline +(R1). Produces the same layout as the real ESM path (to be implemented via +ObsPy, public FDSN API — see research/README.md): / events/.csv ground_truth.json -Used to exercise the full SIL pipeline without downloading the INGV dataset. +Used to exercise the full SIL pipeline without downloading a real dataset. """ from __future__ import annotations import argparse +import csv +import json +import math +import random from pathlib import Path -from fetch_itaca import build_synthetic_dataset +from calibrate_io import resolve_within_root + +SAMPLING_HZ = 100 + +# Standard geophysics gravity constant (1 G = 9.80665 m/s^2). The synthetic +# node "sits" under this baseline, matching the firmware `sensors_event_t`. +GRAVITY = 9.80665 # m/s^2, vertical baseline for the synthetic mock + + +class RealisticSynthetic: + """Realistic accelerometer-like mock: noise + high-frequency P + larger S. + + The waveform mimics what a MEMS node resting under gravity records. Because + the detector's feature is the *magnitude* norm(x,y,z) over a ~9.8 m/s^2 + gravity baseline, only the VERTICAL (Z) component changes that norm in + first order; horizontal shaking perturbs it to second order and stays below + the noise floor. The synthetic therefore puts both the P and S energy on + the vertical axis (as a vertical-component accelerogram does), while the + measured axes still carry a realistic 9.8 m/s^2 gravity offset: + + - continuous white background noise (well below NOISE_FLOOR); + - a short, high-frequency P impulse arriving at ``p_arrival_s``; + - a stronger, lower-frequency S arrival ~1.8 s later; + - a constant GRAVITY vertical offset (the node "sits" under gravity). + + Units: m/s^2, matching the Adafruit `sensors_event_t` API the firmware + feeds into the core and the 9.8 m/s^2 gravity baseline of the C++ detector. + """ + + def __init__(self, fs: int = SAMPLING_HZ) -> None: + self.fs = fs + + def synthesize( + self, + event_id: str, + seed: int = 0, + pga_ms2: float = 2.0, + p_arrival_s: float = 7.0, + ) -> tuple[list[float], list[tuple[float, float, float]], float]: + """Return (times, axes_in_m/s2, exact_p_arrival_s).""" + del event_id + rng = random.Random(seed) + fs = self.fs + + s_offset_s = 1.8 # S follows P by ~1.8 s (near-source delay) + s_arrival_s = p_arrival_s + s_offset_s + duration_s = p_arrival_s + 15.0 + n = int(duration_s * fs) + times = [i / fs for i in range(n)] + + noise_sigma = 0.02 # ~0.02 m/s^2 background gaussian noise (<= 2e-3 G) + axes: list[tuple[float, float, float]] = [] + + for i in range(n): + t = times[i] + ax = rng.gauss(0.0, noise_sigma) + ay = rng.gauss(0.0, noise_sigma) + raw_z = rng.gauss(0.0, noise_sigma) + + # P-wave impulse: short high-frequency burst on the vertical axis + dt_p = t - p_arrival_s + if 0.0 <= dt_p < 0.5: + f_p = 10.0 + az_p = 0.30 * pga_ms2 * math.sin(2 * math.pi * f_p * dt_p) * math.exp(-25.0 * dt_p) + raw_z += az_p + ax += 0.15 * az_p + ay += 0.15 * az_p + + # S-wave arrival: larger, lower-frequency energy on the vertical axis + dt_s = t - s_arrival_s + if 0.0 <= dt_s < 5.0: + f_s = 2.5 + as_ = pga_ms2 * math.sin(2 * math.pi * f_s * dt_s) * math.exp(-1.2 * dt_s) + raw_z += as_ + + # Horizontal projection is kept near-zero: with a gravity baseline the + # magnitude is dominated by the vertical, so horizontal-only motion is + # invisible to norm3 (as on the real node). + axes.append((ax, ay, raw_z + GRAVITY)) + + return times, axes, p_arrival_s + + +def write_accelerogram_csv( + path: Path, times: list[float], axes: list[tuple[float, float, float]] +) -> None: + """Write an accelerogram in the shared t,ax,ay,az CSV format (G units).""" + with path.open("w", newline="") as f: + writer = csv.writer(f) + writer.writerow(["# t,ax,ay,az"]) # header (ignored by parser) + for t, (ax, ay, az) in zip(times, axes): + writer.writerow([f"{t:.6f}", f"{ax:.6f}", f"{ay:.6f}", f"{az:.6f}"]) + + +def build_synthetic_dataset(out_dir: Path, n_events: int = 5, seed: int = 42) -> None: + """Generate a realistic synthetic validation dataset (fallback mode).""" + safe_dir = resolve_within_root(out_dir) + events_dir = safe_dir / "events" + events_dir.mkdir(parents=True, exist_ok=True) + + gen = RealisticSynthetic() + ground_truth: list[dict] = [] + for i in range(n_events): + event_id = f"synth_{i:03d}" + times, axes, p_arrival = gen.synthesize(event_id, seed=seed + i, pga_ms2=1.0 + 0.6 * i) + write_accelerogram_csv(events_dir / f"{event_id}.csv", times, axes) + ground_truth.append({"event_id": event_id, "p_arrival_s": p_arrival}) + + with (safe_dir / "ground_truth.json").open("w") as f: + json.dump(ground_truth, f, indent=2) def main(argv: list[str] | None = None) -> None: @@ -30,4 +143,4 @@ def main(argv: list[str] | None = None) -> None: if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/sonar-project.properties b/sonar-project.properties index 5167104..c5a565e 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -7,14 +7,16 @@ sonar.sourceEncoding=UTF-8 # --- Sources & tests --- sonar.sources=. -sonar.exclusions=**/node_modules/**,**/.pio/**,**/.venv/**,**/dist/**,**/build/** +sonar.exclusions=**/node_modules/**,**/.pio/**,**/.venv/**,**/dist/**,**/build/**,docs/web/**,docs/whitepaper/** sonar.tests=backend/tests, firmware/test # --- Duplication (CPD) --- # lightMapStyle/darkMapStyle share the same featureType/elementType keys but are # two independent palettes; structurally identical JSON is a known CPD false # positive, so the map theme file is excluded from duplication analysis. -sonar.cpd.exclusions=mobile/theme/mapStyle.ts +# docs/web (EN/IT i18n dict) and docs/whitepaper are mirrored page artifacts, +# not QuakeGuard source, so they are excluded from duplication as well. +sonar.cpd.exclusions=mobile/theme/mapStyle.ts,docs/web/**,docs/whitepaper/** # --- Python --- sonar.python.version=3.10