diff --git a/BUILD.md b/BUILD.md index a6b4a89..7fed4c6 100644 --- a/BUILD.md +++ b/BUILD.md @@ -46,7 +46,26 @@ Or via the automation script: ## Quick Start -### Using Automation Script (Recommended) +### Production Hardware (ESP32 DevKit V1) + +> πŸ“Œ See **[docs/WIRING_GUIDE.md](docs/WIRING_GUIDE.md)** for complete wiring instructions. + +```bash +cd firmware +idf.py menuconfig # Set WiFi SSID, Password, Backend IP +idf.py build +idf.py -p COM3 flash monitor +``` + +Start the backend on your computer (must be on the same WiFi network): + +```bash +cd backend +pip install -r requirements.txt +uvicorn app.main:app --host 0.0.0.0 --port 8000 +``` + +### Using Automation Script (QEMU Simulation) ```powershell # First time: install QEMU @@ -128,7 +147,7 @@ idf.py qemu ``` [GridShield] ============================================== -[GridShield] GridShield v3.0.1 [ESP32 - QEMU Simulation] +[GridShield] GridShield v3.3.0 [ESP32 - QEMU Simulation] [GridShield] Platform: ESP-IDF + QEMU [GridShield] ============================================== [GridShield] System started successfully @@ -153,7 +172,11 @@ pip install -r requirements.txt ### Run Development Server ```bash +# Local only (browser access) uvicorn app.main:app --reload --port 8000 + +# LAN access (required for ESP32 to connect) +uvicorn app.main:app --host 0.0.0.0 --port 8000 ``` ### Interactive API Docs @@ -258,10 +281,11 @@ firmware/ β”‚ β”‚ β”œβ”€β”€ network/ # packet.hpp β”‚ β”‚ β”œβ”€β”€ analytics/ # detector.hpp β”‚ β”‚ └── utils/ # gs_macros.hpp, gs_utils.hpp -β”‚ └── platform/ # platform.hpp, mock_platform.hpp +β”‚ └── platform/ # platform.hpp, esp32_platform.hpp β”œβ”€β”€ main/ β”‚ β”œβ”€β”€ CMakeLists.txt # Component registration -β”‚ β”œβ”€β”€ app_main.cpp # ESP-IDF entry point +β”‚ β”œβ”€β”€ app_main.cpp # QEMU simulation entry point +β”‚ β”œβ”€β”€ demo_main.cpp # Production firmware (real ESP32) β”‚ └── src/ # Implementation files β”‚ β”œβ”€β”€ analytics/ # detector.cpp β”‚ β”œβ”€β”€ core/ # system.cpp diff --git a/README.md b/README.md index a2acd69..9f92411 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ **Multi-Layer Security Framework for Advanced Metering Infrastructure (AMI)** [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -[![Version](https://img.shields.io/badge/Version-3.0.1-brightgreen)](.) +[![Version](https://img.shields.io/badge/Version-3.3.1-brightgreen)](.) [![C++17](https://img.shields.io/badge/C%2B%2B-17-00599C?logo=cplusplus)](https://en.cppreference.com/w/cpp/17) [![ESP-IDF](https://img.shields.io/badge/ESP--IDF-v5.5-E7352C?logo=espressif)](https://docs.espressif.com/projects/esp-idf/) [![Platform](https://img.shields.io/badge/Platform-ESP32%20%7C%20QEMU-green)](BUILD.md) @@ -21,7 +21,8 @@ GridShield is a production-grade security solution designed to protect smart ele ### πŸ” Physical Security Layer - **ISR-driven tamper detection** with debouncing logic -- Power-loss alerting via backup capacitor +- **Backup power (UPS)** β€” Li-ion 2500mAh via TP4056 + MT3608 +- **Buzzer alarm** β€” 4 alert patterns (tamper, temp, PZEM fail, boot) - Priority flagging for emergency transmission ### 🌐 Network Security Layer @@ -41,14 +42,16 @@ GridShield is a production-grade security solution designed to protect smart ele - Mock implementations for simulation testing ### πŸ–₯️ Backend & Dashboard -- **FastAPI** REST backend with 9 API endpoints +- **FastAPI** REST backend with 12 API endpoints - SQLite database with SQLAlchemy ORM - **Vite + Chart.js** real-time web dashboard -- 4 dashboards: Overview, Alerts, Anomalies, Fleet Management +- 5 dashboards: Live Monitor (hero cards), Alerts, Anomalies, Fleet, Notifications +- Bilingual UI (Bahasa Indonesia + English) +- 2-second live polling with threshold coloring ### πŸ§ͺ CI/CD & Testing -- **152 unit tests** across 17 test suites -- 6-job GitHub Actions pipeline (build, test, backend-lint, frontend-build, clang-tidy, coverage) +- **186 unit tests** across 20 test suites +- 7-job GitHub Actions pipeline (build, test, backend-test, backend-lint, frontend-build, clang-tidy, coverage) - LibFuzzer + ASan/UBSan fuzzing for packet parser - Code coverage reports via gcov/lcov - Hardware tested on ESP32-D0WD rev1.1 (Dual Core 240MHz) @@ -98,8 +101,10 @@ See [BUILD.md](BUILD.md) for full instructions. - [**Build Instructions**](BUILD.md) β€” Build & simulate with ESP-IDF + QEMU - [**Architecture**](docs/ARCHITECTURE.md) β€” System design with diagrams +- [**IoT Hardware Design**](docs/design/iot-design.html) β€” Component catalog, wiring schematic, assembly guide - [**API Reference**](docs/API.md) β€” Firmware & backend API documentation - [**Quick Start Guide**](docs/QUICKSTART.md) β€” Getting started tutorial +- [**Wiring Guide**](docs/WIRING_GUIDE.md) β€” Pin mapping, wiring diagrams, UPS power system - [**Tech Stack**](docs/TECHSTACK.md) β€” Technology choices - [**Roadmap**](docs/ROADMAP.md) β€” Future development plans - [**Changelog**](docs/CHANGELOG.md) β€” Version history @@ -114,7 +119,7 @@ gridshield/ β”‚ β”‚ β”œβ”€β”€ common/ # Platform-agnostic headers β”‚ β”‚ └── platform/ # HAL interfaces + mock impls β”‚ β”œβ”€β”€ main/ # Implementation files -β”‚ β”œβ”€β”€ test_app/ # Unity test suites (152 tests) +β”‚ β”œβ”€β”€ test_app/ # Unity test suites (186 tests) β”‚ β”œβ”€β”€ fuzz/ # LibFuzzer harness β”‚ β”œβ”€β”€ coverage/ # gcov/lcov coverage scripts β”‚ └── lib/micro-ecc/ # ECC library (secp256r1) @@ -131,9 +136,10 @@ gridshield/ β”‚ β”‚ β”œβ”€β”€ components/ # Navbar, Chart components β”‚ β”‚ └── api.js # Backend API client β”‚ └── package.json -β”œβ”€β”€ .github/workflows/ # CI/CD (6-job pipeline) +β”œβ”€β”€ .github/workflows/ # CI/CD (7-job pipeline) β”œβ”€β”€ scripts/script.ps1 # Build/run automation └── docs/ # Documentation + └── design/ # IoT hardware design (HTML/CSS) ``` ## 🀝 Contributing diff --git a/backend/app/anomaly_engine.py b/backend/app/anomaly_engine.py index b19ff7e..a234593 100644 --- a/backend/app/anomaly_engine.py +++ b/backend/app/anomaly_engine.py @@ -103,11 +103,10 @@ def analyze_reading(reading: models.MeterReading, db: Session) -> models.Anomaly db.commit() db.refresh(anomaly) -<<<<<<< HEAD + # Auto-generate notification for detected anomaly from .notification_engine import on_anomaly on_anomaly(anomaly, db) -======= ->>>>>>> origin/main return anomaly + diff --git a/backend/app/main.py b/backend/app/main.py index c58a3c5..84dd69f 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -9,18 +9,10 @@ from fastapi.middleware.cors import CORSMiddleware from .database import Base, engine -<<<<<<< HEAD from .export import router as export_router from .forensics import router as forensics_router from .meters import router as meters_router from .notifications import router as notifications_router -======= -<<<<<<< HEAD -from .export import router as export_router -======= ->>>>>>> 469b660da70c38354fe5127353f451559b605a7f -from .meters import router as meters_router ->>>>>>> origin/main from .routes import router from .webhooks import router as webhooks_router @@ -37,15 +29,7 @@ async def lifespan(app: FastAPI): description="REST API for GridShield AMI Security System β€” " "Meter data ingestion, tamper alerts, anomaly monitoring, " "and fleet management.", -<<<<<<< HEAD - version="3.3.0", -======= -<<<<<<< HEAD - version="3.2.0", -======= - version="3.1.0", ->>>>>>> 469b660da70c38354fe5127353f451559b605a7f ->>>>>>> origin/main + version="3.3.1", lifespan=lifespan, ) @@ -61,17 +45,10 @@ async def lifespan(app: FastAPI): # Routes app.include_router(router) app.include_router(meters_router) -<<<<<<< HEAD app.include_router(export_router) app.include_router(notifications_router) app.include_router(webhooks_router) app.include_router(forensics_router) -======= -<<<<<<< HEAD -app.include_router(export_router) -======= ->>>>>>> 469b660da70c38354fe5127353f451559b605a7f ->>>>>>> origin/main @app.get("/", tags=["Root"]) @@ -79,14 +56,6 @@ def root(): """Health check.""" return { "name": "GridShield API", -<<<<<<< HEAD - "version": "3.3.0", -======= -<<<<<<< HEAD - "version": "3.2.0", -======= - "version": "3.1.0", ->>>>>>> 469b660da70c38354fe5127353f451559b605a7f ->>>>>>> origin/main + "version": "3.3.1", "status": "running", } diff --git a/backend/app/models.py b/backend/app/models.py index 5eea167..27d3a45 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -4,7 +4,7 @@ from datetime import datetime -from sqlalchemy import Column, Integer, BigInteger, String, Float, DateTime, Boolean +from sqlalchemy import Column, Integer, BigInteger, String, Float, DateTime, Boolean, Text from .database import Base @@ -20,8 +20,12 @@ class MeterReading(Base): energy_wh = Column(Integer, nullable=False) voltage_mv = Column(Integer, nullable=False) current_ma = Column(Integer, nullable=False) + power_mw = Column(Integer, default=0) power_factor = Column(Integer, default=0) phase = Column(Integer, default=0) + temperature_c = Column(Float, nullable=True) + humidity_pct = Column(Float, nullable=True) + relay_on = Column(Boolean, nullable=True) class TamperAlert(Base): @@ -66,7 +70,6 @@ class Meter(Base): status = Column(String(20), default="offline") # online / offline / tampered registered_at = Column(DateTime, default=datetime.utcnow, nullable=False) last_seen_at = Column(DateTime, default=None, nullable=True) -<<<<<<< HEAD class Notification(Base): @@ -112,5 +115,4 @@ class ForensicsReport(Base): event_count = Column(Integer, default=0) summary = Column(String(1000), default="") raw_payload = Column(String(5000), default="{}") -======= ->>>>>>> origin/main + diff --git a/backend/app/routes.py b/backend/app/routes.py index c6a4160..2405e6d 100644 --- a/backend/app/routes.py +++ b/backend/app/routes.py @@ -149,3 +149,38 @@ def get_status(db: Session = Depends(get_db)): unacknowledged_alerts=unack, latest_reading_time=latest, ) + + +# ============================================================================ +# Latest Reading (for efficient polling) +# ============================================================================ +@router.get("/readings/latest", response_model=schemas.MeterReadingResponse | None) +def get_latest_reading( + meter_id: int | None = None, + db: Session = Depends(get_db), +): + """Get the most recent meter reading.""" + query = db.query(models.MeterReading) + if meter_id is not None: + query = query.filter(models.MeterReading.meter_id == meter_id) + return query.order_by(models.MeterReading.timestamp.desc()).first() + + +# ============================================================================ +# Reset All Data +# ============================================================================ +@router.delete("/reset") +def reset_all_data(db: Session = Depends(get_db)): + """Delete all readings, alerts, anomalies, notifications, and forensics.""" + counts = {} + for model_cls, name in [ + (models.MeterReading, "readings"), + (models.TamperAlert, "alerts"), + (models.AnomalyLog, "anomalies"), + (models.Notification, "notifications"), + (models.ForensicsReport, "forensics"), + ]: + count = db.query(model_cls).delete() + counts[name] = count + db.commit() + return {"message": "All data reset", "deleted": counts} diff --git a/backend/app/schemas.py b/backend/app/schemas.py index e1c7115..05fa1a3 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -16,8 +16,12 @@ class MeterReadingCreate(BaseModel): energy_wh: int = Field(..., ge=0, description="Energy in watt-hours") voltage_mv: int = Field(..., ge=0, description="Voltage in millivolts") current_ma: int = Field(..., ge=0, description="Current in milliamps") + power_mw: int = Field(default=0, ge=0, description="Power in milliwatts") power_factor: int = Field(default=0, ge=0, le=1000, description="Power factor (0-1000)") phase: int = Field(default=0, ge=0, le=3) + temperature_c: float | None = Field(default=None, description="Temperature in Celsius") + humidity_pct: float | None = Field(default=None, description="Humidity percentage") + relay_on: bool | None = Field(default=None, description="Relay state") class MeterReadingResponse(BaseModel): @@ -27,8 +31,12 @@ class MeterReadingResponse(BaseModel): energy_wh: int voltage_mv: int current_ma: int + power_mw: int power_factor: int phase: int + temperature_c: float | None = None + humidity_pct: float | None = None + relay_on: bool | None = None model_config = {"from_attributes": True} @@ -131,7 +139,6 @@ class MeterStats(BaseModel): avg_voltage_mv: float avg_current_ma: float last_reading_time: datetime | None = None -<<<<<<< HEAD # ============================================================================ @@ -209,5 +216,4 @@ class ForensicsReportResponse(BaseModel): raw_payload: str model_config = {"from_attributes": True} -======= ->>>>>>> origin/main + diff --git a/docs/API.md b/docs/API.md index 6938450..e7b19e3 100644 --- a/docs/API.md +++ b/docs/API.md @@ -2,7 +2,7 @@ Complete API documentation for GridShield Multi-Layer AMI Security System. -**Version:** 3.0.1 +**Version:** 3.3.1 **Language:** C++17 (Firmware), Python 3.11+ (Backend) **Namespace:** `gridshield` @@ -1901,14 +1901,62 @@ Implementations for QEMU simulation and testing: curl -X POST http://localhost:8000/api/meter-data \ -H "Content-Type: application/json" \ -d '{ - "meter_id": 1311768467294899695, + "meter_id": 1001, "energy_wh": 1200, - "voltage_mv": 220000, + "voltage_mv": 231900, "current_ma": 500, - "power_factor": 950 + "power_mw": 115950, + "power_factor": 950, + "temperature_c": 31.2, + "humidity_pct": 61.0, + "relay_on": true }' ``` +### Example: Get Latest Reading + +```bash +curl http://localhost:8000/api/readings/latest +``` + +**Response:** +```json +{ + "id": 42, + "meter_id": 1001, + "timestamp": "2026-04-15T16:30:00", + "energy_wh": 1200, + "voltage_mv": 231900, + "current_ma": 500, + "power_mw": 115950, + "power_factor": 950, + "phase": 0, + "temperature_c": 31.2, + "humidity_pct": 61.0, + "relay_on": true +} +``` + +### Example: Reset All Data + +```bash +curl -X DELETE http://localhost:8000/api/reset +``` + +**Response:** +```json +{ + "message": "All data reset", + "deleted": { + "readings": 150, + "alerts": 3, + "anomalies": 5, + "notifications": 12, + "forensics": 1 + } +} +``` + ### Interactive Docs - **Swagger UI:** [http://localhost:8000/docs](http://localhost:8000/docs) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 435f9f0..a3a0459 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,7 +1,7 @@ # GridShield - System Architecture -**Version:** 3.0.1 -**Last Updated:** February 2026 +**Version:** 3.3.1 +**Last Updated:** April 2026 **Authors:** M. Ichwan Fauzi, Rafi Indra Pramudhito Zuhayr, Cesar Ardika Bhayangkara **Implementasi Aktual:** Seluruh implementasi teknis di repository ini dikerjakan oleh **Rafi Indra Pramudhito Zuhayr** @@ -717,7 +717,10 @@ gridshield/ β”œβ”€β”€ QUICKSTART.md # Getting started guide β”œβ”€β”€ TECHSTACK.md # Technology choices β”œβ”€β”€ ROADMAP.md # Planned features - └── REQUIREMENTS.md # System requirements + β”œβ”€β”€ REQUIREMENTS.md # System requirements + └── design/ + β”œβ”€β”€ iot-design.html # IoT hardware design (interactive) + └── iot-design-styles.css # Design page styles ``` ### Compilation Units diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index fe83e72..6b3f9bb 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,10 +4,85 @@ 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). -<<<<<<< HEAD -======= -<<<<<<< HEAD ->>>>>>> origin/main + +## [3.3.1] - 2026-04-15 + +### Added +- **Backend API Endpoints**: + - `GET /api/readings/latest` β€” Fetch most recent reading for efficient polling. + - `DELETE /api/reset` β€” Truncate all data tables (readings, alerts, anomalies, notifications, forensics). +- **Backend Schema Extensions**: + - Added `temperature_c`, `humidity_pct`, `power_mw`, `relay_on` fields to `MeterReading` model and Pydantic schemas. + - ESP32 now sends DHT11 temperature/humidity and relay state with each reading. +- **Frontend Dashboard Redesign**: + - 8 hero cards: Voltage, Current, Power, Energy, Temperature, Humidity, Relay, Power Factor. + - Bilingual labels (Bahasa Indonesia + English) on all dashboard elements. + - Reset Data button with confirmation dialog. + - Live polling reduced from 10s to 2s for near-real-time updates. + - "Last updated" live indicator with seconds counter. + - Threshold coloring: voltage (green/amber/red), temperature (green/amber/red). + - Simplified chart: voltage trend only (removed dual-axis Energy+Voltage). +- **Buzzer Alert System** (`firmware/main/demo_main.cpp`): + - Piezo buzzer on GPIO25 via LEDC PWM. + - 4 distinct alert patterns: boot OK (2x chirp), PZEM fail (1x short), high temp (2x medium), tamper (3x long). +- **Backup Power System (UPS)**: + - TP4056 Li-ion charger + MT3608 boost converter + Li-ion 2500mAh battery. + - ESP32 always powered from battery via VIN pin (seamless AC-to-battery switchover). + - Estimated backup time: ~10 hours at ~200mA WiFi draw. + +### Fixed +- **DHT11 Driver** (`firmware/include/common/hardware/sensors/dht11.hpp`): + - Root cause: `GPIO_MODE_OUTPUT` (push-pull) was holding data line HIGH, preventing DHT11 from pulling LOW to respond. Fixed to `GPIO_MODE_OUTPUT_OD` (open-drain). + - Critical section now covers the entire release + response + read sequence (no race condition). + - Increased timeouts from 300us to 500us for better tolerance. + - Added GPIO diagnostic logging (idle level, after-release level, fail phase, bits read). +- **DHT11 Pin Relocation**: Moved from GPIO15 (strapping pin) to GPIO13 (clean pin) to avoid boot-time interference. +- **Stack Overflow**: Increased `CONFIG_ESP_MAIN_TASK_STACK_SIZE` from 3584 to 8192 bytes. + +### Changed +- **Serial Monitor Logs**: Cleaned up to ASCII-only format (no UTF-8/emoji). Format: `231.9V | 0.00A | 0.0W | 14Wh | PF 0.00` + `31.2 degC | 61.0% RH`. +- **Wiring Guide Rewrite**: Updated component list (removed CBB61 capacitors, added TP4056/MT3608/Li-ion), added UPS power architecture, updated all diagrams. +- Backend API version bumped to `3.3.1`. + +## [3.3.0] - 2026-03-30 + +### Added +- **Notification & Webhook System** (`backend/app/notifications.py`, `backend/app/webhooks.py`): + - In-app notification engine with auto-generation from alerts/anomalies. + - Notification API (list, summary, mark-read, mark-all-read). + - Webhook configuration CRUD with event-type filtering. + - HTTP POST dispatch with secret header authentication. +- **Forensics API** (`backend/app/forensics.py`): + - Incident report ingestion from firmware. + - Report listing with meter_id and type filters. + - Auto-notification for critical/high severity reports. +- **Frontend Notifications**: + - Notification center page with read/unread filtering. + - Navigation bell badge with unread count polling. + - Dashboard forensics report summary panel. +- **Firmware Alert Dispatcher** (`firmware/include/common/alerts/alert_dispatcher.hpp`): + - Configurable rule-based event dispatch (16 rules, zero-heap). + - Multiple action types (LogOnly, HttpPost, MqttPublish, All). + - AlertDispatcher tests (10 tests). +- **IoT Hardware Design v2.0** (`docs/design/iot-design.html`): + - Complete component catalog (9 components with detailed specs). + - Interactive SVG wiring schematic with color-coded wire paths. + - Step-by-step connection paths for all 8 sensor/peripheral groups. + - Power distribution tree (220VAC β†’ 5V β†’ 3.3V hierarchy). + - GPIO pin mapping with wire color coding. + - 8-step assembly guide for building a GridShield node. + - Updated BOM with estimated pricing (Rp 370,000). +- **Backend Testing**: + - Notification tests (6), Webhook tests (6), Forensics tests (7). + - Total backend tests: 59. +- **Firmware Tests**: + - AlertDispatcher tests (10). + - Total firmware test count: 186. + +### Changed +- Backend API version bumped to `3.3.0`. +- IoT hardware design split into separate CSS (`iot-design-styles.css`) for maintainability. +- Documentation updated across CHANGELOG, ROADMAP, README, and ARCHITECTURE. ## [3.2.0] - 2026-03-07 ### Added @@ -42,11 +117,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Frontend `api.js` extended with meter CRUD and CSV export functions. - Frontend `style.css` extended with modal, status indicator, and form styles. -<<<<<<< HEAD -======= -======= ->>>>>>> 469b660da70c38354fe5127353f451559b605a7f ->>>>>>> origin/main + ## [3.1.0] - 2026-03-06 ### Added @@ -240,6 +311,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Defined C++17 coding standards. - Established error handling patterns using `Result`. +[3.3.1]: https://github.com/zuudevs/gridshield/compare/v3.3.0...v3.3.1 +[3.3.0]: https://github.com/zuudevs/gridshield/compare/v3.2.0...v3.3.0 +[3.2.0]: https://github.com/zuudevs/gridshield/compare/v3.1.0...v3.2.0 +[3.1.0]: https://github.com/zuudevs/gridshield/compare/v3.0.1-fw...v3.1.0 [3.0.1]: https://github.com/zuudevs/gridshield/compare/v3.0.0-fw...v3.0.1-fw [3.0.0]: https://github.com/zuudevs/gridshield/compare/v2.0.0...v3.0.0 [2.0.0]: https://github.com/zuudevs/gridshield/compare/v1.1.0...v2.0.0 diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 965bd98..884176a 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -45,7 +45,9 @@ gridshield/ β”œβ”€β”€ backend/ ← FastAPI REST API (Python) β”œβ”€β”€ frontend/ ← Dashboard Web (Vite + Chart.js) β”œβ”€β”€ firmware/ ← Firmware ESP32 (C++17) -└── docs/ ← Dokumentasi +└── docs/ + β”œβ”€β”€ design/ ← IoT Hardware Design (wiring, components) + └── ... ← Dokumentasi lainnya ``` --- @@ -68,7 +70,11 @@ pip install -r requirements.txt Database sudah terisi data contoh (`gridshield.db`). Jika ingin reset: ```powershell -py seed.py +# Opsi 1: Via API (saat server sudah jalan) +curl -X DELETE http://localhost:8000/api/reset + +# Opsi 2: Hapus file database, lalu restart server +del gridshield.db ``` ### 2.3. Jalankan Server @@ -122,14 +128,15 @@ npm run dev Buka browser: **http://localhost:5173** -Dashboard memiliki **4 halaman**: +Dashboard memiliki **5 halaman**: | Halaman | URL | Fungsi | |---------|-----|--------| -| **Dashboard** | `http://localhost:5173/` | Overview KPI, grafik konsumsi energi, alert terbaru | +| **Dashboard** | `http://localhost:5173/` | Live monitor 8 hero cards (V, I, P, E, T, RH, Relay, PF) β€” bilingual ID/EN | | **Tamper Alerts** | `http://localhost:5173/#/alerts` | Manajemen alert keamanan fisik | | **Anomalies** | `http://localhost:5173/#/anomalies` | Log deteksi anomali konsumsi | | **Fleet** | `http://localhost:5173/#/fleet` | Manajemen dan monitoring meter | +| **Notifications** | `http://localhost:5173/#/notifications` | Pusat notifikasi (webhook, forensics) | **Tampilan Dashboard:** @@ -195,23 +202,22 @@ idf.py -p COM3 monitor # Monitor serial output ``` [GridShield] ============================================== -[GridShield] GridShield v3.0.1 [ESP32 - QEMU Simulation] -[GridShield] Platform: ESP-IDF + QEMU +[GridShield] GridShield v3.3.1 - Production Mode +[GridShield] PZEM-004T + DHT11 + Relay + Buzzer + Tamper [GridShield] ============================================== - -[GridShield] [Init] Configuring system... -[GridShield] Meter ID: 0x1234567890ABCDEF -[GridShield] Tamper Pin: 4 | Debounce: 50ms -[GridShield] Max Cycles: 20 - -[GridShield] System started successfully -[GridShield] Entering main processing loop... - -[GridShield] Cycle 1/20 OK -[GridShield] Cycle 2/20 OK -... -[GridShield] Cycle 20/20 OK -[GridShield] Simulation complete β€” all cycles finished +[GridShield] Backend: http://192.168.x.x:8000 +[GridShield] Meter ID: 0x00000000000003E9 +[GridShield] [OK] NVS storage initialized +[GridShield] [OK] LED + Relay + Buzzer initialized +[GridShield] [OK] WiFi connected +[GridShield] [OK] DHT11 initialized (GPIO13) +[GridShield] [OK] System initialized (state=3) + +[GridShield] ---- Reading #1 ---- +[GridShield] 231.9V | 0.00A | 0.0W | 14Wh | PF 0.00 +[GridShield] 31.2 degC | 61.0% RH +[GridShield] Relay: ON +[GridShield] POST /api/meter-data -> 201 OK ``` > **πŸŽ‰ Berhasil!** Firmware multi-layer security berjalan β€” tamper detection, crypto, dan anomaly detection aktif. @@ -294,6 +300,8 @@ Pastikan backend sudah berjalan di port 8000 **sebelum** menjalankan frontend. | Dokumen | Isi | |---------|-----| | [Architecture](ARCHITECTURE.md) | Desain sistem lengkap dengan diagram | +| [Wiring Guide](WIRING_GUIDE.md) | Pin mapping, wiring diagram, UPS power system | +| [IoT Hardware Design](design/iot-design.html) | Wiring schematic, komponen, assembly guide | | [API Reference](API.md) | Dokumentasi endpoint firmware & backend | | [Build Guide](../BUILD.md) | Konfigurasi build lanjutan | | [Tech Stack](TECHSTACK.md) | Teknologi yang digunakan | diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md index 6d3096e..dd686d4 100644 --- a/docs/REQUIREMENTS.md +++ b/docs/REQUIREMENTS.md @@ -1,10 +1,10 @@ # SOFTWARE REQUIREMENTS SPECIFICATION (SRS) ## Project: GridShield - Multi-layer Security Model for AMI -**Version:** 2.0.0 +**Version:** 3.3.0 **Status:** Active **Analysis by:** Rafi **Implementasi Aktual:** Seluruh implementasi teknis di repository ini dikerjakan oleh **Rafi Indra Pramudhito Zuhayr** -**Last Updated:** February 2026 +**Last Updated:** March 2026 --- @@ -20,8 +20,8 @@ As per the proposal, the main problems being solved are: ### 1.3 POC Scope Dokumen ini mendefinisikan requirements untuk **Proof of Concept (POC)** dengan batasan: -- **Hardware Target:** ESP32 DevKit V1 (simulasi via Wokwi, deploy ke hardware fisik) -- **Build System:** PlatformIO (satu-satunya toolchain) +- **Hardware Target:** ESP32 DevKit V1 (simulasi via QEMU, deploy ke hardware fisik) +- **Build System:** ESP-IDF v5.5 (satu-satunya toolchain) - **Backend:** Python + FastAPI + SQLite - **Tim:** 3 orang (lihat section PIC mapping) @@ -98,9 +98,10 @@ Dokumen ini mendefinisikan requirements untuk **Proof of Concept (POC)** dengan ## 5. SYSTEM CONSTRAINTS 1. **Programming Language:** Firmware side must use **C++17**. Server/Analytics side uses **Python 3.11+**. -2. **Hardware:** ESP32 DevKit V1 sebagai target utama. Development/testing via **Wokwi simulator**. -3. **Build System:** **PlatformIO** sebagai satu-satunya toolchain (tanpa CMake native build). -4. **Connectivity:** Serial (UART) untuk POC demo, WiFi HTTP untuk production-like demo. +2. **Hardware:** ESP32 DevKit V1 sebagai target utama. Development/testing via **QEMU simulator** atau hardware fisik. +3. **Build System:** **ESP-IDF v5.5** sebagai satu-satunya toolchain. +4. **Connectivity:** Serial (UART) untuk POC demo, WiFi HTTP/MQTT untuk production-like demo. +5. **Sensors:** ACS712, ZMPT101B, PZEM-004T, DS18B20, MPU6050, Micro Switch (lihat [IoT Hardware Design](design/iot-design.html)). --- @@ -116,6 +117,6 @@ Dokumen ini mendefinisikan requirements untuk **Proof of Concept (POC)** dengan --- **Document Information:** -- **Version:** 2.0.0 -- **Last Updated:** February 2026 +- **Version:** 3.3.0 +- **Last Updated:** March 2026 - **Language:** Indonesian + English \ No newline at end of file diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 21b0d99..a3ea7dd 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -2,19 +2,9 @@ Future development plans and feature roadmap for GridShield AMI Security System. -**Last Updated:** March 2026 -<<<<<<< HEAD -**Current Version:** 3.3.0 +**Last Updated:** April 2026 +**Current Version:** 3.3.1 **Next Target:** 3.4.0 (Q3 2027) -======= -<<<<<<< HEAD -**Current Version:** 3.2.0 -**Next Target:** 3.3.0 (Q2 2027) -======= -**Current Version:** 3.1.0-fw -**Next Target:** 3.2.0 (Q2 2027) ->>>>>>> 469b660da70c38354fe5127353f451559b605a7f ->>>>>>> origin/main --- @@ -234,10 +224,7 @@ GridShield aims to become the **industry-standard open-source security framework --- -<<<<<<< HEAD -======= -<<<<<<< HEAD ->>>>>>> origin/main + ### Q2 2027 β€” Integration & Export Enhancement (v3.2.0) **Target Release:** June 2027 (completed March 2026) @@ -276,7 +263,6 @@ GridShield aims to become the **industry-standard open-source security framework --- -<<<<<<< HEAD ### Q3 2027 β€” Notification System & Forensics API (v3.3.0) **Target Release:** September 2027 (completed March 2026) @@ -319,12 +305,17 @@ GridShield aims to become the **industry-standard open-source security framework - [x] AlertDispatcher tests (10 tests) - [x] Total firmware test count: 186 (176 + 10) +- [x] **IoT Hardware Design v2.0** (Documentation) + - [x] Complete component catalog (9 components) + - [x] Interactive SVG wiring schematic with color-coded paths + - [x] Step-by-step connection paths for all sensor groups + - [x] Power distribution tree diagram + - [x] GPIO pin mapping with wire colors + - [x] 8-step assembly guide + - [x] Updated BOM with pricing + --- -======= -======= ->>>>>>> 469b660da70c38354fe5127353f451559b605a7f ->>>>>>> origin/main ## Feature Backlogs ### Core Features @@ -381,15 +372,7 @@ GridShield aims to become the **industry-standard open-source security framework - [x] **Forensics** - [x] Attack signature event logger (SecurityEvent + EventLogger) - [x] Incident timeline reconstruction (get_timeline) -<<<<<<< HEAD - [x] Evidence preservation (EvidenceStore + hash chain) -======= -<<<<<<< HEAD - - [x] Evidence preservation (EvidenceStore + hash chain) -======= - - [ ] Evidence preservation ->>>>>>> 469b660da70c38354fe5127353f451559b605a7f ->>>>>>> origin/main - [x] Automated reporting (IncidentReportGenerator) --- @@ -539,15 +522,8 @@ Features are prioritized based on: | **3.0.0** | December 2026 | Cloud integration, ML analytics, 206 tests (total) | | **3.0.1** | March 2026 | CI fixes, hardware testing (ESP32-D0WD), HKDF test fix, 152 tests | | **3.1.0** | March 2026 | Fleet management API, server-side anomaly detection, forensics module, backend tests (31), firmware tests (+16 = 168 total) | -<<<<<<< HEAD -| **3.2.0** | March 2026 | Data export API (CSV), frontend fleet integration, evidence preservation, backend CI tests, 37+ backend tests, 176 firmware tests | -| **3.3.0** | March 2026 | Notification & webhook system, forensics API, frontend notifications page, alert dispatcher, 59 backend tests, 186 firmware tests | -======= -<<<<<<< HEAD | **3.2.0** | March 2026 | Data export API (CSV), frontend fleet integration, evidence preservation, backend CI tests, 37+ backend tests, 176 firmware tests | -======= ->>>>>>> 469b660da70c38354fe5127353f451559b605a7f ->>>>>>> origin/main +| **3.3.0** | March 2026 | Notification & webhook system, forensics API, frontend notifications page, alert dispatcher, IoT hardware design v2.0, 59 backend tests, 186 firmware tests | --- diff --git a/docs/SECURITY_AUDIT.md b/docs/SECURITY_AUDIT.md index c879380..4226fd0 100644 --- a/docs/SECURITY_AUDIT.md +++ b/docs/SECURITY_AUDIT.md @@ -1,4 +1,4 @@ -# Security Audit Report β€” GridShield v2.0.1 +# Security Audit Report β€” GridShield v3.3.0 **Audit Date:** February 2026 **Auditor:** zuudevs @@ -87,8 +87,8 @@ Critical cryptographic operations are sound, but some areas require hardening be | ID | Severity | Finding | Recommendation | |----|----------|---------|----------------| -| SA-011 | **Medium** | Single sensor input (GPIO only) | Add multi-sensor fusion (accelerometer, light, capacitive) | -| SA-012 | **Low** | No tamper event logging to persistent storage | Log tamper events to NVS for post-incident forensics | +| SA-011 | ~~**Medium**~~ | ~~Single sensor input (GPIO only)~~ | βœ… **Resolved in v3.3.0** β€” Multi-sensor fusion implemented (MPU6050 accelerometer, DS18B20 temperature, micro switch, power-cut ADC) | +| SA-012 | ~~**Low**~~ | ~~No tamper event logging to persistent storage~~ | βœ… **Resolved in v3.1.0** β€” EventLogger + EvidenceStore with hash chain | --- diff --git a/docs/TECHSTACK.md b/docs/TECHSTACK.md index 2ad14e0..28f33f6 100644 --- a/docs/TECHSTACK.md +++ b/docs/TECHSTACK.md @@ -1,7 +1,7 @@ # GridShield Technology Stack -**Version:** 3.0.1 -**Last Updated:** February 2026 +**Version:** 3.3.1 +**Last Updated:** April 2026 **Maintained By:** GridShield Development Team **Implementasi Aktual:** Seluruh implementasi teknis di repository ini dikerjakan oleh **Rafi Indra Pramudhito Zuhayr** @@ -188,9 +188,12 @@ QEMU Commands: - **WebSocket**: Dashboard live updates ### Hardware Protocols -- **GPIO**: Tamper sensor digital input -- **Interrupt**: ISR for tamper detection -- **I2C**: Sensor communication (if needed) +- **GPIO**: Tamper sensor digital input (GPIO4, ISR FALLING edge) +- **Interrupt**: ISR for tamper detection (50ms debounce) +- **UART2**: PZEM-004T energy meter (RXβ†’GPIO16, TXβ†’GPIO17) +- **OneWire**: DHT11 temperature/humidity (GPIO13, open-drain) +- **PWM (LEDC)**: Buzzer alert tones (GPIO25) +- **Digital OUT**: Relay control (GPIO26), LED heartbeat (GPIO2) ### Data Formats - **Binary**: Optimized secure packet (firmware β†’ backend) @@ -214,16 +217,22 @@ QEMU Commands: ### Development Boards (PIC: Cesar) - ESP32 DevKit V1 (Γ—2) β€” primary target - Logic Analyzer (debugging) - -### Sensors Inventory (PIC: Cesar) -- Hall Effect Sensors (ACS712) β€” current measurement -- Voltage Sensors (ZMPT101B) β€” voltage measurement -- Limit Switches β€” tamper detection -- Temperature Sensors (DS18B20) β€” temperature monitoring - -### Power Supply -- USB Power Banks (portable testing) -- Supercapacitors (backup power simulation) +- Multimeter Digital (validation) + +### Hardware Inventory (PIC: Cesar) +- **PZEM-004T v3.0** Energy Meter β€” V/I/P/E/F/PF via UART2 Modbus +- **PZKHCT** CT Clamp β€” current transformer for PZEM +- **DHT11** Temp/Humidity β€” thermal monitoring via GPIO13 (open-drain) +- **JQC-3FF-S-Z** Relay β€” AC load control via GPIO26 +- **Piezo Buzzer** β€” alert tones via GPIO25 (LEDC PWM) +- **Tamper Switch** (pull-up) β€” enclosure protection via GPIO4 ISR +- **MCB IC60N** β€” AC circuit breaker +- **MEAN WELL IRM-10-3.3** β€” AC-DC PSU 3.3V 3A +- **TP4056** β€” Li-ion charger module +- **MT3608** β€” DC-DC boost converter (3.3V β†’ 5V) +- **Li-ion 18650 2500mAh** β€” backup battery (UPS) + +> **πŸ“‹ Hardware Design:** Lihat [Wiring Guide](WIRING_GUIDE.md) dan [IoT Hardware Design](design/iot-design.html) untuk wiring schematic, pinout, dan assembly guide lengkap. --- @@ -258,15 +267,23 @@ Required: - Python 3.11+ - Git -Hardware (when ready for physical testing): -- ESP32 DevKit V1 (Γ—2) -- Limit switch sensors -- ACS712 current sensor -- Breadboard + jumper wires +Hardware (actual components): +- ESP32 DevKit V1 +- PZEM-004T v3.0 + PZKHCT CT Clamp +- DHT11 temperature/humidity sensor +- JQC-3FF-S-Z relay module +- Piezo buzzer (3.3V) +- Tamper switch (pull-up) +- MCB IC60N +- MEAN WELL IRM-10-3.3 (AC-DC PSU) +- TP4056 Li-ion charger module +- MT3608 DC-DC boost converter +- Li-ion 18650 2500mAh battery +- Terminal blocks + breadboard + jumper wires ``` --- -**Document Version:** 3.0.1 -**Last Updated:** February 2026 +**Document Version:** 3.3.1 +**Last Updated:** April 2026 **Review Cycle:** Quarterly \ No newline at end of file diff --git a/docs/THREAT_MODEL.md b/docs/THREAT_MODEL.md index 5adb3c0..df28caf 100644 --- a/docs/THREAT_MODEL.md +++ b/docs/THREAT_MODEL.md @@ -1,7 +1,7 @@ # Threat Model β€” GridShield -**Version:** 2.0.1 -**Date:** February 2026 +**Version:** 3.3.0 +**Date:** March 2026 **Methodology:** STRIDE (Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, Elevation of Privilege) --- @@ -19,6 +19,15 @@ graph LR A --> C[Energy Sensors] A --> D[Crypto Engine] A --> E[NVS Storage] + A --> F2[Alert Dispatcher] + end + subgraph Sensors + B --> B1[Micro Switch GPIO4] + B --> B2[MPU6050 I2C] + C --> C1[ACS712 ADC] + C --> C2[ZMPT101B ADC] + C --> C3[PZEM-004T UART] + C --> C4[DS18B20 OneWire] end A -- Signed Packets --> F[Backend Server] F --> G[Dashboard] @@ -45,6 +54,7 @@ graph LR | Firmware Binary | **High** | Flash | | Configuration | **Medium** | NVS | | Tamper Event Logs | **Medium** | RAM / Transit | +| Forensic Evidence Chain | **High** | RAM (EvidenceStore) | | Device Identity (meter_id) | **Medium** | NVS + Firmware | --- @@ -134,6 +144,6 @@ After all planned mitigations: ## 6. Next Review -- **Q3 2026:** After communication protocol implementation -- **Q4 2026:** After cloud integration + OTA updates +- **Q2 2026:** After IoT hardware deployment with physical sensors +- **Q3 2026:** After communication protocol hardening - **Annually** thereafter, or after any security incident diff --git a/docs/WIRING_GUIDE.md b/docs/WIRING_GUIDE.md new file mode 100644 index 0000000..a951053 --- /dev/null +++ b/docs/WIRING_GUIDE.md @@ -0,0 +1,451 @@ +# GridShield v3.3.1 β€” Panduan Wiring + +## Daftar Komponen + +| # | Komponen | Spesifikasi | Fungsi | +|---|----------|-------------|--------| +| 1 | **ESP32 DevKit V1** | Xtensa LX6 Dual-Core 240MHz | Mikrokontroler utama | +| 2 | **PZEM-004T v3.0** | Energy Meter Modbus RTU | Ukur V, I, P, E, Hz, PF | +| 3 | **PZKHCT** | CT Clamp (Current Transformer) | Sensor arus untuk PZEM | +| 4 | **DHT11** | Sensor Suhu & Kelembaban | Monitor suhu lingkungan | +| 5 | **JQC-3FF-S-Z** | Relay 10A 250VAC | Kontrol ON/OFF beban AC | +| 6 | **MCB IC60N** | Miniature Circuit Breaker | Proteksi arus lebih | +| 7 | **MEAN WELL IRM-10-3.3** | AC-DC PSU 3.3V 3A (10W) | Catu daya utama dari AC 220V | +| 8 | **TP4056** | Li-ion Charger Module | Charging baterai dari IRM | +| 9 | **MT3608** | DC-DC Boost Converter | Boost 3.3V β†’ 5V untuk TP4056 | +| 10 | **Li-ion 18650** | 2500mAh 3.7V | Baterai cadangan (UPS) | +| 11 | **Buzzer Aktif** | Piezo Buzzer 3.3V | Alarm suara (tamper/overheat) | +| 12 | **Tamper Switch** | Switch + Pull-up | Deteksi pembukaan casing | +| 13 | **Terminal Block** | Screw Terminal | Koneksi kabel AC 220V | +| 14 | **Breadboard** | β€” | Prototyping koneksi sensor | + +--- + +## Arsitektur Daya (Power Architecture) + +ESP32 **selalu** powered dari baterai melalui TP4056. Saat AC hidup, IRM-10-3.3 mengisi baterai. Saat AC mati, baterai tetap menyuplai ESP32 β†’ kirim alert **POWER LOSS**. + +``` + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ MEAN WELL IRM-10-3.3 β”‚ +PLN 220V AC ──► MCB IC60N ──┬──────►│ AC IN (L,N) β”‚ + β”‚ β”‚ DC OUT 3.3V β”œβ”€β”€β” + β”‚ β”‚ GND β”€β”€β”€β”€β”€β”€β”€β”€β”œβ”€β”€β”€ + β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ + β”‚ β”‚ + β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ + β”‚ β”‚ MT3608 β”‚β—„β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ IN 3.3V β”‚ (boost 3.3V β†’ 5V) + β”‚ β”‚ OUT 5V β”€β”€β”€β”€β”€β”€β”œβ”€β”€β” + β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ + β”‚ β”‚ + β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ TP4056 β”‚ + β”‚ β”‚ IN+ (5V) β”‚ + β”‚ β”‚ BAT+/BAT- β”€β”€β”€β”€β”œβ”€β”€ Li-ion 2500mAh + β”‚ β”‚ OUT+ β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”œβ”€β”€β–Ί ESP32 VIN + β”‚ β”‚ OUT- β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”œβ”€β”€β–Ί ESP32 GND + β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”œβ”€β”€β”€β”€β”€β”€β–Ί PZEM-004T (AC L,N + CT Clamp) + β”‚ + └──────► Relay COM ──► Beban AC ──► Neutral +``` + +### Alur Daya + +| Kondisi | Alur | ESP32 Status | +|---------|------|--------------| +| **AC ON** | PLN β†’ MCB β†’ IRM β†’ MT3608 β†’ TP4056 β†’ charge battery + power ESP32 | **Hidup** (dari battery+charger) | +| **AC OFF** | Battery β†’ TP4056 OUT β†’ ESP32 VIN | **Tetap hidup!** β†’ kirim alert | +| **Battery habis** | Tidak ada sumber daya | ESP32 mati | + +### Estimasi Backup Time + +| Parameter | Nilai | +|-----------|-------| +| Kapasitas baterai | 2500 mAh | +| Konsumsi ESP32 (WiFi aktif) | ~200 mA | +| Efisiensi AMS1117 | ~85% | +| **Estimasi backup** | **~10 jam** | + +> ⚠️ **TP4056 output = tegangan baterai (3.0–4.2V).** Masuk ke ESP32 VIN, lalu diturunkan oleh AMS1117 onboard ke 3.3V. Saat baterai di bawah ~3.5V, WiFi mungkin tidak stabil. + +> ⚠️ **Saat AMS1117 dropout**: Pada arus rendah (~200mA), dropout AMS1117 sekitar 0.5V. Jadi baterai 3.7V β†’ output 3.2V (masih di atas minimal ESP32: 3.0V). + +### Catatan Power Supply + +| Komponen | Sumber Daya | Tegangan | +|----------|-------------|----------| +| **ESP32 DevKit V1** | TP4056 OUT+ β†’ pin **VIN** | 3.0–4.2V β†’ AMS1117 β†’ 3.3V | +| **PZEM-004T** | Langsung dari AC 220V (terminal L, N) | 220V AC | +| **Relay JQC-3FF** | ESP32 **3V3** pin output | 3.3V DC* | +| **DHT11** | ESP32 pin **3V3** | 3.3V DC | +| **Buzzer** | ESP32 pin **GPIO25** (PWM) | 3.3V | +| **MT3608** | IRM-10-3.3 DC OUT | 3.3V IN β†’ 5V OUT | +| **TP4056** | MT3608 OUT (5V) | 5V IN β†’ charges Li-ion | + +> ⚠️ **JQC-3FF-S-Z biasanya versi 5V.** Jika relay kamu versi 5V, kamu perlu power USB terpisah ke VIN. Jika versi 3.3V, bisa langsung dari ESP32 3V3. Cek label di relay: **"SRA-05VDC" = 5V**, **"SRA-03V3DC" = 3.3V**. + +--- + +## Pin Mapping β€” ESP32 DevKit V1 + +``` + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ ESP32 DevKit V1 β”‚ + β”‚ β”‚ + TP4056 OUT+ ────►─ VIN GND β”œβ”€ GND (common) + TP4056 OUT- ────►─ GND 3V3 β”œβ”€ (output ke sensor) + β”œ VP (GPIO36) GPIO23β”œβ”€ + β”œ VN (GPIO39) GPIO22β”œβ”€ + β”œ GPIO34 GPIO21β”œβ”€ + β”œ GPIO35 GPIO19β”œβ”€ + β”œ GPIO32 GPIO18β”œβ”€ + β”œ GPIO33 GPIO5β”œβ”€ + Buzzer + ────────►─ GPIO25 GPIO17β”œβ”€β”€β”€β”€ PZEM RX (← ESP TX) + Relay IN ─────────►─ GPIO26 GPIO16β”œβ”€β”€β”€β”€ PZEM TX (β†’ ESP RX) + β”œ GPIO27 GPIO4β”œβ”€β”€β”€β”€ Tamper Switch + DHT11 DATA ──────►── GPIO13 GPIO2β”œβ”€β”€β”€β”€ LED (built-in) + β”œ GPIO14 β”œβ”€ + β”œ GPIO12 GND β”œβ”€ + β”œ GPIO15 3V3 β”œβ”€ + β”‚ [USB-C] β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Tabel Pin Assignment + +| Komponen | Interface | ESP32 GPIO | Keterangan | +|----------|-----------|------------|------------| +| PZEM-004T TX | UART2 RX | **GPIO16** | PZEM TX β†’ ESP32 RX (cross!) | +| PZEM-004T RX | UART2 TX | **GPIO17** | ESP32 TX β†’ PZEM RX (cross!) | +| DHT11 DATA | Digital | **GPIO13** | Pull-up internal + 4.7kΞ© external recommended | +| JQC-3FF Relay IN | Digital OUT | **GPIO26** | HIGH = ON, LOW = OFF | +| Buzzer (+) | PWM (LEDC) | **GPIO25** | Piezo buzzer aktif 3.3V | +| Tamper Switch | Digital IN | **GPIO4** | Pull-up, GND = tamper | +| LED Indicator | Digital OUT | **GPIO2** | Built-in LED (heartbeat) | + +--- + +## Wiring Detail per Komponen + +### 1. MEAN WELL IRM-10-3.3 β€” AC-DC Power Supply + +PSU ini mengubah **220V AC β†’ 3.3V DC**. Output-nya **tidak langsung** ke ESP32, tapi ke MT3608 untuk charging baterai. + +| IRM-10-3.3 Pin | Sambung ke | +|----------------|------------| +| **L (AC Input)** | Terminal Block β†’ dari MCB IC60N (Live) | +| **N (AC Input)** | Terminal Block β†’ Neutral | +| **+Vo (DC 3.3V)** | MT3608 **IN+** | +| **-Vo (DC GND)** | MT3608 **IN-** (GND common) | + +``` + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +MCB ──► Live ──►│ L +Vo (3.3V) β”œβ”€β”€β”€β”€β”€β–Ί MT3608 IN+ + Neutral ──►│ N -Vo (GND) β”œβ”€β”€β”€β”€β”€β–Ί GND (common) + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + MEAN WELL IRM-10-3.3 + + Output: 3.3V DC, 3A max (10W) + Input: 85-264V AC, 50/60Hz +``` + +> ⚠️ **PERINGATAN:** +> - IRM-10-3.3 terhubung langsung ke **220V AC**! +> - Pastikan sambungan AC terisolasi dengan baik +> - **JANGAN** sentuh terminal AC saat dalam keadaan hidup +> - Gunakan casing tertutup untuk keselamatan +> - Jika ESP32 juga terhubung USB (untuk programming), **cabut dulu IRM** sebelum colok USB! + +--- + +### 2. MT3608 + TP4056 + Li-ion β€” Backup Power System (UPS) + +Ini adalah sistem daya cadangan. Saat AC mati, baterai tetap menyuplai ESP32. + +**Urutan sambungan:** + +``` +IRM-10-3.3 (3.3V) ──► MT3608 IN+ + MT3608 OUT (5V) ──► TP4056 IN+ + TP4056 BAT ◄──► Li-ion 2500mAh + TP4056 OUT+ ──► ESP32 VIN + GND (common) ──────────────────── ESP32 GND +``` + +#### MT3608 β€” Boost Converter + +| MT3608 Pin | Sambung ke | +|------------|------------| +| **IN+** | IRM-10-3.3 **+Vo (3.3V)** | +| **IN-** | GND (common) | +| **OUT+** | TP4056 **IN+** | +| **OUT-** | GND (common) | + +> πŸ“Œ **Putar trimpot MT3608** hingga output = **5.0V** (ukur pakai multimeter sebelum sambung ke TP4056!) + +#### TP4056 β€” Li-ion Charger + UPS + +| TP4056 Pin | Sambung ke | +|------------|------------| +| **IN+** | MT3608 **OUT+ (5V)** | +| **IN-** | GND (common) | +| **BAT+** | Li-ion **positif (+)** | +| **BAT-** | Li-ion **negatif (-)** | +| **OUT+** | ESP32 **VIN** pin | +| **OUT-** | ESP32 **GND** | + +> πŸ“Œ TP4056 modul biasanya sudah include proteksi overcharge (DW01A + 8205A). +> πŸ“Œ LED merah = charging, LED biru = full charge. +> ⚠️ **JANGAN** sambungkan IRM langsung ke ESP32 3V3 saat menggunakan sistem UPS ini! + +--- + +### 3. PZEM-004T + PZKHCT CT Clamp + +| PZEM-004T Pin | Sambung ke | +|---------------|------------| +| **L (AC)** | Terminal Block β†’ Live (dari MCB) | +| **N (AC)** | Terminal Block β†’ Neutral | +| **TX (TTL)** | ESP32 **GPIO16** (cross-connect!) | +| **RX (TTL)** | ESP32 **GPIO17** (cross-connect!) | + +``` + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +Terminal β”‚ PZEM-004T v3.0 β”‚ +Block ──L───►│ [L] β”‚ + ──N───►│ [N] [CT]◄───────────────── PZKHCT CT Clamp + β”‚ β”‚ (klem di kabel Live) + β”‚ [TX]──────► GPIO16 β”‚ + β”‚ [RX]◄────── GPIO17 β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +> πŸ“Œ **TX PZEM β†’ GPIO16 (RX ESP32), RX PZEM β†’ GPIO17 (TX ESP32) β€” CROSS!** +> πŸ“Œ CT Clamp diklem melingkari kabel **LIVE saja** (jangan potong kabel!) + +--- + +### 4. DHT11 β€” Sensor Suhu & Kelembaban + +| DHT11 Pin | Sambung ke | +|-----------|------------| +| **VCC** (pin 1) | ESP32 **3V3** | +| **DATA** (pin 2) | ESP32 **GPIO13** | +| **NC** (pin 3) | Tidak dipakai | +| **GND** (pin 4) | ESP32 **GND** | + +``` +DHT11 (tampak depan, kisi-kisi menghadap kamu) +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β–‘β–‘β–‘β–‘β–‘β–‘β–‘ β”‚ +β”‚ β”‚ +β””β”€β”¬β”€β”¬β”€β”¬β”€β”¬β”˜ + 1 2 3 4 + β”‚ β”‚ β”‚ + 3V3β”‚ GND + β”‚ + GPIO13 +``` + +> πŸ“Œ Jika pembacaan tidak stabil, tambahkan resistor **4.7kΞ©** antara DATA dan 3V3. +> πŸ“Œ **JANGAN** gunakan GPIO15 β€” itu strapping pin yang mempengaruhi boot ESP32. + +--- + +### 5. JQC-3FF-S-Z Relay β€” Kontrol Beban AC + +| Relay Module Pin | Sambung ke | +|------------------|------------| +| **VCC** | ESP32 **3V3** (atau 5V jika relay versi 5V) | +| **GND** | ESP32 **GND** | +| **IN** (signal) | ESP32 **GPIO26** | + +**Koneksi sisi AC (terminal screw relay):** +``` +MCB ──► COM (Common) ──┐ + β”‚ JQC-3FF Relay + NO ────────────────► Beban AC ──► Neutral + β”‚ + NC β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ (tidak dipakai) +``` + +> πŸ“Œ **GPIO26 HIGH = Relay ON** (beban tersambung) +> ⚠️ Firmware otomatis **matikan relay** jika tamper terdeteksi atau suhu > 60Β°C + +--- + +### 6. Buzzer β€” Alarm Suara + +| Buzzer Pin | Sambung ke | +|------------|------------| +| **+ (positif)** | ESP32 **GPIO25** | +| **- (GND)** | ESP32 **GND** | + +``` +Buzzer (piezo aktif) +β”Œβ”€β”€β”€β”€β”€β”€β”€β” +β”‚ (+) (-)β”‚ +β””β”€β”¬β”€β”€β”€β”¬β”€β”˜ + β”‚ β”‚ +GPIO25 GND +``` + +**Pola bunyi:** +| Event | Pola | Frekuensi | +|-------|------|-----------| +| Boot OK | 2x chirp cepat | 2000 Hz | +| PZEM Fail | 1x beep pendek | 1500 Hz | +| Suhu Tinggi | 2x beep sedang | 2500 Hz | +| Tamper | 3x beep panjang | 3000 Hz | + +> πŸ“Œ Gunakan **buzzer aktif 3.3V** (sudah ada oscillator internal). Buzzer pasif juga bisa karena firmware menggunakan PWM. + +--- + +### 7. MCB IC60N β€” Circuit Breaker + +| Terminal | Sambung ke | +|----------|------------| +| **Input (atas)** | PLN 220V Live | +| **Output (bawah)** | Terminal Block β†’ distribusi | + +``` +PLN 220V Live ──► [MCB IC60N] ──► Terminal Block ──┬── IRM-10-3.3 (L) + β”œβ”€β”€ PZEM-004T (L) +PLN Neutral ──────────────────── Terminal Block ──┬── IRM-10-3.3 (N) + β”œβ”€β”€ PZEM-004T (N) + └── Beban (N) +``` + +--- + +### 8. Tamper Switch β€” Deteksi Pembukaan Casing + +| Switch Pin | Sambung ke | +|------------|------------| +| **Pin 1** | ESP32 **GPIO4** | +| **Pin 2** | ESP32 **GND** | + +> Casing tertutup = LOW (normal), Casing terbuka = HIGH (**TAMPER!** β†’ relay mati, buzzer 3x, alert dikirim) + +--- + +## Diagram Wiring Lengkap + +``` + PLN 220V AC + β”‚ β”‚ + L N + β”‚ β”‚ +β”Œβ”€β”€β”΄β”€β”€β” β”‚ +β”‚ MCB β”‚ β”‚ +β”‚IC60Nβ”‚ β”‚ +β””β”€β”€β”¬β”€β”€β”˜ β”‚ + β”‚ β”‚ +β”Œβ”€β”€β”΄β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€ Terminal Block ────────────────────┐ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”œβ”€β–Ίβ”‚ IRM-10-3.3 β”‚ β”‚ MT3608 β”‚ β”‚ TP4056 β”‚ β”‚ +β”‚ β”‚ L +Vo 3.3Vβ”œβ”€β”€β–Ίβ”‚IN OUT β”œβ”€β”€β–Ίβ”‚IN OUT+β”œβ”€β”€β–Ί ESP32 VIN +β”‚ β”‚ N -Vo GND β”œβ”€β”€β–Ίβ”‚GND GND β”‚ β”‚ OUT-β”œβ”€β”€β–Ί ESP32 GND +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚BAT ◄──► β”‚Li-ion 2500mAhβ”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”œβ”€β–Ίβ”‚ PZEM-004T β”‚ β”‚ +β”‚ β”‚ L β”‚ TX ──► GPIO16 (ESP32 RX) β”‚ +β”‚ β”‚ N β”‚ RX ◄── GPIO17 (ESP32 TX) β”‚ +β”‚ β”‚ CT ◄── PZKHCTβ”‚ (Clamp di Live) β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”œβ”€β–Ίβ”‚ Relay COM β”œβ”€β”€β–ΊNO─── Beban AC │──► N ── +β”‚ β”‚ JQC-3FF β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ IN ◄── GP26 β”‚ β”‚ +β”‚ β”‚ VCC ◄── 3V3 β”‚ β”‚ +β”‚ β”‚ GND ◄── GND β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + + ESP32 DevKit V1 (powered by TP4056 β†’ VIN) + β”œβ”€β”€ GPIO13 ◄── DHT11 DATA (+ 3V3, GND) + β”œβ”€β”€ GPIO25 ──► Buzzer (+, GND) + β”œβ”€β”€ GPIO26 ──► Relay IN + β”œβ”€β”€ GPIO4 ◄── Tamper Switch (+ GND) + β”œβ”€β”€ GPIO2 ──► LED built-in (heartbeat) + └── GPIO16/17 ◄──► PZEM-004T (UART2) +``` + +--- + +## Checklist Sebelum Power On + +### Sisi Backup Power (DC) +- [ ] MT3608: IN+ dari IRM +Vo, putar trimpot hingga OUT = **5.0V** +- [ ] TP4056: IN+ dari MT3608 OUT, BAT ke Li-ion, OUT+ ke ESP32 VIN +- [ ] Li-ion terpasang benar di TP4056 (cek polaritas!) +- [ ] ESP32 VIN dari TP4056 OUT+, GND dari TP4056 OUT- + +### Sisi Sensor (DC) +- [ ] DHT11: VCC=3V3, DATA=GPIO13, GND=GND +- [ ] Relay: VCC=3V3, GND=GND, IN=GPIO26 +- [ ] Buzzer: (+)=GPIO25, (-)=GND +- [ ] Tamper: satu kaki GPIO4, satu kaki GND +- [ ] PZEM TX β†’ GPIO16 (ESP RX), PZEM RX β†’ GPIO17 (ESP TX) β€” **CROSS!** +- [ ] Semua GND common (terhubung satu sama lain) + +### Sisi AC (220V) +- [ ] MCB IC60N terpasang dan **OFF** dulu +- [ ] Terminal block mengamankan semua kabel AC +- [ ] IRM-10-3.3 AC input L dan N tersambung benar +- [ ] PZEM-004T L dan N tersambung benar +- [ ] CT Clamp PZKHCT diklem di kabel **Live** (bukan Neutral!) +- [ ] Relay COM dari MCB, NO ke beban, beban ke Neutral +- [ ] Semua kabel AC terisolasi β€” **tidak ada yang terbuka!** + +### Urutan Power On +1. **Cabut USB** dari ESP32 (jika ada) +2. Flash firmware via USB terlebih dahulu (sebelum pasang ke sistem) +3. Pastikan MCB **OFF** +4. Pastikan MT3608 output = 5V (ukur dulu!) +5. Pasang Li-ion ke TP4056 +6. Periksa semua koneksi sekali lagi +7. Nyalakan MCB β†’ IRM β†’ MT3608 β†’ TP4056 β†’ ESP32 boot +8. Amati: LED 3x blink (starting), 2x blink + chirp (WiFi connected) +9. Cek TP4056 LED: merah = charging, biru = full +10. Tes MCB OFF β†’ ESP32 **tetap hidup** (battery), buzzer beep, alert terkirim + +--- + +## Troubleshooting + +| Masalah | Solusi | +|---------|--------| +| ESP32 tidak menyala | Cek TP4056 OUT dengan multimeter (harus 3.0–4.2V); cek baterai | +| MT3608 output salah | Putar trimpot pelan-pelan, ukur dengan multimeter (target 5.0V) | +| TP4056 LED merah terus | Baterai belum penuh (normal); jika >4 jam, cek koneksi baterai | +| TP4056 tidak charging | Cek MT3608 output (harus 5V); cek polaritas input TP4056 | +| PZEM tidak merespon | Cek TX/RX cross-connect, pastikan AC L/N terhubung | +| DHT11 selalu error | Pastikan pakai GPIO13 (bukan GPIO15!), tambah resistor 4.7kΞ© pull-up | +| Relay tidak ON | Cek apakah relay versi 3.3V atau 5V, sesuaikan catu daya | +| Tamper selalu trigger | Pastikan switch normally-open, pull-up aktif di firmware | +| Buzzer tidak bunyi | Cek polaritas (+/βˆ’), cek apakah buzzer 3.3V atau 5V | +| "POST failed" | Backend harus jalan: `uvicorn --host 0.0.0.0 --port 8000` | +| ESP32 restart terus | Watchdog timeout β€” periksa koneksi WiFi dan backend | +| MCB off tapi ESP32 mati | Cek baterai (mungkin habis); cek koneksi TP4056 β†’ VIN | + +--- + +## Catatan Keselamatan + +> ⚠️ **PERINGATAN β€” Proyek ini melibatkan tegangan AC 220V yang BERBAHAYA!** +> +> - Selalu **matikan MCB** sebelum menyentuh kabel atau komponen AC +> - Gunakan **casing tertutup** untuk deployment final +> - Jangan bekerja sendiri β€” minta orang lain standby +> - Li-ion bisa meledak jika short circuit β€” **jangan** biarkan terminal terbuka +> - Jika ragu, konsultasikan dengan dosen/teknisi listrik diff --git a/docs/design/iot-design-styles.css b/docs/design/iot-design-styles.css new file mode 100644 index 0000000..82390f0 --- /dev/null +++ b/docs/design/iot-design-styles.css @@ -0,0 +1,126 @@ +*,*::before,*::after{box-sizing:border-box;margin:0;padding:0} +:root{ +--bg:#060a14;--bg2:#0c1222;--card:rgba(15,23,42,.75);--card-h:rgba(20,30,55,.85); +--border:rgba(100,120,180,.15);--border-h:rgba(100,160,255,.3); +--text:#e2e8f0;--text2:#94a3b8;--muted:#64748b; +--cyan:#00f5d4;--cyan-d:rgba(0,245,212,.15);--cyan-g:rgba(0,245,212,.4); +--purple:#7b61ff;--purple-d:rgba(123,97,255,.15); +--red:#f87171;--red-d:rgba(248,113,113,.15); +--amber:#fbbf24;--amber-d:rgba(251,191,36,.15); +--green:#4ade80;--green-d:rgba(74,222,128,.15); +--blue:#38bdf8;--blue-d:rgba(56,189,248,.15); +--orange:#fb923c;--orange-d:rgba(251,146,60,.15); +--pink:#f472b6;--pink-d:rgba(244,114,182,.15); +--sans:'Inter',system-ui,sans-serif;--mono:'JetBrains Mono',monospace; +--r:12px;--r2:16px;--r3:20px; +} +html{-webkit-font-smoothing:antialiased;scroll-behavior:smooth} +body{font-family:var(--sans);color:var(--text);background:var(--bg);line-height:1.6;overflow-x:hidden} +body::before{content:'';position:fixed;inset:0;background-image:linear-gradient(rgba(100,160,255,.03) 1px,transparent 1px),linear-gradient(90deg,rgba(100,160,255,.03) 1px,transparent 1px);background-size:60px 60px;pointer-events:none;z-index:0} +a{color:var(--cyan);text-decoration:none} +::-webkit-scrollbar{width:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:rgba(100,120,180,.25);border-radius:3px} +.container{max-width:1200px;margin:0 auto;padding:0 24px;position:relative;z-index:1} +.hero{padding:80px 0 60px;text-align:center;position:relative} +.hero::before{content:'';position:absolute;top:0;left:50%;transform:translateX(-50%);width:600px;height:600px;background:radial-gradient(circle,rgba(0,245,212,.08),rgba(123,97,255,.05),transparent 70%);pointer-events:none;z-index:0} +.hero-badge{display:inline-flex;align-items:center;gap:8px;padding:6px 16px;border-radius:999px;background:var(--cyan-d);border:1px solid rgba(0,245,212,.2);color:var(--cyan);font-size:.75rem;font-weight:600;letter-spacing:.5px;text-transform:uppercase;margin-bottom:20px} +.hero h1{font-size:3rem;font-weight:800;letter-spacing:-.03em;line-height:1.1;margin-bottom:12px;background:linear-gradient(135deg,var(--cyan),var(--purple));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text} +.hero p{color:var(--text2);font-size:1.1rem;max-width:600px;margin:0 auto} +.section{padding:50px 0} +.section-title{font-size:1.5rem;font-weight:700;margin-bottom:8px;display:flex;align-items:center;gap:10px} +.section-title .icon{font-size:1.4rem} +.section-sub{color:var(--text2);font-size:.875rem;margin-bottom:28px} +.glass{background:var(--card);border:1px solid var(--border);border-radius:var(--r2);backdrop-filter:blur(12px);overflow:hidden;transition:all .25s} +.glass:hover{border-color:var(--border-h);transform:translateY(-2px);box-shadow:0 8px 32px rgba(0,0,0,.4)} +.glass-head{padding:20px 24px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between} +.glass-body{padding:24px} +.grid-2{display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:20px} +.grid-3{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:20px} +.grid-4{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:20px} +.badge{display:inline-flex;padding:3px 10px;border-radius:999px;font-size:.7rem;font-weight:600} +.b-cyan{background:var(--cyan-d);color:var(--cyan)} +.b-purple{background:var(--purple-d);color:var(--purple)} +.b-red{background:var(--red-d);color:var(--red)} +.b-amber{background:var(--amber-d);color:var(--amber)} +.b-green{background:var(--green-d);color:var(--green)} +.b-blue{background:var(--blue-d);color:var(--blue)} +.b-orange{background:var(--orange-d);color:var(--orange)} +.b-pink{background:var(--pink-d);color:var(--pink)} +.stat-card{padding:24px;text-align:center} +.stat-icon{font-size:2rem;margin-bottom:8px} +.stat-val{font-size:1.8rem;font-weight:800;font-family:var(--mono);line-height:1.2} +.stat-label{font-size:.75rem;color:var(--muted);margin-top:4px} +.layer-card{position:relative;padding:28px;border-left:3px solid var(--layer-color,var(--cyan))} +.layer-num{font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:var(--layer-color,var(--cyan));margin-bottom:8px} +.layer-title{font-size:1.125rem;font-weight:700;margin-bottom:6px} +.layer-desc{font-size:.8125rem;color:var(--text2);margin-bottom:12px} +.layer-features{list-style:none;display:flex;flex-direction:column;gap:6px} +.layer-features li{font-size:.8125rem;color:var(--text2);padding-left:16px;position:relative} +.layer-features li::before{content:'β€Ί';position:absolute;left:0;color:var(--layer-color,var(--cyan));font-weight:700} +.flow-container{display:flex;align-items:center;justify-content:center;gap:0;flex-wrap:wrap;padding:20px 0} +.flow-node{display:flex;flex-direction:column;align-items:center;gap:6px;padding:16px 20px;min-width:100px} +.flow-icon{width:56px;height:56px;border-radius:var(--r);display:flex;align-items:center;justify-content:center;font-size:1.6rem;border:1px solid var(--border)} +.flow-label{font-size:.75rem;font-weight:600;white-space:nowrap} +.flow-arrow{color:var(--cyan);font-size:1.2rem;font-weight:700;animation:flowPulse 2s ease-in-out infinite;flex-shrink:0} +@keyframes flowPulse{0%,100%{opacity:.3;transform:translateX(0)}50%{opacity:1;transform:translateX(4px)}} +.pin-table,.bom-table{width:100%;border-collapse:collapse;font-size:.8125rem} +.pin-table th,.bom-table th{text-align:left;padding:10px 14px;font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--muted);border-bottom:1px solid var(--border);white-space:nowrap} +.pin-table td,.bom-table td{padding:10px 14px;border-bottom:1px solid rgba(100,120,180,.08);vertical-align:middle} +.pin-table tbody tr:hover,.bom-table tbody tr:hover{background:var(--card-h)} +.pin-table .pin-num{font-family:var(--mono);color:var(--cyan);font-weight:600} +.board-container{display:flex;justify-content:center;padding:20px 0} +.board-svg{max-width:100%;height:auto} +.wiring-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:16px} +.wire-card{padding:20px} +.wire-icon{font-size:1.5rem;margin-bottom:8px} +.wire-title{font-size:.9375rem;font-weight:600;margin-bottom:4px} +.wire-desc{font-size:.8125rem;color:var(--text2);margin-bottom:10px} +.wire-pins{display:flex;flex-wrap:wrap;gap:6px} +.wire-pin{font-family:var(--mono);font-size:.7rem;padding:2px 8px;border-radius:4px;background:rgba(100,120,180,.1);color:var(--cyan)} +.spec-item{display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid rgba(100,120,180,.06);font-size:.8125rem} +.spec-item:last-child{border-bottom:none} +.spec-key{color:var(--text2)} +.spec-val{font-family:var(--mono);font-weight:500;color:var(--text)} +/* Component detail cards */ +.comp-card{padding:24px;position:relative;overflow:hidden} +.comp-card::after{content:'';position:absolute;top:0;right:0;width:80px;height:80px;background:radial-gradient(circle at top right,var(--comp-color,rgba(0,245,212,.08)),transparent 70%);pointer-events:none} +.comp-header{display:flex;align-items:center;gap:12px;margin-bottom:12px} +.comp-icon{width:48px;height:48px;border-radius:var(--r);display:flex;align-items:center;justify-content:center;font-size:1.5rem;border:1px solid var(--border);background:var(--bg2)} +.comp-name{font-size:1rem;font-weight:700} +.comp-model{font-size:.75rem;color:var(--muted);font-family:var(--mono)} +.comp-specs{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-top:12px} +.comp-spec{font-size:.75rem;color:var(--text2);display:flex;gap:4px} +.comp-spec strong{color:var(--text);font-weight:600} +/* Wire path cards */ +.path-card{padding:20px;border-left:3px solid var(--path-color,var(--cyan))} +.path-title{font-weight:700;font-size:.9375rem;margin-bottom:4px} +.path-bus{font-size:.7rem;font-family:var(--mono);color:var(--path-color,var(--cyan));margin-bottom:8px} +.path-steps{list-style:none;display:flex;flex-direction:column;gap:4px} +.path-steps li{font-size:.8rem;color:var(--text2);padding-left:20px;position:relative} +.path-steps li::before{content:'β†’';position:absolute;left:0;color:var(--path-color,var(--cyan))} +/* Assembly steps */ +.step-card{padding:20px;display:flex;gap:16px;align-items:flex-start} +.step-num{width:36px;height:36px;border-radius:50%;background:var(--cyan-d);border:1px solid rgba(0,245,212,.3);display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-weight:700;color:var(--cyan);font-size:.875rem;flex-shrink:0} +.step-content h4{font-size:.9375rem;font-weight:600;margin-bottom:4px} +.step-content p{font-size:.8125rem;color:var(--text2)} +.step-content .step-pins{margin-top:6px;display:flex;flex-wrap:wrap;gap:4px} +/* Power tree */ +.power-tree{display:flex;flex-direction:column;gap:2px;padding:16px} +.power-node{display:flex;align-items:center;gap:8px;padding:8px 12px;font-size:.8125rem;border-radius:var(--r);background:var(--bg2);border:1px solid var(--border)} +.power-node .voltage{font-family:var(--mono);color:var(--amber);font-weight:600;min-width:50px} +.power-branch{margin-left:32px;border-left:2px solid rgba(251,191,36,.2);padding-left:16px} +/* Tooltip */ +.tt{position:relative;cursor:help;border-bottom:1px dotted var(--muted)} +.tt:hover::after{content:attr(data-tip);position:absolute;bottom:100%;left:50%;transform:translateX(-50%);background:var(--bg2);border:1px solid var(--border);padding:4px 10px;border-radius:6px;font-size:.7rem;white-space:nowrap;color:var(--text);z-index:10} +.footer{text-align:center;padding:40px 0;color:var(--muted);font-size:.75rem;border-top:1px solid var(--border);margin-top:40px} +@keyframes fadeUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}} +.fade-up{animation:fadeUp .5s ease both} +.d1{animation-delay:.1s}.d2{animation-delay:.2s}.d3{animation-delay:.3s}.d4{animation-delay:.4s} +@media(max-width:768px){ +.hero h1{font-size:2rem} +.grid-2,.grid-3,.grid-4{grid-template-columns:1fr} +.flow-container{flex-direction:column} +.flow-arrow{transform:rotate(90deg)} +.pin-table,.bom-table{font-size:.75rem} +.pin-table th,.pin-table td,.bom-table th,.bom-table td{padding:8px 10px} +.comp-specs{grid-template-columns:1fr} +} diff --git a/docs/design/iot-design.html b/docs/design/iot-design.html new file mode 100644 index 0000000..86610f7 --- /dev/null +++ b/docs/design/iot-design.html @@ -0,0 +1,582 @@ + + + + + +GridShield β€” IoT Hardware Design + + + + + + +
+
+
⚑ Hardware Design Document v2.0
+

