From f839a24c272af0fa192121ccf9e818c850659df9 Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 12 Mar 2026 06:15:23 +0800 Subject: [PATCH] feat(phase-1): implement Foundation & Kiosk UI - Add Flask backend with SocketIO for real-time updates - Implement sensor service with mock data generation - Create Dashboard UI with rainfall, voltage, status displays - Create Settings UI with station, network, ADC, sensor configuration - Create Calibration UI with live ADC readings and rainfall reset - Touch-optimized interface with 64px touch targets for 7" display Implements: DASH-01 through DASH-09, UI-01 through UI-03, SETT-01 through SETT-08, CAL-01 through CAL-04, BACK-01 through BACK-03 --- .../phases/01-foundation-kiosk-ui/01-PLAN.md | 146 +++++ requirements.txt | 18 + src/app.py | 321 ++++++++++ src/templates/calibration.html | 362 +++++++++++ src/templates/dashboard.html | 442 ++++++++++++++ src/templates/settings.html | 560 ++++++++++++++++++ 6 files changed, 1849 insertions(+) create mode 100644 .planning/phases/01-foundation-kiosk-ui/01-PLAN.md create mode 100644 requirements.txt create mode 100644 src/app.py create mode 100644 src/templates/calibration.html create mode 100644 src/templates/dashboard.html create mode 100644 src/templates/settings.html diff --git a/.planning/phases/01-foundation-kiosk-ui/01-PLAN.md b/.planning/phases/01-foundation-kiosk-ui/01-PLAN.md new file mode 100644 index 000000000..c2d8cea2b --- /dev/null +++ b/.planning/phases/01-foundation-kiosk-ui/01-PLAN.md @@ -0,0 +1,146 @@ +--- +phase: 1 +plan: 1 +name: Foundation & Kiosk UI +type: execution +autonomous: true +wave: 1 +depends_on: [] + +requirements: + - DASH-01 + - DASH-02 + - DASH-03 + - DASH-04 + - DASH-05 + - DASH-06 + - DASH-07 + - DASH-08 + - DASH-09 + - UI-01 + - UI-02 + - UI-03 + - SETT-01 + - SETT-02 + - SETT-03 + - SETT-04 + - SETT-05 + - SETT-06 + - SETT-07 + - SETT-08 + - CAL-01 + - CAL-02 + - CAL-03 + - CAL-04 + - BACK-01 + - BACK-02 + - BACK-03 +--- + +# Phase 1 Plan: Foundation & Kiosk UI + +## Objective + +Build the foundational Flask backend with real-time sensor data API and the 7" touchscreen kiosk UI (port 8080) displaying rainfall readings, voltage status, date/time, station ID, communication status, and software version. Include Settings and Calibration menus for configuration access. + +## Context + +This is the first vertical slice — each layer works end-to-end before adding persistence or network transmission. The research selected Flask + Flask-SocketIO + Bootstrap for Pi Zero 2 W performance. + +## Success Criteria + +1. User can view current rainfall readings (Today, Hourly, MAR Accumulated, Yearly Accumulated) on the dashboard +2. User can view solar voltage and battery voltage with status indicator (HIGH/NORMAL/LOW) on the dashboard +3. User can view current date/time (HH:MM:SS DD-MM-YYYY) and station ID on the dashboard +4. User can view communication status (signal strength, connection status) and software version on the dashboard +5. User can navigate between Dashboard, Settings, and Calibration menus via main menu +6. User can access and modify station info, date/time, network settings, sensor thresholds, and ADC configuration +7. User can view live ADC channel readings and reset rainfall counters in calibration view +8. Dashboard updates automatically with real-time sensor data +9. Touch interface responds with large touch targets (minimum 48px) optimized for 7" 1024x600 display + +--- + +## Tasks + +### Task 1: Initialize Project Structure & Dependencies +**Type:** auto +**Description:** Create project directory structure and requirements.txt +**Verification:** `ls -la src/` shows proper structure, `pip install -r requirements.txt` succeeds + +--- + +### Task 2: Create Flask Backend with SocketIO +**Type:** auto +**Description:** Set up Flask app with SocketIO, routes folder structure, and base configuration +**Verification:** `python src/app.py` starts server on port 8080, `/api/status` returns JSON + +--- + +### Task 3: Implement Sensor Service with Mock Data +**Type:** auto +**Description:** Create sensor service that generates realistic mock rainfall/voltage readings +**Verification:** `/api/sensors` returns valid sensor data structure with all fields + +--- + +### Task 4: Create Status API Endpoint +**Type:** auto +**Description:** Implement `/api/status` returning station ID, date/time, software version +**Verification:** API returns all required status fields + +--- + +### Task 5: Create Settings API Endpoints +**Type:** auto +**Description:** Implement `/api/settings` GET/POST for station, network, ADC, thresholds +**Verification:** GET returns settings, POST updates settings, data persists to JSON + +--- + +### Task 6: Create Calibration API Endpoints +**Type:** auto +**Description:** Implement `/api/calibration` for live ADC readings and rainfall reset +**Verification:** Returns live ADC values, reset endpoint clears rainfall counters + +--- + +### Task 7: Build Dashboard HTML/CSS/JS +**Type:** auto +**Description:** Create main dashboard template with rainfall, voltage, status displays +**Verification:** Dashboard renders with all required elements, responsive to 1024x600 + +--- + +### Task 8: Implement Real-Time Updates via SocketIO +**Type:** auto +**Description:** Frontend connects to SocketIO, receives live sensor updates every second +**Verification:** Dashboard auto-updates without page refresh + +--- + +### Task 9: Build Settings UI +**Type:** auto +**Description:** Create settings pages for station info, date/time, network, ADC config, thresholds +**Verification:** All settings accessible and modifiable through UI forms + +--- + +### Task 10: Build Calibration UI +**Type:** auto +**Description:** Create calibration view showing live ADC readings with channel info +**Verification:** Shows 4 channels with type, ADC value, datum, calculated reading + +--- + +### Task 11: Add Touch-Optimized Navigation +**Type:** auto +**Description:** Implement main menu with large touch targets (64px), back buttons, 7" optimized CSS +**Verification:** Navigation works with touch, all targets >= 48px + +--- + +### Task 12: Final Integration & Testing +**Type:** auto +**Description:** Verify all 27 requirements met, test server startup, verify all pages load +**Verification:** All requirements traceable, no console errors, server stable diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..3fbdc4b48 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,18 @@ +# TCKRTUIYO - Rainfall Station RTU Web Interface +# Phase 1: Foundation & Kiosk UI + +# Web Framework +Flask==3.1.0 +Flask-SocketIO==5.6.0 + +# Async support for Flask-SocketIO +eventlet==0.40.0 + +# System monitoring +psutil==6.1.0 + +# Serial communication (for sensor hardware) +pyserial==3.5 + +# Settings storage +python-dotenv==1.0.0 diff --git a/src/app.py b/src/app.py new file mode 100644 index 000000000..f9fb937e6 --- /dev/null +++ b/src/app.py @@ -0,0 +1,321 @@ +""" +TCKRTUIYO - Rainfall Station RTU Web Interface +Flask Backend with SocketIO for Real-Time Updates +""" +import os +import json +import time +import threading +from datetime import datetime +from flask import Flask, render_template, jsonify, request +from flask_socketio import SocketIO, emit + +# Configuration +app = Flask(__name__) +app.config['SECRET_KEY'] = 'tckrtuiyo-secret-key-change-in-production' +app.config['JSON_SORT_KEYS'] = False + +# SocketIO with eventlet for production performance +socketio = SocketIO(app, cors_allowed_origins="*", async_mode='eventlet') + +# Data paths +DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') +SETTINGS_FILE = os.path.join(DATA_DIR, 'settings.json') + +# Ensure data directory exists +os.makedirs(DATA_DIR, exist_ok=True) + +# Default settings +DEFAULT_SETTINGS = { + "station": { + "id": "TCKRTUIYO-001", + "name": "Rainfall Station", + "location": "Unknown" + }, + "datetime": { + "timezone": "UTC", + "ntp_enabled": True, + "ntp_server": "pool.ntp.org" + }, + "network": { + "ip": "192.168.1.100", + "subnet": "255.255.255.0", + "gateway": "192.168.1.1", + "dns": "8.8.8.8", + "mac": "00:00:00:00:00:00" + }, + "mobile": { + "enabled": False, + "apn": "", + "pin": "" + }, + "adc": { + "channels": [ + {"id": 1, "type": "4-20mA", "name": "Rainfall RF1", "enabled": True}, + {"id": 2, "type": "4-20mA", "name": "Rainfall RF2", "enabled": True}, + {"id": 3, "type": "0-10vDC", "name": "Water Level", "enabled": True}, + {"id": 4, "type": "0-10vDC", "name": "Solar Voltage", "enabled": True} + ] + }, + "sensors": { + "rainfall": { + "rf1_id": "RF1", + "rf2_id": "RF2", + "bucket_size": 0.2 # mm per tip + }, + "water_level": { + "threshold_warning": 80, + "threshold_danger": 95 + }, + "voltage": { + "battery_high": 14.0, + "battery_normal": 12.0, + "battery_low": 11.0, + "solar_high": 15.0, + "solar_normal": 12.0 + } + }, + "evap": { + "enabled": False, + "coefficient": 0.0 + } +} + +# In-memory sensor state (would be populated by hardware in production) +sensor_state = { + "rainfall": { + "today": 0.0, + "hourly": 0.0, + "mar": 0.0, + "yearly": 0.0, + "rf1_tips": 0, + "rf2_tips": 0 + }, + "voltage": { + "solar": 12.4, + "battery": 12.8, + "solar_status": "NORMAL", + "battery_status": "HIGH" + }, + "adc": [ + {"id": 1, "type": "4-20mA", "value": 4.2, "datum": 4.0, "reading": 0.5, "unit": "mm"}, + {"id": 2, "type": "4-20mA", "value": 4.1, "datum": 4.0, "reading": 0.3, "unit": "mm"}, + {"id": 3, "type": "0-10vDC", "value": 2.5, "datum": 0.0, "reading": 25.0, "unit": "cm"}, + {"id": 4, "type": "0-10vDC", "value": 6.2, "datum": 0.0, "reading": 12.4, "unit": "V"} + ], + "communication": { + "signal_strength": -65, + "connection_status": "Connected", + "rssi": "Good" + }, + "last_updated": None +} + +# Software version +SOFTWARE_VERSION = "1.0.0" +BUILD_DATE = "2026-03-12" + + +def load_settings(): + """Load settings from JSON file or return defaults""" + if os.path.exists(SETTINGS_FILE): + try: + with open(SETTINGS_FILE, 'r') as f: + return json.load(f) + except: + pass + return DEFAULT_SETTINGS.copy() + + +def save_settings(settings): + """Save settings to JSON file""" + with open(SETTINGS_FILE, 'w') as f: + json.dump(settings, f, indent=2) + + +def update_sensor_readings(): + """Simulate sensor readings (replace with actual hardware reads in production)""" + global sensor_state + + # Simulate small rainfall occasionally + import random + if random.random() < 0.05: # 5% chance of rain + tips = random.randint(1, 3) + sensor_state["rainfall"]["today"] += tips * 0.2 + sensor_state["rainfall"]["hourly"] += tips * 0.2 + sensor_state["rainfall"]["mar"] += tips * 0.2 + sensor_state["rainfall"]["yearly"] += tips * 0.2 + sensor_state["rainfall"]["rf1_tips"] += tips + + # Simulate voltage fluctuation + sensor_state["voltage"]["solar"] = round(12.0 + random.uniform(-0.5, 1.5), 1) + sensor_state["voltage"]["battery"] = round(12.6 + random.uniform(-0.3, 0.3), 1) + + # Update status based on voltage + settings = load_settings() + vb = sensor_state["voltage"]["battery"] + vs = sensor_state["voltage"]["solar"] + + if vb >= settings["sensors"]["voltage"]["battery_high"]: + sensor_state["voltage"]["battery_status"] = "HIGH" + elif vb >= settings["sensors"]["voltage"]["battery_normal"]: + sensor_state["voltage"]["battery_status"] = "NORMAL" + else: + sensor_state["voltage"]["battery_status"] = "LOW" + + if vs >= settings["sensors"]["voltage"]["solar_normal"]: + sensor_state["voltage"]["solar_status"] = "HIGH" + else: + sensor_state["voltage"]["solar_status"] = "NORMAL" + + # Update ADC readings + for ch in sensor_state["adc"]: + ch["value"] = round(ch["value"] + random.uniform(-0.1, 0.1), 2) + if ch["type"] == "4-20mA": + ch["reading"] = round((ch["value"] - ch["datum"]) / 16.0 * 100, 1) + else: + ch["reading"] = round(ch["value"] * 2.0, 1) + + # Update timestamp + sensor_state["last_updated"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + +def sensor_background_thread(): + """Background thread to update sensor readings and emit via SocketIO""" + while True: + update_sensor_readings() + # Emit to all connected clients + socketio.emit('sensor_update', { + 'rainfall': sensor_state["rainfall"], + 'voltage': sensor_state["voltage"], + 'last_updated': sensor_state["last_updated"] + }) + socketio.sleep(1) + + +# ==================== ROUTES ==================== + +@app.route('/') +def index(): + """Main kiosk dashboard""" + return render_template('dashboard.html', + page='dashboard', + version=SOFTWARE_VERSION, + build_date=BUILD_DATE) + + +@app.route('/settings') +def settings(): + """Settings page""" + return render_template('settings.html', + page='settings', + version=SOFTWARE_VERSION, + build_date=BUILD_DATE) + + +@app.route('/calibration') +def calibration(): + """Calibration page""" + return render_template('calibration.html', + page='calibration', + version=SOFTWARE_VERSION, + build_date=BUILD_DATE) + + +# ==================== API ENDPOINTS ==================== + +@app.route('/api/status') +def api_status(): + """Get system status""" + now = datetime.now() + settings = load_settings() + + return jsonify({ + "station_id": settings["station"]["id"], + "station_name": settings["station"]["name"], + "datetime": now.strftime("%H:%M:%S"), + "date": now.strftime("%d-%m-%Y"), + "software_version": SOFTWARE_VERSION, + "build_date": BUILD_DATE, + "connection_status": sensor_state["communication"]["connection_status"], + "signal_strength": sensor_state["communication"]["signal_strength"], + "rssi": sensor_state["communication"]["rssi"] + }) + + +@app.route('/api/sensors') +def api_sensors(): + """Get current sensor readings""" + return jsonify({ + "rainfall": sensor_state["rainfall"], + "voltage": sensor_state["voltage"], + "last_updated": sensor_state["last_updated"] + }) + + +@app.route('/api/settings') +def api_settings_get(): + """Get all settings""" + return jsonify(load_settings()) + + +@app.route('/api/settings', methods=['POST']) +def api_settings_post(): + """Update settings""" + settings = load_settings() + data = request.get_json() + + # Update specific sections + if "station" in data: + settings["station"].update(data["station"]) + if "datetime" in data: + settings["datetime"].update(data["datetime"]) + if "network" in data: + settings["network"].update(data["network"]) + if "mobile" in data: + settings["mobile"].update(data["mobile"]) + if "adc" in data: + settings["adc"].update(data["adc"]) + if "sensors" in data: + settings["sensors"].update(data["sensors"]) + if "evap" in data: + settings["evap"].update(data["evap"]) + + save_settings(settings) + return jsonify({"success": True, "settings": settings}) + + +@app.route('/api/calibration') +def api_calibration(): + """Get calibration data - live ADC readings""" + return jsonify({ + "adc": sensor_state["adc"], + "rainfall": { + "rf1_tips": sensor_state["rainfall"]["rf1_tips"], + "rf2_tips": sensor_state["rainfall"]["rf2_tips"], + "rf1_id": load_settings()["sensors"]["rainfall"]["rf1_id"], + "rf2_id": load_settings()["sensors"]["rainfall"]["rf2_id"] + } + }) + + +@app.route('/api/calibration/reset_rainfall', methods=['POST']) +def api_calibration_reset(): + """Reset rainfall counters""" + sensor_state["rainfall"]["today"] = 0.0 + sensor_state["rainfall"]["hourly"] = 0.0 + sensor_state["rainfall"]["rf1_tips"] = 0 + sensor_state["rainfall"]["rf2_tips"] = 0 + return jsonify({"success": True}) + + +# ==================== MAIN ==================== + +if __name__ == '__main__': + # Start background sensor thread + socketio.start_background_task(sensor_background_thread) + + # Run with eventlet for production performance on Pi Zero 2 W + print(f"Starting TCKRTUIYO server on port 8080...") + print(f"Software Version: {SOFTWARE_VERSION}") + socketio.run(app, host='0.0.0.0', port=8080, debug=False) diff --git a/src/templates/calibration.html b/src/templates/calibration.html new file mode 100644 index 000000000..788e4912e --- /dev/null +++ b/src/templates/calibration.html @@ -0,0 +1,362 @@ + + + + + + TCKRTUIYO - Calibration + + + + + + + + +
+ + + + +
+
+ + Live ADC Readings +
+
+ + +
+

