20 KiB
Architecture Patterns: Raspberry Pi-Based Web Monitoring Systems
Project: TCKRTUIYO - Rainfall Station RTU Web Interface
Researched: 2026-03-12
Confidence: HIGH
Executive Summary
Raspberry Pi-based web monitoring systems typically follow a three-tier architecture:
- Frontend — Web UI served via HTTP, displaying data with real-time updates
- Backend API — Flask/FastAPI handling sensor data collection and serving JSON endpoints
- Data Layer — Sensor interfaces, CSV logging, and network transmission
For this rainfall monitoring RTU, the architecture must accommodate:
- Dual interfaces: Kiosk mode (1024x600, port 8080) vs Remote access (Full HD, port 9090)
- Low-powered hardware: Pi Zero 2 W with ~512MB RAM, single-core equivalent
- CSV-based data workflow: Local storage → server transmission via FTP/SCP/SFTP/WebDAV
Recommended Architecture
┌─────────────────────────────────────────────────────────────────────────────┐
│ RAINFALL MONITORING RTU │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ WEB LAYER (Dual Port) │ │
│ │ ┌─────────────────────────┐ ┌─────────────────────────────┐ │ │
│ │ │ KIOSK INTERFACE │ │ REMOTE INTERFACE │ │ │
│ │ │ http://pi:8080 │ │ http://pi:9090 │ │ │
│ │ │ 1024x600 fixed │ │ Full HD responsive │ │ │
│ │ │ Chromium Kiosk Mode │ │ Auth required │ │ │
│ │ └───────────┬─────────────┘ └──────────────┬──────────────┘ │ │
│ │ │ │ │ │
│ │ └────────────┬───────────────────┘ │ │
│ │ │ │ │
│ │ ┌──────▼──────┐ │ │
│ │ │ STATIC │ │ │
│ │ │ FILES │ │ │
│ │ │ (HTML/CSS/ │ │ │
│ │ │ JS/VUE) │ │ │
│ │ └──────┬──────┘ │ │
│ └───────────────────────────│──────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────────▼──────────────────────────────────────────┐ │
│ │ API LAYER (Flask/FastAPI) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ /api/ │ │ /api/ │ │ /api/ │ │ /api/ │ │ │
│ │ │ status │ │ sensors │ │ settings │ │ files │ │ │
│ │ │ (GET) │ │ (GET) │ │ (GET/POST) │ │ (GET/POST) │ │ │
│ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │
│ │ │ │ │ │ │ │
│ │ └────────────────┴────────┬────────┴─────────────────┘ │ │
│ │ │ │ │
│ │ ┌────────▼────────┐ │ │
│ │ │ BLUEPRINT │ │ │
│ │ │ ROUTING │ │ │
│ │ └────────┬────────┘ │ │
│ └───────────────────────────────│───────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────────────▼───────────────────────────────────────┐ │
│ │ SERVICE LAYER │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ SENSOR │ │ DATA │ │ NETWORK │ │ CONFIG │ │ │
│ │ │ SERVICE │ │ LOGGER │ │ SERVICE │ │ SERVICE │ │ │
│ │ │ (read │ │ (CSV │ │ (FTP/ │ │ (read/ │ │ │
│ │ │ GPIO/ │ │ write) │ │ SFTP) │ │ write) │ │ │
│ │ │ ADC) │ │ │ │ │ │ │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │ │ │ │ │
│ └─────────┼────────────────┼─────────────────┼─────────────────┼──────────┘ │
│ │ │ │ │ │
│ ──────────┼────────────────┼─────────────────┼─────────────────┼──────────────│
│ │ │ │ │ │
│ ┌─────────▼────────────────▼─────────────────▼─────────────────▼──────────┐ │
│ │ DATA LAYER │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ HARDWARE │ │ CSV │ │ NETWORK │ │ CONFIG │ │ │
│ │ │ INTERFACE │ │ FILES │ │ INTERFACE │ │ FILES │ │ │
│ │ │ (GPIO/ │ │ (local │ │ (modem/ │ │ (JSON/ │ │ │
│ │ │ ADC/ │ │ storage) │ │ ethernet) │ │ YAML) │ │ │
│ │ │ Serial) │ │ │ │ │ │ │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Component Boundaries
1. Web Layer (HTTP Servers)
| Component | Responsibility | Boundary |
|---|---|---|
| Kiosk Server (port 8080) | Serves fixed 1024x600 UI for touchscreen | Reads from API layer only |
| Remote Server (port 9090) | Serves responsive Full HD UI with authentication | Reads/Writes via API layer |
Communication:
- Both serve static files (HTML/CSS/JavaScript)
- Both make AJAX/Fetch requests to API layer
- Single API backend shared by both ports
2. API Layer (Flask/FastAPI)
| Endpoint | Method | Purpose | Writes To |
|---|---|---|---|
/api/status |
GET | Station ID, time, comm status, version | None |
/api/sensors |
GET | Rainfall (today/hourly/MAR/yearly), solar, battery | None |
/api/settings |
GET/POST | All configuration (ADC, network, calibration) | Config files |
/api/files |
GET/POST/DELETE | CSV file management | CSV storage |
/api/calibration |
GET | Live sensor readings | None |
Communication:
- JSON responses to Web Layer
- Delegates to Service Layer for operations
3. Service Layer
| Service | Responsibility | Communicates With |
|---|---|---|
| SensorService | Polls GPIO/ADC/Serial for rainfall, water level, voltage | Hardware Interface |
| DataLogger | Writes sensor readings to CSV with timestamps | CSV Files |
| NetworkService | Transmits CSV via FTP/SFTP/SCP/WebDAV | Remote Server |
| ConfigService | Reads/writes JSON configuration files | Config Files |
4. Data Layer
| Component | Location | Purpose |
|---|---|---|
| Hardware Interface | GPIO pins, ADC (MCP3008/3208), Serial | Physical sensor connections |
| CSV Storage | /data/*.csv |
Time-series rainfall data |
| Network Interface | Ethernet/USB Modem | Server communication |
| Config Storage | /config/*.json |
Station settings |
Data Flow
Primary Flow: Sensor Reading → Display
1. TIMER (every N seconds)
│
▼
2. SENSOR SERVICE reads from hardware
- Rainfall tips (GPIO interrupt)
- Water level (ADC via SPI)
- Solar/Battery voltage (ADC)
│
▼
3. UPDATE IN-MEMORY STATE
- today_rainfall, hourly_rainfall, etc.
- battery_voltage, solar_voltage
│
▼
4. DATA LOGGER writes to CSV
- Append timestamp + readings
│
▼
5. API LAYER receives GET /api/sensors
- Returns JSON with current state
│
▼
6. WEB LAYER displays data
- Polling every 1-5 seconds (kiosk)
- Socket.IO push (if used)
Secondary Flow: Settings Change
1. USER taps setting on touchscreen
2. WEB LAYER sends POST /api/settings
3. API LAYER validates payload
4. CONFIG SERVICE writes to JSON file
5. SENSOR SERVICE reloads config
6. ACK response to UI
Tertiary Flow: CSV Transmission
1. SCHEDULED TASK (cron or timer)
│
▼
2. NETWORK SERVICE checks queue
│
▼
3. READ pending CSV files
│
▼
4. TRANSMIT via selected protocol
- FTP / SFTP / SCP / WebDAV
│
▼
5. LOG transmission result
- Success: mark as sent
- Failure: retry queue
Build Order & Dependencies
Based on research of similar systems and component dependencies:
Phase 1: Foundation (Weeks 1-2)
Goal: Get basic sensor reading and display working
| Order | Component | Depends On | Rationale |
|---|---|---|---|
| 1 | Hardware Interface | None | Must read sensors before anything else |
| 2 | Sensor Service | Hardware Interface | Abstracts reading logic |
| 3 | Basic API (status, sensors) | Sensor Service | Data must exist to serve |
| 4 | Minimal Web UI (dashboard) | API | Shows readings on screen |
Why: This follows the "vertical slice" approach — each layer works end-to-end before adding features.
Phase 2: Local Interface (Weeks 3-4)
Goal: Complete kiosk-mode interface
| Order | Component | Depends On |
|---|---|---|
| 5 | Settings UI | Phase 1 API |
| 6 | Calibration UI | Phase 1 API |
| 7 | File Management UI | Phase 1 API |
| 8 | Port 8080 Kiosk Server | All above |
Phase 3: Data Persistence (Weeks 5-6)
Goal: CSV logging and transmission
| Order | Component | Depends On |
|---|---|---|
| 9 | Data Logger Service | Sensor Service |
| 10 | CSV Storage Layer | Data Logger |
| 11 | Network Transmission Service | CSV Storage |
Phase 4: Remote Access (Weeks 7-8)
Goal: Full HD remote interface
| Order | Component | Depends On |
|---|---|---|
| 12 | Authentication Layer | Phase 2 UI |
| 13 | Responsive Templates | Phase 2 UI |
| 14 | Port 9090 Remote Server | All above |
Architecture Patterns to Follow
Pattern 1: Service-Oriented Architecture (SOA)
Each service (Sensor, Network, Config) is a Python class/module with clear interface:
class SensorService:
def read_all(self) -> SensorReading:
"""Read all sensors and return unified reading"""
def read_rainfall(self) -> float:
"""Read rainfall counter"""
def read_voltage(self, channel: int) -> float:
"""Read ADC channel voltage"""
Why: Isolates hardware access, easy to mock for testing, clear boundaries.
Pattern 2: Blueprint-Based Routing
Flask blueprints separate concerns:
# routes/status.py
status_bp = Blueprint('status', __name__)
@status_bp.route('/api/status')
def get_status(): ...
# routes/sensors.py
sensors_bp = Blueprint('sensors', __name__)
@sensors_bp.route('/api/sensors')
def get_sensors(): ...
# routes/settings.py
settings_bp = Blueprint('settings', __name__)
settings_bp.route('/api/settings', methods=['GET', 'POST'])
Why: Clean separation, easier maintenance, modular.
Pattern 3: Configuration-Driven Design
Store all settings in JSON files, not hardcoded:
{
"station": {
"id": "D007",
"name": "Station A"
},
"network": {
"protocol": "FTP",
"server": "myvscada.example.com",
"port": 21
},
"sensors": {
"rainfall": {
"id": "123456RF",
"danger_mm": 30.0,
"danger_window_min": 60
}
}
}
Why: Allows runtime changes without code modification, matches existing RTU behavior.
Pattern 4: Polling with Fallback
For real-time updates on Pi Zero 2 W:
// Primary: Polling (simple, reliable)
setInterval(fetchSensorData, 2000); // Every 2 seconds
// Fallback: Only add Socket.IO if latency becomes issue
// Don't optimize until measured
Why: Socket.IO adds overhead. On Pi Zero 2 W with limited resources, polling every 1-2 seconds is acceptable and simpler to debug.
Anti-Patterns to Avoid
Anti-Pattern 1: Monolithic API Handler
Bad:
@app.route('/api/<any>')
def handle_all():
# 500 lines of if/else for different endpoints
Why: Unmaintainable, hard to test, violates single responsibility.
Anti-Pattern 2: Direct Hardware Calls in Routes
Bad:
@app.route('/api/sensors')
def get_sensors():
# Direct GPIO/ADC calls inline
rainfall = read_rainfall_gpio() # Bad!
Why: Hard to test, mixes HTTP handling with hardware access.
Anti-Pattern 3: Storing State in Global Variables
Bad:
current_rainfall = 0 # Global state!
@app.route('/api/sensors')
def get_sensors():
return {'rainfall': current_rainfall}
Why: Race conditions, hard to reason about state, difficult to persist across restarts.
Scalability Considerations
| Concern | At Deployment (1 station) | At 10 stations | At 100 stations |
|---|---|---|---|
| Data Storage | CSV files, ~10MB/year | Same per station | Consider SQLite per station |
| Network | Single FTP push | Same | Batch uploads, queue system |
| UI Performance | Polling 2s acceptable | N/A | N/A |
| CPU Load | ~10-15% idle | Same | Same |
Key insight: This is an edge device, not a server. Architecture focuses on reliability and low resource usage rather than horizontal scaling.
Dual Interface Implementation
Shared API, Separate Static Roots
app.py
├── /static_kiosk/ (1024x600 optimized)
│ ├── css/
│ ├── js/
│ └── index.html
├── /static_remote/ (Full HD responsive)
│ ├── css/
│ ├── js/
│ └── index.html
Route Configuration
# Kiosk mode - no auth, fixed resolution
@app.route('/', defaults={'path': ''}, subdomain='kiosk')
@app.route('/<path>', defaults={'path': ''}, subdomain='kiosk')
def serve_kiosk(path):
return send_from_directory('static_kiosk', 'index.html')
# Remote mode - auth required, responsive
@app.route('/')
@app.route('/<path>')
@login_required
def serve_remote(path):
return send_from_directory('static_remote', 'index.html')
Alternative (simpler): Single static folder with CSS media queries adapting to screen size, two separate ports.
Confidence Assessment
| Area | Level | Reason |
|---|---|---|
| Architecture Pattern | HIGH | Matches proven IoT/SCADA patterns from research |
| Component Boundaries | HIGH | Clear separation matches Flask best practices |
| Data Flow | HIGH | Follows standard sensor→API→UI pattern |
| Build Order | MEDIUM | Suggested based on typical patterns; verify during implementation |
| Pi Zero 2 W Constraints | HIGH | Performance considerations documented |
Sources
- Szymon Pulut, "The Architecture Behind the IoT Dashboard" (2024) — IoT dashboard architecture with Raspberry Pi
- GitHub g1forfun/pi-monitor — Flask + Socket.IO real-time monitoring example
- Random Nerd Tutorials — Raspberry Pi sensor data logging patterns
- ResearchGate — RTU modular architecture for industrial monitoring
- Sensors and Materials (2025) — Django + Raspberry Pi web monitoring
- Raspberry Pi Forums — Pi Zero 2 W performance discussions