GridShield IoT
System Design

+

ESP32 DevKit V1 hardware architecture β€” complete component catalog, wiring paths, circuit schematic, power distribution, and assembly guide.

+
+
+ + +
+
+

πŸ“Š System Overview

+

Key specifications of the GridShield hardware platform

+
+
🧠
ESP32
Dual-Core Xtensa LX6 @ 240MHz
+
πŸ’Ύ
520KB
SRAM + 4MB Flash
+
πŸ“‘
WiFi
802.11 b/g/n + BLE 4.2
+
πŸ”
AES
Hardware Crypto Accelerator
+
+
+
+ + +
+
+

🧩 Component Catalog

+

Detailed specifications for every hardware component in the GridShield system

+
+ +
+
🧠
ESP32 DevKit V1
ESP32-D0WD-V3 Β· 30-pin
+

Main controller with dual-core processor, integrated WiFi/BLE, hardware crypto accelerator, and 34 programmable GPIO pins.

+
+
CPU: 240MHz
+
RAM: 520KB
+
Flash: 4MB
+
ADC: 18ch 12-bit
+
UART: 3 ports
+
I2C: 2 buses
+
VCC: 3.3V
+
USB: CP2102
+
+
+ +
+
⚑
ACS712 Current Sensor
30A Module Β· Hall Effect
+