ADC Channels

+
+ +
+
+ + +
+

Rainfall Counters

+
+
+
RF1 Tips
+
0
+
+
+
RF2 Tips
+
0
+
+
+ +
+
+ + + + + + diff --git a/src/templates/dashboard.html b/src/templates/dashboard.html new file mode 100644 index 000000000..be52f9a1d --- /dev/null +++ b/src/templates/dashboard.html @@ -0,0 +1,442 @@ + + + + + + TCKRTUIYO - Dashboard + + + + + + + + +
+ +
+
--:--:--
+
--
+
+ + +
+ Loading... +
+ + +
+ +
+
+
Today
+
0.0
+ mm +
+
+
Hourly
+
0.0
+ mm +
+
+
MAR
+
0.0
+ mm +
+
+
Yearly
+
0.0
+ mm +
+
+ + +
+
Solar Voltage
+
+ -- + V +
+ -- +
+ + +
+
Battery Voltage
+
+ -- + V +
+ -- +
+ + +
+
Communication
+
+
+
+
+
+
+
+
+
Connected
+ -65 dBm +
+
+
+ + +
+
Software
+
+ v{{ version }} +
+ {{ build_date }} +
+
+ + +
+ Last Updated: -- +
+ + +
+ TCKRTUIYO - Rainfall Station RTU +
+
+ + + + + + diff --git a/src/templates/settings.html b/src/templates/settings.html new file mode 100644 index 000000000..a1cdc188f --- /dev/null +++ b/src/templates/settings.html @@ -0,0 +1,560 @@ + + + + + + TCKRTUIYO - Settings + + + + + + + + +
+ + + + +
+
+ Station +
+
+ Date/Time +
+
+ Network +
+
+ ADC +
+
+ Sensors +
+
+ + +
+
+

Station Information

+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+

Date & Time Settings

+
+ + +
+
+ +
+
+ + +
+
+
+ + +
+
+

Network Settings

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+

Mobile Network (4G)

+
+ +
+
+ + +
+
+ + +
+
+
+ + +
+
+

ADC Channel Configuration

+
+ +
+
+
+ + +
+
+

Rainfall Sensors

+
+ + +
+
+ + +
+
+ + +
+
+ +
+

Water Level Thresholds

+
+ + +
+
+ + +
+
+ +
+

Voltage Thresholds

+
+ + +
+
+ + +
+
+ + +
+
+ +
+

EVAP Settings

+
+ +
+
+ + +
+
+
+ + +
+ +
+
+ + + + +