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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions BUILD.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand Down
5 changes: 2 additions & 3 deletions backend/app/anomaly_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

35 changes: 2 additions & 33 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
)

Expand All @@ -61,32 +45,17 @@ 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"])
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",
}
10 changes: 6 additions & 4 deletions backend/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Comment on lines +23 to +26

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add migration for new meter_readings columns

Adding new meter_readings fields here breaks upgrades that keep an existing gridshield.db: SQLAlchemy create_all does not ALTER existing tables, so older databases will still lack these columns and ORM reads/writes can fail at runtime with no such column errors. This impacts core paths like /api/meter-data and /api/readings after deployment unless a schema migration (or explicit compatibility handling) is added.

Useful? React with 👍 / 👎.

humidity_pct = Column(Float, nullable=True)
relay_on = Column(Boolean, nullable=True)


class TamperAlert(Base):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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

35 changes: 35 additions & 0 deletions backend/app/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
12 changes: 9 additions & 3 deletions backend/app/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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}

Expand Down Expand Up @@ -131,7 +139,6 @@ class MeterStats(BaseModel):
avg_voltage_mv: float
avg_current_ma: float
last_reading_time: datetime | None = None
<<<<<<< HEAD


# ============================================================================
Expand Down Expand Up @@ -209,5 +216,4 @@ class ForensicsReportResponse(BaseModel):
raw_payload: str

model_config = {"from_attributes": True}
=======
>>>>>>> origin/main

Loading
Loading