Hall-effect based linear current sensor. Measures AC/DC up to 30A with 66mV/A sensitivity. Output centered at VCC/2 (1.65V).

+
+
Range: Β±30A
+
Sens: 66mV/A
+
VCC: 5V
+
Out: Analog
+
Pin: GPIO34
+
Bus: ADC1_CH6
+
+
+ +
+
πŸ”Œ
ZMPT101B Voltage Sensor
AC Voltage Transformer
+

Precision AC voltage transformer module. Measures up to 250VAC with isolation. Output through voltage divider to 0-3.3V range.

+
+
Range: 0-250V AC
+
Ratio: 1:1
+
VCC: 5V
+
Out: Analog
+
Pin: GPIO35
+
Bus: ADC1_CH7
+
+
+ +
+
πŸ“Š
PZEM-004T v3.0
AC Energy Meter Module
+

Multi-function AC energy meter. Measures voltage, current, power, energy, frequency, and power factor via Modbus-RTU over UART.

+
+
V: 80-260VAC
+
I: 0-100A (CT)
+
Proto: Modbus
+
Baud: 9600
+
RX: GPIO16
+
TX: GPIO17
+
+
+ +
+
🌑️
DS18B20 Temperature
Waterproof Probe Β· OneWire
+

Digital temperature sensor with Β±0.5Β°C accuracy. OneWire protocol requires single data line with 4.7kΞ© pull-up resistor.

+
+
Range: -55~125Β°C
+
Acc: Β±0.5Β°C
+
Res: 12-bit
+
Proto: OneWire
+
Pin: GPIO27
+
Pull: 4.7kΞ©
+
+
+ +
+
πŸ“
MPU6050 IMU
6-Axis Β· Accelerometer + Gyro
+

6-axis motion sensor for physical tamper detection. I2C interface detects shock, tilt, and orientation changes.

+
+
Accel: Β±16g
+
Gyro: Β±2000Β°/s
+
Proto: I2C
+
Addr: 0x68
+
SDA: GPIO21
+
SCL: GPIO22
+
+
+ +
+
πŸ”΄
Tamper Micro Switch
Normally-Closed Β· Panel Mount
+

Casing intrusion detector. NC contact opens when meter cover is removed, triggering ISR on falling edge with debounce logic.

+
+
Type: NC
+
Trigger: FALLING
+
Pin: GPIO4
+
Pull: 10kΞ© UP
+
Debounce: 50ms
+
ISR: Yes
+
+
+ +
+
πŸ’‘
Status LEDs (Γ—3)
3mm Diffused Β· R/A/G
+

Visual status indicators. Green=OK, Amber=Warning, Red=Tamper Alert. Each with 220Ξ© current-limiting resistor.

+
+
Green: GPIO25
+
Amber: GPIO26
+
Red: GPIO14
+
R: 220Ξ© each
+
If: 20mA
+
Vf: 2.0-3.2V
+
+
+ +
+
πŸ”‹
Backup Capacitor
1000Β΅F 16V Electrolytic
+

Emergency power backup for tamper alert transmission during power cut. Provides ~2-3 seconds of operation at minimal power.

+
+
Cap: 1000Β΅F
+
Voltage: 16V
+
Type: Electrolytic
+
Mon: GPIO32
+
+
+ +
+
+
+ + +
+
+

πŸ”Œ Wiring Schematic Diagram

+

Complete visual wiring between ESP32 and all peripherals with wire color coding

+
+ + + + + + + + + + + + + + + + +ESP32-D0WD +DevKit V1 Β· 30-pin + + +USB + + + +GPIO4 +GPIO16 +GPIO17 +GPIO21 +GPIO22 +GPIO25 +GPIO26 +GPIO27 + + + + + + +GPIO13 +GPIO14 +GPIO25 +GPIO32 +GPIO33 +GPIO34 +GPIO35 +3V3/GND + + + + + + + + +πŸ”΄ Tamper SW +NC Β· 10kΞ© pull-up + + + + + +πŸ“Š PZEM-004T +UART2 Β· 9600baud +RX←GPIO16 TX←GPIO17 + + + + + + + +πŸ“ MPU6050 +I2C Β· 0x68 +SDAβ†’21 SCLβ†’22 INTβ†’13 + + + + +🌑️ DS18B20 +OneWire Β· 4.7kΞ© pull-up + + + + + + +πŸ’‘ Status LEDs β€” 220Ξ© series resistors +🟒 GPIO25 (OK) Β· 🟑 GPIO26 (Warning) Β· πŸ”΄ GPIO14 (Tamper Alert) + + + + +⚑ ACS712 +30A Β· ADC1_CH6 +66mV/A β†’ GPIO34 + + + + +πŸ”Œ ZMPT101B +AC Voltage β†’ GPIO35 + + + + +πŸ”‹ Battery +ADC1_CH4 Β· GPIO32 + + + + +⚑ 3V3 + GND +Power Rail + + + +WIRE LEGEND +Tamper/Power +UART (Modbus) +I2C / OneWire +ADC (Analog) +Battery Monitor +GPIO Output (LED) + + +
+
+
+ + +
+
+

πŸ”— Connection Paths Detail

+

Step-by-step wiring instructions for each sensor connection

+
+ +
+
πŸ”΄ Tamper Switch β†’ ESP32
+
DIGITAL INPUT Β· ISR Β· GPIO4
+
    +
  • Switch COM terminal β†’ ESP32 GPIO4
  • +
  • Switch NC terminal β†’ GND rail
  • +
  • 10kΞ© resistor: GPIO4 β†’ 3V3 (pull-up)
  • +
  • Firmware: INPUT_PULLUP, ISR FALLING edge
  • +
  • Debounce: 50ms software filter
  • +
+
+ +
+
πŸ“Š PZEM-004T β†’ ESP32
+
UART2 Β· MODBUS-RTU Β· 9600 BAUD
+
    +
  • PZEM TX β†’ ESP32 GPIO16 (RX2)
  • +
  • PZEM RX β†’ ESP32 GPIO17 (TX2)
  • +
  • PZEM 5V β†’ 5V rail (VIN)
  • +
  • PZEM GND β†’ GND rail
  • +
  • CT clamp around AC live wire
  • +
+
+ +
+
πŸ“ MPU6050 β†’ ESP32
+
I2C BUS Β· ADDRESS 0x68 Β· 400kHz
+
    +
  • MPU SDA β†’ ESP32 GPIO21
  • +
  • MPU SCL β†’ ESP32 GPIO22
  • +
  • MPU INT β†’ ESP32 GPIO13
  • +
  • MPU VCC β†’ 3V3 rail
  • +
  • MPU GND β†’ GND rail
  • +
  • MPU AD0 β†’ GND (addr 0x68)
  • +
+
+ +
+
🌑️ DS18B20 β†’ ESP32
+
ONEWIRE Β· GPIO27 Β· PARASITIC OK
+
    +
  • DS18B20 Data (yellow) β†’ ESP32 GPIO27
  • +
  • DS18B20 VCC (red) β†’ 3V3 rail
  • +
  • DS18B20 GND (black) β†’ GND rail
  • +
  • 4.7kΞ© resistor: GPIO27 β†’ 3V3 (pull-up)
  • +
+
+ +
+
⚑ ACS712 β†’ ESP32
+
ADC1_CH6 Β· GPIO34 Β· ANALOG INPUT
+
    +
  • ACS712 OUT β†’ ESP32 GPIO34
  • +
  • ACS712 VCC β†’ 5V rail
  • +
  • ACS712 GND β†’ GND rail
  • +
  • AC load wire through sensor hole
  • +
  • Output: VCC/2 Β± (66mV Γ— Amps)
  • +
+
+ +
+
πŸ”Œ ZMPT101B β†’ ESP32
+
ADC1_CH7 Β· GPIO35 Β· VOLTAGE DIVIDER
+
    +
  • ZMPT OUT β†’ Voltage Divider input
  • +
  • R1=100kΞ© from ZMPT OUT to node
  • +
  • R2=10kΞ© from node to GND
  • +
  • Divider node β†’ ESP32 GPIO35
  • +
  • ZMPT VCC β†’ 5V, GND β†’ GND
  • +
+
+ +
+
πŸ’‘ Status LEDs β†’ ESP32
+
GPIO OUTPUT Β· 220Ξ© SERIES
+
    +
  • Green LED anode β†’ 220Ξ© β†’ GPIO25
  • +
  • Amber LED anode β†’ 220Ξ© β†’ GPIO26
  • +
  • Red LED anode β†’ 220Ξ© β†’ GPIO14
  • +
  • All LED cathodes β†’ GND rail
  • +
+
+ +
+
πŸ”‹ Power Monitoring β†’ ESP32
+
ADC Β· GPIO32 + GPIO33
+
    +
  • Backup cap + β†’ voltage divider β†’ GPIO32
  • +
  • Solar panel V+ β†’ divider β†’ GPIO33
  • +
  • Divider: R1=100kΞ©, R2=10kΞ©
  • +
  • Max input to ADC: 3.3V
  • +
+
+ +
+
+
+ + +
+
+

⚑ Power Distribution Tree

+

Power supply architecture and voltage regulation paths

+
+
+
220VAC AC Mains Input (from meter)
+
+
5V DC USB / Power Adapter (5V/2A)
+
+
3.3V AMS1117-3.3 LDO (on DevKit) β†’ ESP32, DS18B20, MPU6050
+
3.3V ADC Reference Voltage (max input)
+
+
5V Direct β†’ ACS712, ZMPT101B, PZEM-004T
+
5V Backup Cap (1000Β΅F) β†’ Emergency tamper alert power
+
+
+
220VAC PZEM-004T voltage sense (galvanic isolation)
+
220VAC ZMPT101B transformer input (galvanic isolation)
+
~30A ACS712 through-hole (pass-through, no contact)
+
+
+
+
+
+ + +
+
+

πŸ“‹ GPIO Pin Mapping

+

Complete pin assignment table for the GridShield hardware design

+
+ + + + + + + + + + + + + + + + + + +
GPIOFunctionDirectionInterfaceWire ColorNotes
GPIO4Tamper SwitchINPUTDigitalπŸ”΄ Red10kΞ© pull-up, ISR FALLING edge
GPIO13MPU6050 INTINPUTDigital🟒 GreenAccelerometer interrupt
GPIO14Status LED (Red)OUTPUTDigitalπŸ”΄ Red220Ξ© series, tamper alert
GPIO16PZEM-004T RXINPUTUART2🟣 PurplePZEM TX β†’ ESP RX2
GPIO17PZEM-004T TXOUTPUTUART2🟣 PurpleESP TX2 β†’ PZEM RX
GPIO21I2C SDAI/OI2C🟒 GreenMPU6050 data line
GPIO22I2C SCLI/OI2C🟑 YellowMPU6050 clock line
GPIO25Status LED (Green)OUTPUTDigital🟒 Green220Ω series, system OK
GPIO26Status LED (Amber)OUTPUTDigital🟑 Yellow220Ω series, warning
GPIO27DS18B20 DataI/OOneWire🟑 Yellow4.7kΩ external pull-up
GPIO32Battery ADCADCADC1_CH4🟠 OrangeBackup cap voltage
GPIO33Solar ADCADCADC1_CH5🟠 OrangeSolar panel voltage
GPIO34ACS712 OutputADCADC1_CH6🟠 OrangeCurrent sensor, input-only
GPIO35ZMPT101B OutputADCADC1_CH7🟠 OrangeVoltage sensor, input-only
+
+
+
+ + +
+
+

πŸ”§ Assembly Guide

+

Step-by-step wiring procedure for building a GridShield node

+
+
1

Prepare Power Rails

Place ESP32 on breadboard center. Connect 3V3 and GND pins to power rails. Add 5V from VIN pin to separate rail. Add 100nF decoupling cap near each sensor VCC pin.

+
2

Wire I2C Bus (MPU6050)

Connect SDA→GPIO21, SCL→GPIO22, INT→GPIO13. Add 4.7kΩ pull-ups on SDA and SCL to 3V3. Power from 3V3 rail. Tie AD0 to GND for address 0x68.

SDA→21SCL→22INT→13
+
3

Wire OneWire Bus (DS18B20)

Connect data wire (yellow) to GPIO27. Add 4.7kΞ© pull-up from GPIO27 to 3V3. Connect red wire to 3V3, black to GND.

DATA→274.7kΩ→3V3
+
4

Wire UART2 (PZEM-004T)

Cross-connect: PZEM TXβ†’ESP GPIO16 (RX2), PZEM RXβ†’ESP GPIO17 (TX2). Power PZEM from 5V rail. Route CT clamp around AC live wire. ⚠️ AC voltage connections require proper isolation.

TX→RX2(16)RX→TX2(17)
+
5

Wire ADC Sensors (ACS712 + ZMPT101B)

ACS712 OUT→GPIO34 directly (output is 0-3.3V range). ZMPT101B: add voltage divider (100kΩ/10kΩ) before GPIO35. Both powered from 5V rail.

ACS→34ZMPT→div→35
+
6

Wire Tamper Switch

Connect NC terminal to GPIO4, COM to GND. Add 10kΞ© pull-up from GPIO4 to 3V3. Configure ISR for FALLING edge with 50ms debounce in firmware.

NC→GPIO4COM→GND10kΩ→3V3
+
7

Wire Status LEDs

For each LED: anode→220Ω resistor→GPIO pin, cathode→GND. Green→GPIO25, Amber→GPIO26, Red→GPIO14.

πŸŸ’β†’25πŸŸ‘β†’26πŸ”΄β†’14
+
8

Add Backup Power + Monitoring

Connect 1000Β΅F capacitor across 5V/GND rail. Add voltage divider (100kΞ©/10kΞ©) from cap to GPIO32 for monitoring. Optional: solar panel divider to GPIO33.

Cap→5V/GNDMon→32
+
+
+
+ + +
+
+

🧾 Bill of Materials

+

Complete component list with estimated pricing (IDR, March 2026)

+
+ + + + + + + + + + + + + + + + + + +
#ComponentSpecificationQtyInterfaceEst. Price
1ESP32 DevKit V1ESP32-D0WD, 30-pin, CP21021β€”Rp 75,000
2ACS712 Current Sensor30A module, 66mV/A1ADCRp 25,000
3ZMPT101B Voltage SensorAC voltage transformer1ADCRp 30,000
4PZEM-004T v3.0AC energy meter (V/I/P/E/F/PF) + CT1UARTRp 85,000
5DS18B20 TemperatureWaterproof probe, Β±0.5Β°C1OneWireRp 15,000
6MPU6050 IMU Module6-axis accel+gyro, I2C1I2CRp 20,000
7Micro Switch (Tamper)NC, panel mount, lever1GPIORp 10,000
8LEDs (R/A/G)3mm, 20mA, diffused3GPIORp 5,000
9Resistor Kit220Ω×3, 10kΩ×2, 100kΩ×2, 4.7kΩ×29β€”Rp 10,000
10Capacitors1000Β΅F/16V + 100nF ceramic Γ—56β€”Rp 15,000
11Breadboard + Jumpers830-point + M/M, M/F, F/F kit1β€”Rp 40,000
12USB CableMicro-USB, data capable1β€”Rp 15,000
13Power Adapter5V/2A, micro-USB or barrel1β€”Rp 25,000
Total Estimated CostRp 370,000
+
+
+
+ + +
+
+

πŸ›‘οΈ 3-Layer Security Architecture

+

Defense-in-depth approach mapped to hardware components

+
+
Layer 1 β€” Physical
Tamper Detection Hardware
Hardware-level intrusion detection using multiple sensor modalities
  • Micro-switch casing sensor (GPIO4, ISR + 50ms debounce)
  • MPU6050 shock & tilt detection (I2C 0x68, INTβ†’GPIO13)
  • DS18B20 thermal anomaly monitoring (OneWire GPIO27)
  • Power-cut detection via backup cap ADC (GPIO32)
  • GPIO wake-on-tamper from deep sleep
+
Layer 2 β€” Network
Cryptographic Security
Production-grade encryption and authentication for all communications
  • ECDSA P-256 signature (micro-ecc library)
  • AES-256-GCM via ESP32 HW accelerator
  • HKDF key derivation + rotation
  • MQTT over TLS 1.2 (WiFi 802.11n)
  • Secure Boot v2 + Flash encryption
+
Layer 3 β€” Application
Analytics & Detection
Intelligent anomaly detection with edge processing
  • Statistical anomaly detector (Οƒ-deviation)
  • PZEM-004T energy data analysis
  • Cross-layer threat correlation
  • Forensic event logging (circular buffer)
  • Evidence preservation with hash chain
+
+
+
+ + +
+
+

πŸ”„ Data Flow Architecture

+

End-to-end data pipeline from physical sensors to cloud dashboard

+
+
+
⚑
Sensors
ACS712 Β· ZMPT101B
DS18B20 Β· MPU6050
PZEM-004T Β· Switch
+
β†’β†’β†’
+
🧠
ESP32
ADC Β· I2C Β· UART
Encrypt Β· Sign
Analyze Β· Log
+
β†’β†’β†’
+
πŸ“‘
MQTT/TLS
WiFi 802.11n
Encrypted payload
Signed packets
+
β†’β†’β†’
+
πŸ–₯️
Backend
FastAPI Β· SQLite
Anomaly Engine
+
β†’β†’β†’
+
πŸ“Š
Dashboard
Vite SPA
Chart.js
Real-time alerts
+
+
+
+
+ + +
+
+

βš™οΈ System Specifications

+

Detailed hardware and software specifications

+
+
MCU SpecificationsESP32
+
ProcessorXtensa LX6 Dual-Core
+
Clock Speed240 MHz
+
SRAM520 KB
+
Flash4 MB (QSPI)
+
WiFi802.11 b/g/n
+
BluetoothBLE 4.2
+
GPIO34 programmable
+
ADC18 Γ— 12-bit SAR
+
CryptoAES / SHA / RSA HW accel
+
RNGHardware TRNG
+
+
Firmware Profilev3.3.0
+
FrameworkESP-IDF v5.5
+
LanguageC++17
+
Binary Size~890 KB
+
Heap AllocZero (static only)
+
Unit Tests186 (20 suites)
+
SecuritySecure Boot v2 + Flash Enc
+
OTASigned images (ECDSA)
+
Power ModesActive / Light / Deep Sleep
+
WatchdogTask WDT + Interrupt WDT
+
ProtocolsMQTT Β· Modbus Β· OneWire Β· I2C
+
+
+
+
+ + + + + diff --git a/firmware/include/common/hardware/sensors/dht11.hpp b/firmware/include/common/hardware/sensors/dht11.hpp new file mode 100644 index 0000000..4994238 --- /dev/null +++ b/firmware/include/common/hardware/sensors/dht11.hpp @@ -0,0 +1,231 @@ +/** + * @file dht11.hpp + * @author zuudevs (zuudevs@gmail.com) + * @brief DHT11 temperature & humidity sensor driver (single-wire protocol) + * @version 1.0 + * @date 2026-04-15 + * + * Digital sensor using proprietary single-wire protocol (NOT Dallas OneWire). + * Range: Temperature 0-50Β°C (Β±2Β°C), Humidity 20-80% (Β±5%). + * + * @copyright Copyright (c) 2026 + */ + +#pragma once + +#include "core/error.hpp" +#include "platform/platform.hpp" + +#include + +// ESP-IDF timing (microsecond precision required) +#if GS_PLATFORM_ESP32 +#include "driver/gpio.h" +#include "esp_log.h" +#include "esp_timer.h" +#include "rom/ets_sys.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#endif + +namespace gridshield::hardware::sensors { + +// ============================================================================ +// DHT11 CONSTANTS +// ============================================================================ +static constexpr uint32_t DHT11_START_SIGNAL_MS = 20; // Pull LOW for 20ms +static constexpr uint32_t DHT11_RESPONSE_TIMEOUT_US = 100; +static constexpr uint32_t DHT11_BIT_THRESHOLD_US = 40; // >40ΞΌs HIGH = bit 1 +static constexpr size_t DHT11_DATA_BITS = 40; // 5 bytes Γ— 8 bits +static constexpr size_t DHT11_DATA_BYTES = 5; +static constexpr uint32_t DHT11_MIN_INTERVAL_MS = 1000; // Min 1s between reads + +// ============================================================================ +// DHT11 READING +// ============================================================================ +struct DHT11Reading +{ + int16_t temperature_c10{}; // Temperature in 0.1Β°C (e.g., 253 = 25.3Β°C) + uint16_t humidity_x10{}; // Humidity in 0.1% (e.g., 650 = 65.0%) +}; + +// ============================================================================ +// DHT11 CONFIGURATION +// ============================================================================ +struct DHT11Config +{ + uint8_t pin{}; + + GS_CONSTEXPR DHT11Config() noexcept = default; +}; + +// ============================================================================ +// DHT11 DRIVER +// ============================================================================ +class DHT11Driver +{ +public: + DHT11Driver() noexcept = default; + + /** + * @brief Initialize the GPIO pin for DHT11. + */ + core::Result init(const DHT11Config& config) noexcept + { + config_ = config; + +#if GS_PLATFORM_ESP32 + // Configure as open-drain output (can switch to input for reading) + gpio_config_t io_conf = {}; + io_conf.pin_bit_mask = (1ULL << config_.pin); + io_conf.mode = GPIO_MODE_INPUT_OUTPUT_OD; + io_conf.pull_up_en = GPIO_PULLUP_ENABLE; + io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; + io_conf.intr_type = GPIO_INTR_DISABLE; + + esp_err_t err = gpio_config(&io_conf); + if (err != ESP_OK) { + return GS_MAKE_ERROR(core::ErrorCode::HardwareFailure); + } + + // Set HIGH (idle state) + gpio_set_level(static_cast(config_.pin), 1); +#endif + + initialized_ = true; + return core::Result{}; + } + + /** + * @brief Read temperature and humidity from DHT11. + * + * Blocking call (~25ms). Must wait at least 1 second between reads. + */ + core::Result read() noexcept + { + if (GS_UNLIKELY(!initialized_)) { + return core::Result{GS_MAKE_ERROR(core::ErrorCode::SystemNotInitialized)}; + } + +#if GS_PLATFORM_ESP32 + uint8_t data[DHT11_DATA_BYTES] = {}; + auto gpio_pin = static_cast(config_.pin); + + // === Start Signal: pull LOW for 25ms === + gpio_set_direction(gpio_pin, GPIO_MODE_INPUT); + int idle_level = gpio_get_level(gpio_pin); + + gpio_set_direction(gpio_pin, GPIO_MODE_OUTPUT_OD); + gpio_pullup_en(gpio_pin); + gpio_set_level(gpio_pin, 0); + ets_delay_us(25000); // 25ms β€” some modules need > 18ms + + // --- CRITICAL SECTION: must cover release + response + read --- + portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED; + taskENTER_CRITICAL(&mux); + + // Release bus: let pull-up bring it HIGH + gpio_set_level(gpio_pin, 1); + ets_delay_us(30); + gpio_set_direction(gpio_pin, GPIO_MODE_INPUT); + + int after_release = gpio_get_level(gpio_pin); + + int fail_phase = 0; // 0=ok, 1=wait_response, 2=low_hold, 3=high_hold, 4=bit_read + + // Wait for DHT11 to pull LOW (response ~80ΞΌs) + if (!wait_for_level(gpio_pin, 0, 500)) { fail_phase = 1; } + + // DHT11 holds LOW ~80ΞΌs then goes HIGH + if (!fail_phase && !wait_for_level(gpio_pin, 1, 500)) { fail_phase = 2; } + + // DHT11 holds HIGH ~80ΞΌs then goes LOW (start of first bit) + if (!fail_phase && !wait_for_level(gpio_pin, 0, 500)) { fail_phase = 3; } + + // === Read 40 bits of data === + size_t bits_read = 0; + if (!fail_phase) { + for (size_t i = 0; i < DHT11_DATA_BITS; ++i) { + if (!wait_for_level(gpio_pin, 1, 500)) { fail_phase = 4; break; } + + int64_t t_start = esp_timer_get_time(); + if (!wait_for_level(gpio_pin, 0, 500)) { fail_phase = 4; break; } + int64_t duration = esp_timer_get_time() - t_start; + + size_t byte_idx = i / 8; + size_t bit_idx = 7 - (i % 8); + if (duration > DHT11_BIT_THRESHOLD_US) { + data[byte_idx] |= (1 << bit_idx); + } + bits_read = i + 1; + } + } + + taskEXIT_CRITICAL(&mux); + // --- END CRITICAL SECTION --- + + // Restore output mode (idle HIGH) + gpio_set_direction(gpio_pin, GPIO_MODE_OUTPUT_OD); + gpio_set_level(gpio_pin, 1); + + if (fail_phase) { + ESP_LOGW("DHT11", "DIAG: idle=%d after_release=%d fail_phase=%d bits=%d", + idle_level, after_release, fail_phase, (int)bits_read); + return core::Result{GS_MAKE_ERROR(core::ErrorCode::SensorReadFailure)}; + } + + // Restore output mode (idle HIGH) + gpio_set_direction(gpio_pin, GPIO_MODE_OUTPUT); + gpio_set_level(gpio_pin, 1); + + // === Verify checksum === + uint8_t checksum = data[0] + data[1] + data[2] + data[3]; + if (checksum != data[4]) { + return core::Result{GS_MAKE_ERROR(core::ErrorCode::IntegrityViolation)}; + } + + // === Parse reading === + DHT11Reading reading{}; + // data[0] = humidity integer, data[1] = humidity decimal + reading.humidity_x10 = static_cast(data[0] * 10 + data[1]); + // data[2] = temperature integer, data[3] = temperature decimal + reading.temperature_c10 = static_cast(data[2] * 10 + data[3]); + + return core::Result{reading}; + +#else + // Non-ESP32 stub + DHT11Reading reading{}; + reading.temperature_c10 = 250; // 25.0Β°C + reading.humidity_x10 = 600; // 60.0% + return core::Result{reading}; +#endif + } + + GS_NODISCARD bool is_initialized() const noexcept + { + return initialized_; + } + +private: +#if GS_PLATFORM_ESP32 + /** + * @brief Wait for a specific GPIO level with timeout. + */ + static bool wait_for_level(gpio_num_t pin, int level, uint32_t timeout_us) noexcept + { + int64_t deadline = esp_timer_get_time() + timeout_us; + while (gpio_get_level(pin) != level) { + if (esp_timer_get_time() > deadline) { + return false; + } + } + return true; + } +#endif + + DHT11Config config_{}; + bool initialized_{false}; +}; + +} // namespace gridshield::hardware::sensors diff --git a/firmware/main/Kconfig.projbuild b/firmware/main/Kconfig.projbuild index 73e0413..b638198 100644 --- a/firmware/main/Kconfig.projbuild +++ b/firmware/main/Kconfig.projbuild @@ -1,11 +1,12 @@ -menu "GridShield Demo Config" +menu "GridShield Configuration" config GRIDSHIELD_DEMO_MODE - bool "Enable Demo Mode (no sensors, simulated data)" - default y + bool "Enable QEMU Simulation Mode (disable for real hardware)" + default n help - Enable demo mode. The ESP32 will generate simulated meter readings - and send them to the backend over WiFi. No sensors required. + When disabled (default): Production mode β€” ESP32 runs with real + sensors, WiFi, and full security pipeline. + When enabled: QEMU simulation mode with mock platform. config GRIDSHIELD_WIFI_SSID string "WiFi SSID" diff --git a/firmware/main/app_main.cpp b/firmware/main/app_main.cpp index 5348c7e..f8bd8ee 100644 --- a/firmware/main/app_main.cpp +++ b/firmware/main/app_main.cpp @@ -118,7 +118,7 @@ static bool validate_config(const SystemConfig& config) void app_main(void) { ESP_LOGI(TAG, "=============================================="); - ESP_LOGI(TAG, "GridShield v2.1 [ESP32 - QEMU Simulation]"); + ESP_LOGI(TAG, "GridShield v3.3.0 [ESP32 - QEMU Simulation]"); ESP_LOGI(TAG, "Platform: ESP-IDF + QEMU + mbedTLS"); ESP_LOGI(TAG, "=============================================="); diff --git a/firmware/main/demo_main.cpp b/firmware/main/demo_main.cpp index 33eacec..1c71b8c 100644 --- a/firmware/main/demo_main.cpp +++ b/firmware/main/demo_main.cpp @@ -1,17 +1,18 @@ /** - * @file demo_main.cpp - * @brief GridShield Demo Firmware β€” WiFi + HTTP to Backend + * @file prod_main.cpp + * @brief GridShield Production Firmware β€” PZEM-004T + DHT11 + Relay + Tamper * - * Runs on real ESP32 hardware WITHOUT sensors. Generates simulated meter - * readings, tamper alerts, and anomaly events, then POSTs them to the - * GridShield FastAPI backend over WiFi. + * Production entry point for ESP32 DevKit V1 with actual hardware: + * - PZEM-004T v3.0 (energy meter β€” V, I, P, E, freq, PF via UART) + * - PZKHCT CT Clamp (current transformer for PZEM) + * - DHT11 (temperature & humidity) + * - JQC-3FF-S-Z Relay (AC load control) + * - Buzzer (GPIO25 β€” alert/alarm tone) + * - Tamper switch with pull-up (enclosure protection) + * - MCB IC60N (AC circuit breaker β€” external protection) + * - Terminal blocks (AC wiring) * - * Build: - * cd firmware - * idf.py set-target esp32 - * idf.py menuconfig (set WiFi creds in "GridShield Demo Config") - * idf.py build - * idf.py -p COM3 flash monitor + * NO mock/demo/Arduino dependencies β€” 100% ESP-IDF native. * * @copyright Copyright (c) 2026 zuudevs */ @@ -21,7 +22,13 @@ #include #include #include -#include + +// GridShield Core +#include "core/system.hpp" +#include "hardware/sensor_manager.hpp" +#include "hardware/sensors/dht11.hpp" +#include "platform/esp32_platform.hpp" +#include "platform/platform.hpp" // ESP-IDF #include "esp_log.h" @@ -34,12 +41,15 @@ #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/event_groups.h" +#include "driver/gpio.h" +#include "driver/uart.h" +#include "driver/ledc.h" #include "lwip/err.h" #include "lwip/sys.h" #include "cJSON.h" // ============================================================================ -// CONFIGURATION β€” Edit these before flashing! +// CONFIGURATION β€” from menuconfig // ============================================================================ #define WIFI_SSID CONFIG_GRIDSHIELD_WIFI_SSID #define WIFI_PASS CONFIG_GRIDSHIELD_WIFI_PASS @@ -48,18 +58,248 @@ #define METER_ID 0x00000000000003E9ULL // 1001 decimal #define READING_INTERVAL_S 5 // send reading every 5s -#define ALERT_CHANCE_PCT 8 // 8% chance per cycle -#define ANOMALY_CHANCE_PCT 5 // 5% chance per cycle +#define VERSION_STRING "3.3.1" + +// ============================================================================ +// PIN ASSIGNMENTS β€” ESP32 DevKit V1 +// ============================================================================ +// +// Komponen Interface ESP32 Pin GPIO +// ───────────────────────────────────────────────────── +// PZEM-004T + CT Clamp UART2 TX=GPIO17 RX=GPIO16 +// DHT11 Digital GPIO13 +// JQC-3FF Relay Digital OUT GPIO26 +// Buzzer PWM (LEDC) GPIO25 +// Tamper Switch Digital IN GPIO4 (pull-up) +// LED Indicator Digital OUT GPIO2 (built-in) +// +// ============================================================================ + +static constexpr uint8_t PIN_PZEM_TX = 17; +static constexpr uint8_t PIN_PZEM_RX = 16; +static constexpr uint8_t PIN_DHT11 = 13; +static constexpr uint8_t PIN_RELAY = 26; +static constexpr uint8_t PIN_BUZZER = 25; +static constexpr uint8_t PIN_TAMPER = 4; +static constexpr uint8_t PIN_LED = 2; + +static const char* TAG = "GridShield"; -static const char* TAG = "GS-Demo"; +using namespace gridshield; // ============================================================================ -// WiFi Event Handling +// ESP32-NATIVE PLATFORM IMPLEMENTATIONS (no mock, no Arduino) +// ============================================================================ + +class Esp32Time : public platform::IPlatformTime +{ +public: + core::timestamp_t get_timestamp_ms() noexcept override + { + return static_cast(xTaskGetTickCount()) * portTICK_PERIOD_MS; + } + void delay_ms(uint32_t milliseconds) noexcept override + { + vTaskDelay(pdMS_TO_TICKS(milliseconds)); + } +}; + +class Esp32GPIO : public platform::IPlatformGPIO +{ +public: + core::Result configure(uint8_t pin, PinMode mode) noexcept override + { + gpio_config_t io_conf = {}; + io_conf.pin_bit_mask = (1ULL << pin); + io_conf.intr_type = GPIO_INTR_DISABLE; + switch (mode) { + case PinMode::Input: + io_conf.mode = GPIO_MODE_INPUT; + break; + case PinMode::Output: + io_conf.mode = GPIO_MODE_OUTPUT; + break; + case PinMode::InputPullup: + io_conf.mode = GPIO_MODE_INPUT; + io_conf.pull_up_en = GPIO_PULLUP_ENABLE; + break; + case PinMode::InputPulldown: + io_conf.mode = GPIO_MODE_INPUT; + io_conf.pull_down_en = GPIO_PULLDOWN_ENABLE; + break; + } + esp_err_t err = gpio_config(&io_conf); + if (err != ESP_OK) return GS_MAKE_ERROR(core::ErrorCode::HardwareFailure); + return core::Result{}; + } + core::Result read(uint8_t pin) noexcept override + { + return core::Result(gpio_get_level(static_cast(pin)) != 0); + } + core::Result write(uint8_t pin, bool value) noexcept override + { + gpio_set_level(static_cast(pin), value ? 1 : 0); + return core::Result{}; + } +}; + +class Esp32Interrupt : public platform::IPlatformInterrupt +{ +public: + Esp32Interrupt() noexcept + { + memset(callbacks_, 0, sizeof(callbacks_)); + memset(contexts_, 0, sizeof(contexts_)); + memset(enabled_, 0, sizeof(enabled_)); + } + core::Result attach(uint8_t pin, TriggerMode mode, + InterruptCallback callback, void* context) noexcept override + { + if (!callback) return GS_MAKE_ERROR(core::ErrorCode::InvalidParameter); + callbacks_[pin] = callback; + contexts_[pin] = context; + gpio_int_type_t isr_type = GPIO_INTR_ANYEDGE; + switch (mode) { + case TriggerMode::Rising: isr_type = GPIO_INTR_POSEDGE; break; + case TriggerMode::Falling: isr_type = GPIO_INTR_NEGEDGE; break; + case TriggerMode::Change: isr_type = GPIO_INTR_ANYEDGE; break; + case TriggerMode::Low: isr_type = GPIO_INTR_LOW_LEVEL; break; + case TriggerMode::High: isr_type = GPIO_INTR_HIGH_LEVEL; break; + } + gpio_set_intr_type(static_cast(pin), isr_type); + gpio_install_isr_service(0); + gpio_isr_handler_add(static_cast(pin), gpio_isr_bridge, (void*)(uintptr_t)pin); + return core::Result{}; + } + core::Result detach(uint8_t pin) noexcept override + { + gpio_isr_handler_remove(static_cast(pin)); + callbacks_[pin] = nullptr; + contexts_[pin] = nullptr; + return core::Result{}; + } + core::Result enable(uint8_t pin) noexcept override + { + enabled_[pin] = true; + gpio_intr_enable(static_cast(pin)); + return core::Result{}; + } + core::Result disable(uint8_t pin) noexcept override + { + enabled_[pin] = false; + gpio_intr_disable(static_cast(pin)); + return core::Result{}; + } + static Esp32Interrupt* instance; +private: + static void gpio_isr_bridge(void* arg) + { + uint8_t pin = static_cast((uintptr_t)arg); + if (instance && instance->enabled_[pin] && instance->callbacks_[pin]) { + instance->callbacks_[pin](instance->contexts_[pin]); + } + } + InterruptCallback callbacks_[40]; + void* contexts_[40]; + bool enabled_[40]; +}; +Esp32Interrupt* Esp32Interrupt::instance = nullptr; + +class Esp32Comm : public platform::IPlatformComm +{ +public: + core::Result init() noexcept override { connected_ = true; return core::Result{}; } + core::Result shutdown() noexcept override { connected_ = false; return core::Result{}; } + core::Result send(const uint8_t*, size_t length) noexcept override + { return core::Result(length); } + core::Result receive(uint8_t*, size_t, uint32_t) noexcept override + { return core::Result(GS_MAKE_ERROR(core::ErrorCode::NetworkTimeout)); } + bool is_connected() noexcept override { return connected_; } +private: + bool connected_{false}; +}; + +/** + * ESP-IDF UART driver for PZEM-004T Modbus communication. + */ +class Esp32UART : public platform::IPlatformUART +{ +public: + core::Result init(uint8_t port, uint32_t baud_rate, + uint8_t tx_pin, uint8_t rx_pin) noexcept override + { + uart_config_t uart_config = {}; + uart_config.baud_rate = static_cast(baud_rate); + uart_config.data_bits = UART_DATA_8_BITS; + uart_config.parity = UART_PARITY_DISABLE; + uart_config.stop_bits = UART_STOP_BITS_1; + uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; + uart_config.source_clk = UART_SCLK_DEFAULT; + + auto uart_num = static_cast(port); + esp_err_t err; + + err = uart_driver_install(uart_num, 256, 256, 0, NULL, 0); + if (err != ESP_OK) { + ESP_LOGE("UART", "driver install failed: %s", esp_err_to_name(err)); + return GS_MAKE_ERROR(core::ErrorCode::UARTError); + } + + err = uart_param_config(uart_num, &uart_config); + if (err != ESP_OK) { + ESP_LOGE("UART", "param config failed: %s", esp_err_to_name(err)); + return GS_MAKE_ERROR(core::ErrorCode::UARTError); + } + + err = uart_set_pin(uart_num, tx_pin, rx_pin, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); + if (err != ESP_OK) { + ESP_LOGE("UART", "set pin failed: %s", esp_err_to_name(err)); + return GS_MAKE_ERROR(core::ErrorCode::UARTError); + } + + ESP_LOGI("UART", "Port %d initialized (baud=%lu, TX=GP%d, RX=GP%d)", + port, (unsigned long)baud_rate, tx_pin, rx_pin); + return core::Result{}; + } + + core::Result write(uint8_t port, const uint8_t* data, size_t length) noexcept override + { + auto uart_num = static_cast(port); + int written = uart_write_bytes(uart_num, data, length); + if (written < 0) { + return core::Result(GS_MAKE_ERROR(core::ErrorCode::UARTError)); + } + return core::Result(static_cast(written)); + } + + core::Result read(uint8_t port, uint8_t* buffer, size_t max_length, + uint32_t timeout_ms) noexcept override + { + auto uart_num = static_cast(port); + int read_bytes = uart_read_bytes(uart_num, buffer, max_length, + pdMS_TO_TICKS(timeout_ms)); + if (read_bytes < 0) { + return core::Result(GS_MAKE_ERROR(core::ErrorCode::UARTError)); + } + if (read_bytes == 0) { + return core::Result(GS_MAKE_ERROR(core::ErrorCode::SensorTimeout)); + } + return core::Result(static_cast(read_bytes)); + } + + core::Result shutdown(uint8_t port) noexcept override + { + uart_driver_delete(static_cast(port)); + return core::Result{}; + } +}; + +// ============================================================================ +// WiFi // ============================================================================ static EventGroupHandle_t s_wifi_event_group; #define WIFI_CONNECTED_BIT BIT0 #define WIFI_FAIL_BIT BIT1 - static int s_retry_num = 0; #define MAX_RETRY 10 @@ -72,10 +312,9 @@ static void wifi_event_handler(void* arg, esp_event_base_t event_base, if (s_retry_num < MAX_RETRY) { esp_wifi_connect(); s_retry_num++; - ESP_LOGW(TAG, "Retrying WiFi connection (%d/%d)...", s_retry_num, MAX_RETRY); + ESP_LOGW(TAG, "Retrying WiFi (%d/%d)...", s_retry_num, MAX_RETRY); } else { xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT); - ESP_LOGE(TAG, "WiFi connection failed after %d retries", MAX_RETRY); } } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data; @@ -88,20 +327,17 @@ static void wifi_event_handler(void* arg, esp_event_base_t event_base, static bool wifi_init_sta(void) { s_wifi_event_group = xEventGroupCreate(); - ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default()); esp_netif_create_default_wifi_sta(); - wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&cfg)); - esp_event_handler_instance_t instance_any_id; - esp_event_handler_instance_t instance_got_ip; + esp_event_handler_instance_t inst_any, inst_ip; ESP_ERROR_CHECK(esp_event_handler_instance_register( - WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, &instance_any_id)); + WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, &inst_any)); ESP_ERROR_CHECK(esp_event_handler_instance_register( - IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL, &instance_got_ip)); + IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL, &inst_ip)); wifi_config_t wifi_config = {}; strncpy((char*)wifi_config.sta.ssid, WIFI_SSID, sizeof(wifi_config.sta.ssid) - 1); @@ -113,20 +349,13 @@ static bool wifi_init_sta(void) ESP_ERROR_CHECK(esp_wifi_start()); ESP_LOGI(TAG, "Connecting to WiFi SSID: %s ...", WIFI_SSID); - EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE, portMAX_DELAY); - - if (bits & WIFI_CONNECTED_BIT) { - ESP_LOGI(TAG, "WiFi connected successfully"); - return true; - } - ESP_LOGE(TAG, "WiFi connection FAILED"); - return false; + return (bits & WIFI_CONNECTED_BIT) != 0; } // ============================================================================ -// HTTP POST Helper +// HTTP POST // ============================================================================ static bool http_post_json(const char* path, const char* json_str) { @@ -139,10 +368,7 @@ static bool http_post_json(const char* path, const char* json_str) config.timeout_ms = 5000; esp_http_client_handle_t client = esp_http_client_init(&config); - if (!client) { - ESP_LOGE(TAG, "HTTP client init failed"); - return false; - } + if (!client) return false; esp_http_client_set_header(client, "Content-Type", "application/json"); esp_http_client_set_post_field(client, json_str, strlen(json_str)); @@ -155,118 +381,167 @@ static bool http_post_json(const char* path, const char* json_str) ESP_LOGE(TAG, "POST %s failed: %s", path, esp_err_to_name(err)); return false; } - if (status >= 200 && status < 300) { - ESP_LOGI(TAG, "POST %s β†’ %d OK", path, status); + ESP_LOGI(TAG, "POST %s -> %d OK", path, status); return true; } - - ESP_LOGW(TAG, "POST %s β†’ %d", path, status); + ESP_LOGW(TAG, "POST %s -> %d", path, status); return false; } // ============================================================================ -// Simulated Data Generators +// Relay Control +// ============================================================================ +static bool relay_state = false; + +static void relay_init(void) +{ + gpio_config_t io_conf = {}; + io_conf.pin_bit_mask = (1ULL << PIN_RELAY); + io_conf.mode = GPIO_MODE_OUTPUT; + io_conf.pull_up_en = GPIO_PULLUP_DISABLE; + io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; + gpio_config(&io_conf); + gpio_set_level(static_cast(PIN_RELAY), 0); // OFF by default + relay_state = false; +} + +static void relay_set(bool on) +{ + gpio_set_level(static_cast(PIN_RELAY), on ? 1 : 0); + relay_state = on; + ESP_LOGI(TAG, "Relay: %s", on ? "ON" : "OFF"); +} + +// ============================================================================ +// Buzzer (LEDC PWM on GPIO25) // ============================================================================ -static uint32_t random_range(uint32_t min_val, uint32_t max_val) +static void buzzer_init(void) +{ + ledc_timer_config_t timer_conf = {}; + timer_conf.speed_mode = LEDC_LOW_SPEED_MODE; + timer_conf.duty_resolution = LEDC_TIMER_8_BIT; + timer_conf.timer_num = LEDC_TIMER_0; + timer_conf.freq_hz = 2000; + timer_conf.clk_cfg = LEDC_AUTO_CLK; + ledc_timer_config(&timer_conf); + + ledc_channel_config_t ch_conf = {}; + ch_conf.gpio_num = PIN_BUZZER; + ch_conf.speed_mode = LEDC_LOW_SPEED_MODE; + ch_conf.channel = LEDC_CHANNEL_0; + ch_conf.timer_sel = LEDC_TIMER_0; + ch_conf.duty = 0; + ch_conf.hpoint = 0; + ledc_channel_config(&ch_conf); +} + +static void buzzer_tone(uint32_t freq_hz, uint32_t duration_ms) { - return min_val + (esp_random() % (max_val - min_val + 1)); + ledc_set_freq(LEDC_LOW_SPEED_MODE, LEDC_TIMER_0, freq_hz); + ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 128); // 50% duty + ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0); + vTaskDelay(pdMS_TO_TICKS(duration_ms)); + ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 0); + ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0); } -static void send_meter_reading(uint64_t meter_id, int cycle) +static void buzzer_beep(int count, uint32_t freq_hz, uint32_t on_ms, uint32_t off_ms) { - // Simulate daily pattern based on cycle count - int sim_hour = (cycle * READING_INTERVAL_S / 3600) % 24; - float factor; - if (sim_hour >= 6 && sim_hour <= 9) factor = 1.35f; // morning peak - else if (sim_hour >= 10 && sim_hour <= 17) factor = 0.95f; // daytime - else if (sim_hour >= 18 && sim_hour <= 22) factor = 1.55f; // evening peak - else factor = 0.45f; // night - - // Add randomness - float noise = ((float)(esp_random() % 200) - 100.0f) / 100.0f; // -1.0 to +1.0 - factor += noise * 0.15f; - - int base_energy = 1200; - int energy_wh = (int)(base_energy * factor) + (int)(esp_random() % 100) - 50; - if (energy_wh < 0) energy_wh = 0; - - int voltage_mv = 220000 + (int)(esp_random() % 10000) - 5000; // 215V-225V - int current_ma = (energy_wh * 1000) / 220 + (int)(esp_random() % 200) - 100; - if (current_ma < 0) current_ma = 0; - - int power_factor = 850 + (int)(esp_random() % 150); // 850-1000 - int phase = esp_random() % 2; - - cJSON* json = cJSON_CreateObject(); - // cJSON doesn't support uint64, so use double for meter_id - cJSON_AddNumberToObject(json, "meter_id", (double)meter_id); - cJSON_AddNumberToObject(json, "energy_wh", energy_wh); - cJSON_AddNumberToObject(json, "voltage_mv", voltage_mv); - cJSON_AddNumberToObject(json, "current_ma", current_ma); - cJSON_AddNumberToObject(json, "power_factor", power_factor); - cJSON_AddNumberToObject(json, "phase", phase); - - char* str = cJSON_PrintUnformatted(json); - ESP_LOGI(TAG, "[READING] energy=%d Wh, voltage=%.1f V, current=%d mA", - energy_wh, voltage_mv / 1000.0f, current_ma); - http_post_json("/api/meter-data", str); - free(str); - cJSON_Delete(json); + for (int i = 0; i < count; i++) { + buzzer_tone(freq_hz, on_ms); + if (i < count - 1) vTaskDelay(pdMS_TO_TICKS(off_ms)); + } } -static void send_tamper_alert(uint64_t meter_id) +// Predefined alert patterns +static void buzzer_alert_tamper(void) { buzzer_beep(3, 3000, 500, 200); } // 3x long high +static void buzzer_alert_temp(void) { buzzer_beep(2, 2500, 300, 150); } // 2x medium +static void buzzer_alert_pzem(void) { buzzer_beep(1, 1500, 150, 0); } // 1x short low +static void buzzer_boot_ok(void) { buzzer_beep(2, 2000, 80, 80); } // 2x quick chirp + +// ============================================================================ +// LED Indicator +// ============================================================================ +static void led_init(void) { - static const char* tamper_types[] = { - "CasingOpened", "MagneticInterference", "PowerCutAttempt", - "PhysicalShock", "VibrationDetected", "TemperatureAnomaly" - }; - int idx = esp_random() % 6; - int severity = (esp_random() % 5); // 0-4 - - cJSON* json = cJSON_CreateObject(); - cJSON_AddNumberToObject(json, "meter_id", (double)meter_id); - cJSON_AddStringToObject(json, "tamper_type", tamper_types[idx]); - cJSON_AddNumberToObject(json, "severity", severity); - - char* str = cJSON_PrintUnformatted(json); - ESP_LOGW(TAG, "[ALERT] 🚨 %s (severity=%d)", tamper_types[idx], severity); - http_post_json("/api/tamper-alert", str); - free(str); - cJSON_Delete(json); + gpio_config_t io_conf = {}; + io_conf.pin_bit_mask = (1ULL << PIN_LED); + io_conf.mode = GPIO_MODE_OUTPUT; + gpio_config(&io_conf); } -static void send_anomaly(uint64_t meter_id) +static void led_blink(int count, int ms) { - static const char* anomaly_types[] = { - "UnexpectedDrop", "UnexpectedSpike", "ZeroConsumption", "PatternDeviation" - }; - static const char* severity_levels[] = { "Low", "Medium", "High", "Critical" }; - - int type_idx = esp_random() % 4; - int sev_idx = esp_random() % 4; - - float expected = 800.0f + (float)(esp_random() % 1200); - float deviation_pct = 15.0f + (float)(esp_random() % 80); - int direction = (esp_random() % 2) ? 1 : -1; - float current_val = expected * (1.0f + direction * deviation_pct / 100.0f); - int confidence = 40 + (int)(esp_random() % 60); - - cJSON* json = cJSON_CreateObject(); - cJSON_AddNumberToObject(json, "meter_id", (double)meter_id); - cJSON_AddStringToObject(json, "anomaly_type", anomaly_types[type_idx]); - cJSON_AddStringToObject(json, "severity", severity_levels[sev_idx]); - cJSON_AddNumberToObject(json, "current_value", current_val); - cJSON_AddNumberToObject(json, "expected_value", expected); - cJSON_AddNumberToObject(json, "deviation_percent", deviation_pct); - cJSON_AddNumberToObject(json, "confidence", confidence); - - char* str = cJSON_PrintUnformatted(json); - ESP_LOGW(TAG, "[ANOMALY] ⚠️ %s deviation=%.1f%% confidence=%d%%", - anomaly_types[type_idx], deviation_pct, confidence); - http_post_json("/api/anomalies", str); - free(str); - cJSON_Delete(json); + for (int i = 0; i < count; i++) { + gpio_set_level(static_cast(PIN_LED), 1); + vTaskDelay(pdMS_TO_TICKS(ms)); + gpio_set_level(static_cast(PIN_LED), 0); + vTaskDelay(pdMS_TO_TICKS(ms)); + } +} + +// ============================================================================ +// Platform Instances (static, no heap) +// ============================================================================ +static Esp32Time esp32_time; +static Esp32GPIO esp32_gpio; +static Esp32Interrupt esp32_interrupt; +static Esp32Comm esp32_comm; +static Esp32UART esp32_uart; +static platform::esp32::Esp32Crypto esp32_crypto; +static platform::esp32::Esp32Storage esp32_storage; + +static platform::PlatformServices services; +static GridShieldSystem gs_system; + +// DHT11 sensor (standalone, not in SensorManager) +static hardware::sensors::DHT11Driver dht11; + +// ============================================================================ +// System Configuration +// ============================================================================ +static SystemConfig create_production_config() +{ + SystemConfig config; + config.meter_id = METER_ID; + config.heartbeat_interval_ms = 60000; + config.reading_interval_ms = READING_INTERVAL_S * 1000; + + // ================================================================ + // HARDWARE β€” Komponen yang terpasang: + // 1. PZEM-004T + PZKHCT CT Clamp (UART2) + // 2. DHT11 (GPIO15) β€” dibaca langsung, bukan via SensorManager + // 3. JQC-3FF-S-Z Relay (GPIO26) + // 4. Tamper switch pull-up (GPIO4) + // 5. MCB IC60N (external AC protection) + // 6. Terminal blocks (AC wiring) + // ================================================================ + + // Tamper detection β€” switch on enclosure + config.tamper_config.sensor_pin = PIN_TAMPER; + config.tamper_config.debounce_ms = 50; + + // Baseline consumption profile for anomaly detection + for (size_t i = 0; i < analytics::PROFILE_HISTORY_SIZE; ++i) { + config.baseline_profile.hourly_avg_wh[i] = 1200; + } + config.baseline_profile.daily_avg_wh = 1200; + config.baseline_profile.variance_threshold = 30; + + // --- PZEM-004T Energy meter (UART2) --- + config.sensor_config.enable_pzem004t = true; + config.sensor_config.pzem_config.uart_port = 2; + config.sensor_config.pzem_config.tx_pin = PIN_PZEM_TX; + config.sensor_config.pzem_config.rx_pin = PIN_PZEM_RX; + + // Sensors NOT installed: + config.sensor_config.enable_acs712 = false; // PZEM handles current + config.sensor_config.enable_zmpt101b = false; // PZEM handles voltage + config.sensor_config.enable_ds18b20 = false; // Using DHT11 instead + config.sensor_config.enable_mpu6050 = false; // Not available + + return config; } // ============================================================================ @@ -275,51 +550,253 @@ static void send_anomaly(uint64_t meter_id) extern "C" void app_main(void) { ESP_LOGI(TAG, "=============================================="); - ESP_LOGI(TAG, " GridShield v3.0.0 β€” Demo Mode"); - ESP_LOGI(TAG, " Real ESP32 β†’ WiFi β†’ Backend"); - ESP_LOGI(TAG, " No sensors β€” simulated readings"); + ESP_LOGI(TAG, " GridShield v%s - Production Mode", VERSION_STRING); + ESP_LOGI(TAG, " PZEM-004T + DHT11 + Relay + Buzzer + Tamper"); ESP_LOGI(TAG, "=============================================="); ESP_LOGI(TAG, "Backend: http://%s:%d", BACKEND_HOST, BACKEND_PORT); ESP_LOGI(TAG, "Meter ID: 0x%llX", (unsigned long long)METER_ID); - // Initialize NVS (required for WiFi) - esp_err_t ret = nvs_flash_init(); - if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { - ESP_ERROR_CHECK(nvs_flash_erase()); - ret = nvs_flash_init(); + // ---- Phase 1: NVS Storage ---- + auto nvs_result = esp32_storage.init(); + if (nvs_result.is_error()) { + ESP_LOGE(TAG, "NVS init failed"); + return; + } + ESP_LOGI(TAG, "[OK] NVS storage initialized"); + + // ---- Phase 2: LED + Relay ---- + led_init(); + relay_init(); + buzzer_init(); + led_blink(3, 100); // 3 blinks = starting + ESP_LOGI(TAG, "[OK] LED + Relay + Buzzer initialized (Relay=GPIO%d, Buzzer=GPIO%d)", PIN_RELAY, PIN_BUZZER); + + // ---- Phase 3: Watchdog ---- + auto wdt_result = platform::esp32::Esp32Watchdog::init(30); + if (wdt_result.is_ok()) { + ESP_LOGI(TAG, "[OK] Watchdog timer (30s)"); } - ESP_ERROR_CHECK(ret); - // Connect to WiFi + // ---- Phase 4: WiFi ---- if (!wifi_init_sta()) { - ESP_LOGE(TAG, "Cannot start demo without WiFi. Restarting in 10s..."); + ESP_LOGE(TAG, "WiFi FAILED. Restarting in 10s..."); + led_blink(10, 200); // rapid blink = error vTaskDelay(pdMS_TO_TICKS(10000)); esp_restart(); } + ESP_LOGI(TAG, "[OK] WiFi connected"); + led_blink(2, 300); // 2 slow blinks = connected + buzzer_boot_ok(); // chirp = boot OK + + // ---- Phase 5: DHT11 ---- + hardware::sensors::DHT11Config dht_config; + dht_config.pin = PIN_DHT11; + auto dht_result = dht11.init(dht_config); + if (dht_result.is_ok()) { + ESP_LOGI(TAG, "[OK] DHT11 initialized (GPIO%d)", PIN_DHT11); + } else { + ESP_LOGW(TAG, "[--] DHT11 init failed - continuing without"); + } + + // ---- Phase 6: Platform Services ---- + Esp32Interrupt::instance = &esp32_interrupt; + services.time = &esp32_time; + services.gpio = &esp32_gpio; + services.interrupt = &esp32_interrupt; + services.crypto = &esp32_crypto; + services.storage = &esp32_storage; + services.comm = &esp32_comm; + services.uart = &esp32_uart; + + // ---- Phase 7: GridShield System ---- + SystemConfig config = create_production_config(); + auto result = gs_system.initialize(config, services); + if (result.is_error()) { + ESP_LOGE(TAG, "System init failed (code=%d)", static_cast(result.error().code)); + return; + } + ESP_LOGI(TAG, "[OK] GridShield system initialized"); + + // ---- Phase 8: SensorManager (manual init β€” system.cpp doesn't do it) ---- + auto sm_result = gs_system.sensors().initialize(services, config.sensor_config); + if (sm_result.is_ok()) { + ESP_LOGI(TAG, "[OK] SensorManager initialized (PZEM=%s)", + config.sensor_config.enable_pzem004t ? "ON" : "OFF"); + } else { + ESP_LOGW(TAG, "[--] SensorManager init failed (code=%d) β€” continuing", + static_cast(sm_result.error().code)); + } + + result = gs_system.start(); + if (result.is_error()) { + ESP_LOGE(TAG, "System start failed"); + return; + } + + // Turn relay ON β€” power is flowing through MCB + relay_set(true); ESP_LOGI(TAG, "=============================================="); - ESP_LOGI(TAG, " Demo started! Sending data every %ds", READING_INTERVAL_S); + ESP_LOGI(TAG, " PRODUCTION LOOP RUNNING"); + ESP_LOGI(TAG, " PZEM-004T: GPIO%d(TX) GPIO%d(RX)", PIN_PZEM_TX, PIN_PZEM_RX); + ESP_LOGI(TAG, " DHT11: GPIO%d", PIN_DHT11); + ESP_LOGI(TAG, " Relay: GPIO%d (ON)", PIN_RELAY); + ESP_LOGI(TAG, " Tamper: GPIO%d (ACTIVE)", PIN_TAMPER); + ESP_LOGI(TAG, " Interval: %ds", READING_INTERVAL_S); ESP_LOGI(TAG, "=============================================="); - // Main loop β€” send data to backend + // ---- Main Loop ---- int cycle = 0; + bool prev_tamper = false; + while (true) { cycle++; + platform::esp32::Esp32Watchdog::feed(); + + // === Step 1: Process security cycle === + gs_system.process_cycle(); + + // === Step 2: Read PZEM-004T (via SensorManager) === + hardware::SensorData sensor_data{}; + bool pzem_ok = false; + + if (gs_system.sensors().is_initialized()) { + auto sr = gs_system.sensors().read_all(); + if (sr.is_ok()) { + sensor_data = sr.value(); + pzem_ok = sensor_data.pzem_available; + if (!pzem_ok) { + ESP_LOGW(TAG, "[%d] PZEM: SensorManager OK but pzem_available=false", cycle); + } + } else { + ESP_LOGW(TAG, "[%d] PZEM: read_all failed (code=%d)", + cycle, static_cast(sr.error().code)); + } + } else { + ESP_LOGW(TAG, "[%d] PZEM: SensorManager NOT initialized! uart=%p", + cycle, (void*)services.uart); + } - // Always send a meter reading - send_meter_reading(METER_ID, cycle); + if (!pzem_ok) { + ESP_LOGW(TAG, "[%d] PZEM-004T read FAILED -- skipping", cycle); + buzzer_alert_pzem(); + } - // Occasionally send a tamper alert (~8% chance) - if ((esp_random() % 100) < ALERT_CHANCE_PCT) { - send_tamper_alert(METER_ID); + // === Step 3: Read DHT11 (temperature + humidity) === + int16_t dht_temp_c10 = 0; + uint16_t dht_humidity_x10 = 0; + bool dht_ok = false; + + if (dht11.is_initialized()) { + auto dht_reading = dht11.read(); + if (dht_reading.is_ok()) { + auto& d = dht_reading.value(); + dht_temp_c10 = d.temperature_c10; + dht_humidity_x10 = d.humidity_x10; + sensor_data.temperature_c10 = dht_temp_c10; + dht_ok = true; + } else { + ESP_LOGW(TAG, "[%d] DHT11 read FAILED (code=%d, line=%lu, file=%s)", + cycle, + static_cast(dht_reading.error().code), + (unsigned long)dht_reading.error().line, + dht_reading.error().file ? dht_reading.error().file : "?"); + } + } else { + ESP_LOGW(TAG, "[%d] DHT11 NOT initialized!", cycle); + } + + // === Step 4: Send reading to backend (only real data) === + if (pzem_ok) { + cJSON* json = cJSON_CreateObject(); + cJSON_AddNumberToObject(json, "meter_id", (double)METER_ID); + cJSON_AddNumberToObject(json, "energy_wh", sensor_data.energy_wh); + cJSON_AddNumberToObject(json, "voltage_mv", sensor_data.voltage_mv); + cJSON_AddNumberToObject(json, "current_ma", sensor_data.current_ma); + cJSON_AddNumberToObject(json, "power_factor", sensor_data.power_factor_100); + cJSON_AddNumberToObject(json, "power_mw", sensor_data.power_mw); + cJSON_AddNumberToObject(json, "phase", 0); + + if (dht_ok) { + cJSON_AddNumberToObject(json, "temperature_c", dht_temp_c10 / 10.0f); + cJSON_AddNumberToObject(json, "humidity_pct", dht_humidity_x10 / 10.0f); + } + + cJSON_AddBoolToObject(json, "relay_on", relay_state); + + char* str = cJSON_PrintUnformatted(json); + + ESP_LOGI(TAG, "---- Reading #%d ----", cycle); + ESP_LOGI(TAG, " %.1fV | %.2fA | %.1fW | %luWh | PF %.2f", + sensor_data.voltage_mv / 1000.0f, + sensor_data.current_ma / 1000.0f, + sensor_data.power_mw / 1000.0f, + (unsigned long)sensor_data.energy_wh, + sensor_data.power_factor_100 / 100.0f); + + if (dht_ok) { + ESP_LOGI(TAG, " %.1f degC | %.1f%% RH", + dht_temp_c10 / 10.0f, dht_humidity_x10 / 10.0f); + } + + http_post_json("/api/meter-data", str); + free(str); + cJSON_Delete(json); } - // Occasionally send an anomaly (~5% chance) - if ((esp_random() % 100) < ANOMALY_CHANCE_PCT) { - send_anomaly(METER_ID); + // === Step 5: Security pipeline === + core::MeterReading reading = hardware::SensorManager::to_meter_reading( + sensor_data, services.time->get_timestamp_ms()); + reading.power_factor = static_cast(sensor_data.power_factor_100); + gs_system.send_meter_reading(reading); + + // === Step 6: Tamper detection === + bool tamper_now = (gs_system.get_state() == core::SystemState::Tampered); + if (tamper_now && !prev_tamper) { + ESP_LOGE(TAG, "TAMPER DETECTED!"); + // Emergency: cut relay + alarm + relay_set(false); + buzzer_alert_tamper(); + led_blink(5, 100); + + cJSON* alert = cJSON_CreateObject(); + cJSON_AddNumberToObject(alert, "meter_id", (double)METER_ID); + cJSON_AddStringToObject(alert, "tamper_type", "PhysicalTamper"); + cJSON_AddNumberToObject(alert, "severity", 4); + char* s = cJSON_PrintUnformatted(alert); + http_post_json("/api/tamper-alert", s); + free(s); + cJSON_Delete(alert); } + prev_tamper = tamper_now; + + // === Step 7: Temperature protection === + if (dht_ok && dht_temp_c10 > 600) { // > 60 degC + ESP_LOGW(TAG, "HIGH TEMP %.1fC -- cutting relay!", dht_temp_c10 / 10.0f); + relay_set(false); + buzzer_alert_temp(); + + cJSON* alert = cJSON_CreateObject(); + cJSON_AddNumberToObject(alert, "meter_id", (double)METER_ID); + cJSON_AddStringToObject(alert, "tamper_type", "TemperatureAnomaly"); + cJSON_AddNumberToObject(alert, "severity", 3); + cJSON_AddNumberToObject(alert, "temperature_c", dht_temp_c10 / 10.0f); + char* s = cJSON_PrintUnformatted(alert); + http_post_json("/api/tamper-alert", s); + free(s); + cJSON_Delete(alert); + } + + // LED heartbeat + gpio_set_level(static_cast(PIN_LED), 1); + vTaskDelay(pdMS_TO_TICKS(50)); + gpio_set_level(static_cast(PIN_LED), 0); + + ESP_LOGD(TAG, "--- Cycle %d | State: %d | Mode: %d ---", + cycle, + static_cast(gs_system.get_state()), + static_cast(gs_system.get_mode())); - ESP_LOGI(TAG, "--- Cycle %d complete, sleeping %ds ---", cycle, READING_INTERVAL_S); vTaskDelay(pdMS_TO_TICKS(READING_INTERVAL_S * 1000)); } } diff --git a/firmware/sdkconfig b/firmware/sdkconfig index a9225fe..f816538 100644 --- a/firmware/sdkconfig +++ b/firmware/sdkconfig @@ -431,6 +431,16 @@ CONFIG_PARTITION_TABLE_OFFSET=0x8000 CONFIG_PARTITION_TABLE_MD5=y # end of Partition Table +# +# GridShield Configuration +# +CONFIG_GRIDSHIELD_DEMO_MODE=y +CONFIG_GRIDSHIELD_WIFI_SSID="Mr Jum corporation" +CONFIG_GRIDSHIELD_WIFI_PASS="No Limit 231." +CONFIG_GRIDSHIELD_BACKEND_HOST="192.168.1.14" +CONFIG_GRIDSHIELD_BACKEND_PORT=8000 +# end of GridShield Configuration + # # Compiler options # @@ -1111,7 +1121,7 @@ CONFIG_ESP_SYSTEM_PANIC_REBOOT_DELAY_SECONDS=0 CONFIG_ESP_SYSTEM_EVENT_QUEUE_SIZE=32 CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=2304 -CONFIG_ESP_MAIN_TASK_STACK_SIZE=3584 +CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 CONFIG_ESP_MAIN_TASK_AFFINITY_CPU0=y # CONFIG_ESP_MAIN_TASK_AFFINITY_CPU1 is not set # CONFIG_ESP_MAIN_TASK_AFFINITY_NO_AFFINITY is not set @@ -2135,7 +2145,7 @@ CONFIG_ESP32_PANIC_PRINT_REBOOT=y # CONFIG_ESP32_PANIC_GDBSTUB is not set CONFIG_SYSTEM_EVENT_QUEUE_SIZE=32 CONFIG_SYSTEM_EVENT_TASK_STACK_SIZE=2304 -CONFIG_MAIN_TASK_STACK_SIZE=3584 +CONFIG_MAIN_TASK_STACK_SIZE=8192 CONFIG_CONSOLE_UART_DEFAULT=y # CONFIG_CONSOLE_UART_CUSTOM is not set # CONFIG_CONSOLE_UART_NONE is not set diff --git a/firmware/sdkconfig.defaults b/firmware/sdkconfig.defaults index 3afa265..9690504 100644 --- a/firmware/sdkconfig.defaults +++ b/firmware/sdkconfig.defaults @@ -28,3 +28,6 @@ CONFIG_NVS_COMPATIBLE_ENCRYPTION=n # === Security: NVS Encryption === # CONFIG_NVS_ENCRYPTION=y + +# === Main Task Stack (default 3584 too small for UART + DHT11 + HTTP) === +CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 diff --git a/firmware/test_app/main/test_main.cpp b/firmware/test_app/main/test_main.cpp index e622797..f8de8a3 100644 --- a/firmware/test_app/main/test_main.cpp +++ b/firmware/test_app/main/test_main.cpp @@ -31,11 +31,7 @@ extern "C" void test_sensors_suite(void); extern "C" void test_ota_power_suite(void); extern "C" void test_forensics_suite(void); extern "C" void test_evidence_store_suite(void); -<<<<<<< HEAD extern "C" void test_alert_dispatcher_suite(void); -======= - ->>>>>>> origin/main extern "C" void app_main(void) { @@ -70,11 +66,7 @@ extern "C" void app_main(void) test_ota_power_suite(); test_forensics_suite(); test_evidence_store_suite(); -<<<<<<< HEAD test_alert_dispatcher_suite(); -======= - ->>>>>>> origin/main int failures = UNITY_END(); diff --git a/frontend/src/api.js b/frontend/src/api.js index 99d1b28..1bc331a 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -123,7 +123,6 @@ export function exportAnomalies(meterId) { const params = meterId != null ? `?meter_id=${meterId}` : ''; triggerDownload(`/export/anomalies${params}`, 'gridshield_anomalies.csv'); } -<<<<<<< HEAD // ============================================================================ // Notifications @@ -170,5 +169,14 @@ export function getForensicsReports({ meterId, limit = 50 } = {}) { export function getForensicsReport(id) { return request(`/forensics/reports/${id}`); } -======= ->>>>>>> origin/main + +/** Get latest reading (for efficient polling) */ +export function getLatestReading(meterId) { + const params = meterId != null ? `?meter_id=${meterId}` : ''; + return request(`/readings/latest${params}`); +} + +/** Reset all data (readings, alerts, anomalies, notifications) */ +export function resetAllData() { + return fetch(`${BASE}/reset`, { method: 'DELETE' }).then(r => r.json()); +} diff --git a/frontend/src/components/navbar.js b/frontend/src/components/navbar.js index cee6fb2..5db16a4 100644 --- a/frontend/src/components/navbar.js +++ b/frontend/src/components/navbar.js @@ -42,7 +42,7 @@ export function renderNavbar() { `; diff --git a/frontend/src/pages/dashboard.js b/frontend/src/pages/dashboard.js index b61a7a2..1435925 100644 --- a/frontend/src/pages/dashboard.js +++ b/frontend/src/pages/dashboard.js @@ -1,24 +1,62 @@ /** * GridShield β€” Dashboard Page - * KPI cards + energy chart + recent alerts + * Live monitoring with hero cards, voltage chart, alerts, and reset */ -<<<<<<< HEAD -import { getStatus, getReadings, getAlerts, exportReadings, getNotificationSummary, getForensicsReports } from '../api.js'; -======= -import { getStatus, getReadings, getAlerts, exportReadings } from '../api.js'; ->>>>>>> origin/main +import { getStatus, getReadings, getAlerts, getLatestReading, resetAllData, getNotificationSummary } from '../api.js'; import { createLineChart } from '../components/chart.js'; -/** Helper: format timestamp */ -function timeAgo(ts) { - const diff = Date.now() - new Date(ts).getTime(); - const mins = Math.floor(diff / 60000); - if (mins < 1) return 'just now'; - if (mins < 60) return `${mins}m ago`; - const hrs = Math.floor(mins / 60); - if (hrs < 24) return `${hrs}h ago`; - return `${Math.floor(hrs / 24)}d ago`; +/** Bilingual labels */ +const L = { + voltage: { id: 'Tegangan', en: 'Voltage' }, + current: { id: 'Arus', en: 'Current' }, + power: { id: 'Daya', en: 'Power' }, + energy: { id: 'Energi', en: 'Energy' }, + temperature: { id: 'Suhu Panel', en: 'Temperature' }, + humidity: { id: 'Kelembapan', en: 'Humidity' }, + relay: { id: 'Status Relay', en: 'Relay Status' }, + pf: { id: 'Faktor Daya', en: 'Power Factor' }, + liveMonitor: { id: 'Monitor Langsung', en: 'Live Monitor' }, + voltTrend: { id: 'Tren Tegangan', en: 'Voltage Trend' }, + alerts: { id: 'Peringatan Terbaru', en: 'Recent Alerts' }, + stats: { id: 'Statistik', en: 'Statistics' }, + readings: { id: 'Total Pembacaan', en: 'Total Readings' }, + meters: { id: 'Meter Aktif', en: 'Active Meters' }, + anomalies: { id: 'Anomali', en: 'Anomalies' }, + reset: { id: 'Reset Data', en: 'Reset Data' }, + resetConfirm:{ id: 'Hapus semua data? Aksi ini tidak bisa dibatalkan.', en: 'Delete all data? This cannot be undone.' }, + noAlerts: { id: 'Tidak ada peringatan', en: 'No recent alerts' }, + updated: { id: 'Diperbarui', en: 'Updated' }, + secAgo: { id: 'detik lalu', en: 'sec ago' }, + relayOn: { id: 'Hidup', en: 'ON' }, + relayOff: { id: 'Mati', en: 'OFF' }, + na: { id: 'N/A', en: 'N/A' }, +}; + +function t(key) { + const item = L[key]; + if (!item) return key; + return `${item.id}${item.en}`; +} +function tRaw(key) { + const item = L[key]; + return item ? item.id : key; +} + +/** Threshold color for voltage */ +function voltColor(mv) { + const v = mv / 1000; + if (v < 190 || v > 250) return 'var(--color-red)'; + if (v < 200 || v > 245) return 'var(--color-amber)'; + return 'var(--color-green)'; +} + +/** Threshold color for temperature */ +function tempColor(c) { + if (c == null) return 'var(--color-text-secondary)'; + if (c > 55) return 'var(--color-red)'; + if (c > 45) return 'var(--color-amber)'; + return 'var(--color-green)'; } function severityColor(sev) { @@ -28,146 +66,158 @@ function severityColor(sev) { return 'var(--color-green)'; } +function timeAgo(ts) { + const diff = Date.now() - new Date(ts).getTime(); + const secs = Math.floor(diff / 1000); + if (secs < 5) return 'just now'; + if (secs < 60) return `${secs}s ago`; + const mins = Math.floor(secs / 60); + if (mins < 60) return `${mins}m ago`; + const hrs = Math.floor(mins / 60); + if (hrs < 24) return `${hrs}h ago`; + return `${Math.floor(hrs / 24)}d ago`; +} + export default async function renderDashboard(container) { - // Fetch data in parallel -<<<<<<< HEAD - const [status, readings, alerts, notifSummary, forensics] = await Promise.all([ + const [status, readings, alerts, notifSummary] = await Promise.all([ getStatus(), - getReadings({ limit: 100 }), - getAlerts({ limit: 10 }), + getReadings({ limit: 50 }), + getAlerts({ limit: 5 }), getNotificationSummary(), - getForensicsReports({ limit: 5 }), -======= - const [status, readings, alerts] = await Promise.all([ - getStatus(), - getReadings({ limit: 100 }), - getAlerts({ limit: 10 }), ->>>>>>> origin/main ]); + const latest = readings.length > 0 ? readings[0] : null; + container.innerHTML = `
- -
-
-
- Total Readings - πŸ“Š + +
+
+
+ ${latest ? (latest.voltage_mv / 1000).toFixed(1) : '--'} + V
-
${status.total_readings.toLocaleString()}
-
All meter data collected
+
${t('voltage')}
-
-
- Active Meters - πŸ”Œ +
+
+ ${latest ? (latest.current_ma / 1000).toFixed(2) : '--'} + A
-
${status.active_meters}
-
Reporting devices
+
${t('current')}
-
-
- Unack. Alerts - 🚨 +
+
+ ${latest ? (latest.power_mw / 1000).toFixed(1) : '--'} + W
-
${status.unacknowledged_alerts}
-
Tamper alerts pending
+
${t('power')}
-
-
- Anomalies - ⚠️ +
+
+ ${latest ? latest.energy_wh : '--'} + Wh
-
${status.total_anomalies}
-
Detected events
+
${t('energy')}
-
-
- Notifications - πŸ”” +
+
+ ${latest && latest.temperature_c != null ? latest.temperature_c.toFixed(1) : '--'} + °C
-
${notifSummary.unread_count}
-
Unread alerts
+
${t('temperature')}
+
+ +
+
+ ${latest && latest.humidity_pct != null ? latest.humidity_pct.toFixed(1) : '--'} + % +
+
${t('humidity')}
+
+ +
+
+ ${latest && latest.relay_on != null + ? `${latest.relay_on ? 'ON' : 'OFF'}` + : '--'} +
+
${t('relay')}
+
+ +
+
+ ${latest ? (latest.power_factor / 100).toFixed(2) : '--'} +
+
${t('pf')}
- +
-
+
- Energy Consumption (Wh) -
- Last ${readings.length} readings - -
+ ${t('voltTrend')} + ${readings.length} data
-
+
-
+
- Recent Alerts - ${status.unacknowledged_alerts} active + ${t('alerts')} + ${status.unacknowledged_alerts}
-
-
-
- Forensics Reports - ${forensics.length} recent + -
`; - // --- Energy Chart --- + // --- Voltage Chart --- const sorted = [...readings].reverse(); - const labels = sorted.map((r, i) => { - const d = new Date(r.timestamp); - return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); - }); + const labels = sorted.map(r => new Date(r.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })); - const chartContainer = document.getElementById('energy-chart'); + const chartContainer = document.getElementById('voltage-chart'); const chart = createLineChart(chartContainer, { labels, - datasets: [ - { - label: 'Energy (Wh)', - data: sorted.map(r => r.energy_wh), - borderColor: '#00f5d4', - backgroundColor: 'rgba(0, 245, 212, 0.08)', - fill: true, - }, - { - label: 'Voltage (V)', - data: sorted.map(r => r.voltage_mv / 1000), - borderColor: '#7b61ff', - backgroundColor: 'rgba(123, 97, 255, 0.05)', - fill: false, - yAxisID: 'y1', - }, - ], + datasets: [{ + label: 'Voltage (V)', + data: sorted.map(r => r.voltage_mv / 1000), + borderColor: '#00f5d4', + backgroundColor: 'rgba(0, 245, 212, 0.06)', + fill: true, + }], options: { scales: { - y: { position: 'left', title: { display: true, text: 'Wh' } }, - y1: { - position: 'right', - title: { display: true, text: 'Voltage (V)' }, - grid: { drawOnChartArea: false }, + y: { + beginAtZero: false, + suggestedMin: 200, + suggestedMax: 250, + title: { display: true, text: 'Volt (V)' }, }, }, }, @@ -176,76 +226,97 @@ export default async function renderDashboard(container) { // --- Recent Alerts --- const alertsList = document.getElementById('recent-alerts'); if (alerts.length === 0) { - alertsList.innerHTML = ` -
-
βœ…
-
No recent alerts
-
`; + alertsList.innerHTML = `
OK
${tRaw('noAlerts')}
`; } else { alertsList.innerHTML = alerts.map(a => `
${a.tamper_type.replace(/_/g, ' ')}
-
${timeAgo(a.timestamp)} Β· Meter ${a.meter_id.toString(16).toUpperCase().slice(-8)}
+
${timeAgo(a.timestamp)}
${a.acknowledged ? 'ACK' : 'NEW'}
`).join(''); } - // --- Export button --- - document.getElementById('export-readings-btn')?.addEventListener('click', () => { - exportReadings(); + // --- Reset Button --- + document.getElementById('reset-btn')?.addEventListener('click', async () => { + if (!confirm(tRaw('resetConfirm'))) return; + try { + const result = await resetAllData(); + alert(`Data reset! Deleted: ${JSON.stringify(result.deleted)}`); + location.reload(); + } catch (e) { + alert('Reset failed: ' + e.message); + } }); -<<<<<<< HEAD - // --- Forensics Reports --- - const forensicsList = document.getElementById('forensics-list'); - if (forensics.length === 0) { - forensicsList.innerHTML = ` -
-
πŸ”
-
No forensics reports
-
`; - } else { - forensicsList.innerHTML = forensics.map(r => ` -
- -
-
${r.report_type} incident
-
${timeAgo(r.timestamp)} Β· ${r.event_count} events Β· ${r.confidence}% confidence
-
- ${r.severity} -
- `).join(''); - } + // --- Live Update (2s polling) --- + let lastUpdateTime = Date.now(); + const updateIndicator = () => { + const el = document.getElementById('last-updated'); + if (el) { + const sec = Math.floor((Date.now() - lastUpdateTime) / 1000); + el.textContent = sec < 2 ? 'live' : `${sec}s`; + } + }; + const indicatorTimer = setInterval(updateIndicator, 1000); - // --- Auto-refresh --- - const interval = setInterval(async () => { - try { - const [s, ns] = await Promise.all([getStatus(), getNotificationSummary()]); -======= - // --- Auto-refresh --- - const interval = setInterval(async () => { + const pollTimer = setInterval(async () => { try { - const s = await getStatus(); ->>>>>>> origin/main - const el = (id) => document.getElementById(id); + const [r, s] = await Promise.all([getLatestReading(), getStatus()]); + + if (r) { + lastUpdateTime = Date.now(); + const el = id => document.getElementById(id); + + // Update hero values + if (el('val-voltage')) { + const v = (r.voltage_mv / 1000).toFixed(1); + el('val-voltage').innerHTML = `${v}V`; + el('val-voltage').style.color = voltColor(r.voltage_mv); + } + if (el('val-current')) el('val-current').innerHTML = `${(r.current_ma / 1000).toFixed(2)}A`; + if (el('val-power')) el('val-power').innerHTML = `${((r.power_mw || 0) / 1000).toFixed(1)}W`; + if (el('val-energy')) el('val-energy').innerHTML = `${r.energy_wh}Wh`; + if (el('val-temp') && r.temperature_c != null) { + el('val-temp').innerHTML = `${r.temperature_c.toFixed(1)}°C`; + el('val-temp').style.color = tempColor(r.temperature_c); + } + if (el('val-humidity') && r.humidity_pct != null) { + el('val-humidity').innerHTML = `${r.humidity_pct.toFixed(1)}%`; + } + if (el('val-relay') && r.relay_on != null) { + el('val-relay').innerHTML = `${r.relay_on ? 'ON' : 'OFF'}`; + } + if (el('val-pf')) el('val-pf').textContent = (r.power_factor / 100).toFixed(2); + + // Push to chart + const time = new Date(r.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); + const ds = chart.data; + if (ds.labels[ds.labels.length - 1] !== time) { + ds.labels.push(time); + ds.datasets[0].data.push(r.voltage_mv / 1000); + if (ds.labels.length > 50) { + ds.labels.shift(); + ds.datasets[0].data.shift(); + } + chart.update('none'); + } + } + + // Update stats + const el = id => document.getElementById(id); if (el('stat-readings')) el('stat-readings').textContent = s.total_readings.toLocaleString(); - if (el('stat-meters')) el('stat-meters').textContent = s.active_meters; - if (el('stat-alerts')) el('stat-alerts').textContent = s.unacknowledged_alerts; - if (el('stat-anomalies')) el('stat-anomalies').textContent = s.total_anomalies; -<<<<<<< HEAD - if (el('stat-notifs')) el('stat-notifs').textContent = ns.unread_count; -======= ->>>>>>> origin/main + if (el('stat-meters')) el('stat-meters').textContent = s.active_meters; + if (el('stat-anomalies'))el('stat-anomalies').textContent = s.total_anomalies; } catch (_) { /* ignore if navigated away */ } - }, 10000); + }, 2000); - // Return cleanup function return () => { - clearInterval(interval); + clearInterval(pollTimer); + clearInterval(indicatorTimer); chart.destroy(); }; } diff --git a/frontend/src/style.css b/frontend/src/style.css index 1a6446a..6c2921a 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -991,7 +991,6 @@ a:hover { gap: var(--space-2); } -<<<<<<< HEAD /* --- Notification Badge --- */ .notif-badge { display: inline-flex; @@ -1103,11 +1102,205 @@ a:hover { color: var(--severity-medium); } -/* --- Charts Row Responsive for 3 panels --- */ +/* --- Charts Row Responsive for 2 panels --- */ @media (min-width: 1025px) { .charts-row { - grid-template-columns: 2fr 1fr 1fr; + grid-template-columns: 2fr 1fr; } } -======= ->>>>>>> origin/main + +/* ============================================================================ + Hero Cards β€” Live Monitor Dashboard + ============================================================================ */ + +.hero-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: var(--space-4); + margin-bottom: var(--space-6); +} + +@media (max-width: 900px) { + .hero-grid { grid-template-columns: repeat(2, 1fr); } +} +@media (max-width: 500px) { + .hero-grid { grid-template-columns: 1fr; } +} + +.hero-card { + background: var(--color-bg-card); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: var(--space-5) var(--space-6); + text-align: center; + backdrop-filter: blur(12px); + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); + position: relative; + overflow: hidden; +} + +.hero-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + border-radius: var(--radius-lg) var(--radius-lg) 0 0; +} + +.hero-voltage::before { background: var(--color-accent); } +.hero-current::before { background: var(--color-purple); } +.hero-power::before { background: var(--color-amber); } +.hero-energy::before { background: var(--color-blue); } +.hero-temp::before { background: var(--color-orange); } +.hero-humidity::before{ background: var(--color-green); } +.hero-relay::before { background: var(--color-purple); } +.hero-pf::before { background: var(--color-text-secondary); } + +.hero-card:hover { + border-color: var(--color-border-hover); + box-shadow: var(--shadow-md); +} + +.hero-value { + font-size: var(--text-3xl); + font-weight: 700; + font-family: var(--font-mono); + letter-spacing: -0.5px; + line-height: 1.2; + color: var(--color-text-primary); + transition: color var(--transition-fast); +} + +.hero-unit { + font-size: var(--text-lg); + font-weight: 400; + color: var(--color-text-secondary); + margin-left: 2px; +} + +.hero-label { + font-size: var(--text-sm); + color: var(--color-text-secondary); + margin-top: var(--space-2); + line-height: 1.4; +} + +/* Bilingual labels */ +.l-id { + display: inline; +} +.l-en { + display: none; +} +.hero-label .l-id::after { + content: ' / '; + color: var(--color-text-muted); + font-size: 0.75em; +} +.hero-label .l-en { + display: inline; + color: var(--color-text-muted); + font-size: 0.85em; +} + +/* Relay badge */ +.relay-badge { + display: inline-block; + padding: var(--space-1) var(--space-4); + border-radius: var(--radius-md); + font-size: var(--text-xl); + font-weight: 700; + font-family: var(--font-mono); + letter-spacing: 1px; +} + +.relay-on { + background: var(--color-green-dim); + color: var(--color-green); + box-shadow: 0 0 12px rgba(74, 222, 128, 0.2); +} + +.relay-off { + background: var(--color-red-dim); + color: var(--color-red); +} + +/* Live indicator */ +.live-indicator { + display: flex; + align-items: center; + gap: var(--space-2); + font-size: var(--text-sm); + color: var(--color-text-secondary); + font-family: var(--font-mono); +} + +/* Header actions */ +.header-actions { + display: flex; + align-items: center; + gap: var(--space-4); +} + +/* Reset button danger variant */ +.btn-danger { + background: var(--color-red-dim); + color: var(--color-red); + border: 1px solid rgba(248, 113, 113, 0.2); +} +.btn-danger:hover { + background: rgba(248, 113, 113, 0.25); + border-color: var(--color-red); +} + +/* Stats footer */ +.panel-footer { + padding: var(--space-4) var(--space-5); + border-top: 1px solid var(--color-border); +} + +.stats-footer { + display: flex; + justify-content: space-around; +} + +.stat-mini { + text-align: center; +} + +.stat-mini-val { + display: block; + font-size: var(--text-xl); + font-weight: 700; + font-family: var(--font-mono); + color: var(--color-text-primary); +} + +.stat-mini-label { + font-size: var(--text-xs); + color: var(--color-text-muted); + line-height: 1.4; +} + +.stat-mini-label .l-en { + display: inline; + color: var(--color-text-muted); + font-size: 0.85em; +} +.stat-mini-label .l-id::after { + content: ' / '; + color: var(--color-text-muted); + font-size: 0.75em; +} + +/* Chart & alerts panel sizing */ +.chart-panel { min-height: 320px; } +.alerts-panel { + display: flex; + flex-direction: column; +} +.alerts-panel .panel-body { + flex: 1; +}