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
This commit is contained in:
2026-03-12 06:15:23 +08:00
parent 5876e61cbf
commit f839a24c27
6 changed files with 1849 additions and 0 deletions

View File

@@ -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

18
requirements.txt Normal file
View File

@@ -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

321
src/app.py Normal file
View File

@@ -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)

View File

@@ -0,0 +1,362 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TCKRTUIYO - Calibration</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<style>
:root {
--touch-target-min: 48px;
--touch-target-primary: 64px;
--primary-color: #0d6efd;
--bg-color: #f8f9fa;
--card-bg: #ffffff;
}
body {
font-size: 18px;
background-color: var(--bg-color);
}
/* Touch-optimized navigation */
.nav-menu {
display: flex;
gap: 10px;
padding: 15px;
background: var(--card-bg);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
position: sticky;
top: 0;
z-index: 100;
}
.nav-btn {
flex: 1;
min-height: var(--touch-target-primary);
font-size: 1.2rem;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
border-radius: 12px;
transition: transform 0.1s, box-shadow 0.1s;
cursor: pointer;
}
.nav-btn:active {
transform: scale(0.98);
}
.nav-btn.active {
background-color: var(--primary-color);
color: white;
border: none;
}
.nav-btn:not(.active) {
background-color: #e9ecef;
color: #495057;
border: 1px solid #dee2e6;
}
/* Calibration cards */
.calib-card {
background: var(--card-bg);
border-radius: 16px;
padding: 20px;
margin: 15px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.calib-card h4 {
font-size: 1.3rem;
color: #212529;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #e9ecef;
display: flex;
align-items: center;
gap: 10px;
}
/* ADC Channel display */
.adc-channel {
background: #f8f9fa;
border-radius: 12px;
padding: 15px;
margin-bottom: 15px;
}
.adc-channel h5 {
font-size: 1.1rem;
margin-bottom: 15px;
font-weight: 600;
display: flex;
justify-content: space-between;
align-items: center;
}
.channel-type {
font-size: 0.85rem;
padding: 4px 10px;
background: #e9ecef;
border-radius: 6px;
color: #495057;
}
.channel-data {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.channel-item {
background: white;
padding: 10px;
border-radius: 8px;
text-align: center;
}
.channel-label {
font-size: 0.8rem;
color: #6c757d;
margin-bottom: 5px;
}
.channel-value {
font-size: 1.2rem;
font-weight: 600;
color: #212529;
}
/* Rainfall counters */
.rainfall-counter {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
margin-bottom: 20px;
}
.rainfall-box {
background: #e7f1ff;
border-radius: 12px;
padding: 20px;
text-align: center;
}
.rainfall-label {
font-size: 0.9rem;
color: #6c757d;
margin-bottom: 10px;
}
.rainfall-value {
font-size: 2rem;
font-weight: 700;
color: var(--primary-color);
}
/* Reset button */
.btn-reset {
min-height: var(--touch-target-primary);
font-size: 1.1rem;
font-weight: 600;
padding: 15px 30px;
border-radius: 12px;
}
/* Alert */
.alert {
border-radius: 12px;
padding: 15px 20px;
}
/* Live indicator */
.live-indicator {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 0.9rem;
color: #198754;
}
.live-dot {
width: 10px;
height: 10px;
background: #198754;
border-radius: 50%;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>
</head>
<body>
<!-- Navigation Menu -->
<nav class="nav-menu">
<a href="/" class="nav-btn">
<i class="bi bi-speedometer2"></i> Dashboard
</a>
<a href="/settings" class="nav-btn">
<i class="bi bi-gear"></i> Settings
</a>
<a href="/calibration" class="nav-btn active">
<i class="bi bi-sliders"></i> Calibration
</a>
</nav>
<div class="container-fluid p-0">
<!-- Alert for status -->
<div id="alertMessage" class="alert alert-success m-3" style="display: none;">
Settings saved successfully!
</div>
<!-- Live indicator -->
<div class="calib-card" style="padding: 10px 20px;">
<div class="live-indicator">
<span class="live-dot"></span>
Live ADC Readings
</div>
</div>
<!-- ADC Channels -->
<div class="calib-card">
<h4><i class="bi bi-gpu"></i> ADC Channels</h4>
<div id="adcChannels">
<!-- ADC channels will be populated by JS -->
</div>
</div>
<!-- Rainfall Counters -->
<div class="calib-card">
<h4><i class="bi bi-cloud-rain"></i> Rainfall Counters</h4>
<div class="rainfall-counter">
<div class="rainfall-box">
<div class="rainfall-label" id="rf1Label">RF1 Tips</div>
<div class="rainfall-value" id="rf1Tips">0</div>
</div>
<div class="rainfall-box">
<div class="rainfall-label" id="rf2Label">RF2 Tips</div>
<div class="rainfall-value" id="rf2Tips">0</div>
</div>
</div>
<button class="btn btn-danger btn-reset w-100" onclick="resetRainfall()">
<i class="bi bi-arrow-counterclockwise"></i> Reset Rainfall Counters
</button>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/socket.io-client@4.7.4/dist/socket.io.min.js"></script>
<script>
const socket = io();
// Load calibration data
async function loadCalibration() {
try {
const response = await fetch('/api/calibration');
const data = await response.json();
// Update rainfall counters
document.getElementById('rf1Label').textContent = data.rainfall.rf1_id + ' Tips';
document.getElementById('rf1Tips').textContent = data.rainfall.rf1_tips;
document.getElementById('rf2Label').textContent = data.rainfall.rf2_id + ' Tips';
document.getElementById('rf2Tips').textContent = data.rainfall.rf2_tips;
// Render ADC channels
renderAdcChannels(data.adc);
} catch (error) {
console.error('Error loading calibration:', error);
}
}
// Render ADC channels
function renderAdcChannels(channels) {
const container = document.getElementById('adcChannels');
container.innerHTML = '';
channels.forEach(channel => {
const div = document.createElement('div');
div.className = 'adc-channel';
div.innerHTML = `
<h5>
<span>Channel ${channel.id}</span>
<span class="channel-type">${channel.type}</span>
</h5>
<div class="channel-data">
<div class="channel-item">
<div class="channel-label">ADC Value</div>
<div class="channel-value">${channel.value.toFixed(2)}</div>
</div>
<div class="channel-item">
<div class="channel-label">Datum</div>
<div class="channel-value">${channel.datum.toFixed(2)}</div>
</div>
<div class="channel-item">
<div class="channel-label">Reading</div>
<div class="channel-value">${channel.reading.toFixed(1)} ${channel.unit}</div>
</div>
<div class="channel-item">
<div class="channel-label">Status</div>
<div class="channel-value">${channel.value > 0 ? 'Active' : 'Idle'}</div>
</div>
</div>
`;
container.appendChild(div);
});
}
// Real-time ADC updates via Socket.IO
socket.on('sensor_update', function(data) {
// Refresh calibration data periodically
loadCalibration();
});
// Reset rainfall counters
async function resetRainfall() {
if (!confirm('Are you sure you want to reset all rainfall counters?')) {
return;
}
try {
const response = await fetch('/api/calibration/reset_rainfall', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
const result = await response.json();
if (result.success) {
showAlert('Rainfall counters reset successfully!');
loadCalibration();
}
} catch (error) {
console.error('Error resetting rainfall:', error);
showAlert('Error resetting rainfall counters', 'danger');
}
}
function showAlert(message, type = 'success') {
const alert = document.getElementById('alertMessage');
alert.className = 'alert alert-' + type;
alert.textContent = message;
alert.style.display = 'block';
setTimeout(() => {
alert.style.display = 'none';
}, 3000);
}
// Initial load
loadCalibration();
// Refresh every 2 seconds
setInterval(loadCalibration, 2000);
</script>
</body>
</html>

View File

@@ -0,0 +1,442 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TCKRTUIYO - Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<style>
/* Touch-optimized for 7" 1024x600 display */
:root {
--touch-target-min: 48px;
--touch-target-primary: 64px;
--primary-color: #0d6efd;
--success-color: #198754;
--warning-color: #ffc107;
--danger-color: #dc3545;
--bg-color: #f8f9fa;
--card-bg: #ffffff;
}
body {
font-size: 18px;
background-color: var(--bg-color);
min-height: 100vh;
}
/* Touch-optimized navigation */
.nav-menu {
display: flex;
gap: 10px;
padding: 15px;
background: var(--card-bg);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
position: sticky;
top: 0;
z-index: 100;
}
.nav-btn {
flex: 1;
min-height: var(--touch-target-primary);
font-size: 1.2rem;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
border-radius: 12px;
transition: transform 0.1s, box-shadow 0.1s;
cursor: pointer;
}
.nav-btn:active {
transform: scale(0.98);
box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
}
.nav-btn.active {
background-color: var(--primary-color);
color: white;
border: none;
}
.nav-btn:not(.active) {
background-color: #e9ecef;
color: #495057;
border: 1px solid #dee2e6;
}
/* Card styles */
.sensor-card {
background: var(--card-bg);
border-radius: 16px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
margin-bottom: 15px;
}
.sensor-card h5 {
font-size: 1rem;
color: #6c757d;
margin-bottom: 10px;
font-weight: 500;
}
.sensor-value {
font-size: 2.5rem;
font-weight: 700;
color: #212529;
line-height: 1;
}
.sensor-unit {
font-size: 1rem;
color: #6c757d;
margin-left: 5px;
}
/* Status indicators */
.status-badge {
display: inline-block;
padding: 8px 16px;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
min-width: 80px;
text-align: center;
}
.status-high {
background-color: #d1e7dd;
color: #0f5132;
}
.status-normal {
background-color: #cff4fc;
color: #055160;
}
.status-low {
background-color: #f8d7da;
color: #842029;
}
/* Date/Time display */
.datetime-display {
text-align: center;
padding: 20px;
background: linear-gradient(135deg, #0d6efd 0%, #0a58ca 100%);
color: white;
border-radius: 16px;
margin-bottom: 15px;
}
.time-display {
font-size: 3rem;
font-weight: 700;
font-family: 'Courier New', monospace;
}
.date-display {
font-size: 1.5rem;
opacity: 0.9;
}
/* Station info */
.station-info {
text-align: center;
padding: 15px;
background: var(--card-bg);
border-radius: 12px;
margin-bottom: 15px;
}
.station-id {
font-size: 1.3rem;
font-weight: 600;
color: #212529;
}
/* Communication status */
.comm-status {
display: flex;
align-items: center;
gap: 15px;
padding: 15px;
}
.signal-bars {
display: flex;
align-items: flex-end;
gap: 3px;
height: 30px;
}
.signal-bar {
width: 8px;
background-color: #dee2e6;
border-radius: 2px;
}
.signal-bar.active {
background-color: var(--success-color);
}
.signal-bar:nth-child(1) { height: 25%; }
.signal-bar:nth-child(2) { height: 50%; }
.signal-bar:nth-child(3) { height: 75%; }
.signal-bar:nth-child(4) { height: 100%; }
/* Grid layout */
.dashboard-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
padding: 15px;
}
.rainfall-grid {
grid-column: 1 / -1;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
}
.rainfall-item {
text-align: center;
padding: 15px;
background: #e7f1ff;
border-radius: 12px;
}
.rainfall-label {
font-size: 0.9rem;
color: #6c757d;
margin-bottom: 5px;
}
.rainfall-value {
font-size: 1.8rem;
font-weight: 700;
color: var(--primary-color);
}
/* Last updated */
.last-updated {
text-align: center;
padding: 10px;
color: #6c757d;
font-size: 0.9rem;
}
/* Version info */
.version-info {
text-align: center;
padding: 10px;
color: #adb5bd;
font-size: 0.8rem;
}
@media (max-width: 600px) {
.dashboard-grid {
grid-template-columns: 1fr;
}
.rainfall-grid {
grid-template-columns: repeat(2, 1fr);
}
.sensor-value {
font-size: 2rem;
}
.time-display {
font-size: 2.5rem;
}
}
</style>
</head>
<body>
<!-- Navigation Menu -->
<nav class="nav-menu">
<a href="/" class="nav-btn active">
<i class="bi bi-speedometer2"></i> Dashboard
</a>
<a href="/settings" class="nav-btn">
<i class="bi bi-gear"></i> Settings
</a>
<a href="/calibration" class="nav-btn">
<i class="bi bi-sliders"></i> Calibration
</a>
</nav>
<div class="container-fluid p-0">
<!-- Date/Time Display -->
<div class="datetime-display">
<div class="time-display" id="timeDisplay">--:--:--</div>
<div class="date-display" id="dateDisplay">--</div>
</div>
<!-- Station Info -->
<div class="station-info">
<span class="station-id" id="stationId">Loading...</span>
</div>
<!-- Dashboard Content -->
<div class="dashboard-grid">
<!-- Rainfall Section -->
<div class="sensor-card rainfall-grid">
<div class="rainfall-item">
<div class="rainfall-label">Today</div>
<div class="rainfall-value" id="rainfallToday">0.0</div>
<small>mm</small>
</div>
<div class="rainfall-item">
<div class="rainfall-label">Hourly</div>
<div class="rainfall-value" id="rainfallHourly">0.0</div>
<small>mm</small>
</div>
<div class="rainfall-item">
<div class="rainfall-label">MAR</div>
<div class="rainfall-value" id="rainfallMar">0.0</div>
<small>mm</small>
</div>
<div class="rainfall-item">
<div class="rainfall-label">Yearly</div>
<div class="rainfall-value" id="rainfallYearly">0.0</div>
<small>mm</small>
</div>
</div>
<!-- Solar Voltage -->
<div class="sensor-card">
<h5><i class="bi bi-sun-fill"></i> Solar Voltage</h5>
<div class="sensor-value">
<span id="solarVoltage">--</span>
<span class="sensor-unit">V</span>
</div>
<span class="status-badge status-normal" id="solarStatus">--</span>
</div>
<!-- Battery Voltage -->
<div class="sensor-card">
<h5><i class="bi bi-battery-full"></i> Battery Voltage</h5>
<div class="sensor-value">
<span id="batteryVoltage">--</span>
<span class="sensor-unit">V</span>
</div>
<span class="status-badge" id="batteryStatus">--</span>
</div>
<!-- Communication Status -->
<div class="sensor-card">
<h5><i class="bi bi-wifi"></i> Communication</h5>
<div class="comm-status">
<div class="signal-bars">
<div class="signal-bar active"></div>
<div class="signal-bar active"></div>
<div class="signal-bar active"></div>
<div class="signal-bar" id="signal4"></div>
</div>
<div>
<div id="connectionStatus">Connected</div>
<small class="text-muted" id="signalStrength">-65 dBm</small>
</div>
</div>
</div>
<!-- Software Version -->
<div class="sensor-card">
<h5><i class="bi bi-info-circle"></i> Software</h5>
<div class="sensor-value" style="font-size: 1.5rem;">
v<span id="softwareVersion">{{ version }}</span>
</div>
<small class="text-muted">{{ build_date }}</small>
</div>
</div>
<!-- Last Updated -->
<div class="last-updated">
<i class="bi bi-clock-history"></i> Last Updated: <span id="lastUpdated">--</span>
</div>
<!-- Version Footer -->
<div class="version-info">
TCKRTUIYO - Rainfall Station RTU
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/socket.io-client@4.7.4/dist/socket.io.min.js"></script>
<script>
// Socket.IO connection for real-time updates
const socket = io();
// Status API data
async function loadStatus() {
try {
const response = await fetch('/api/status');
const data = await response.json();
document.getElementById('timeDisplay').textContent = data.datetime;
document.getElementById('dateDisplay').textContent = data.date;
document.getElementById('stationId').textContent = data.station_id;
document.getElementById('connectionStatus').textContent = data.connection_status;
document.getElementById('signalStrength').textContent = data.signal_strength + ' dBm';
// Update signal bars based on RSSI
const rssi = data.signal_strength;
const bars = ['signal4'];
if (rssi > -50) {
// Excellent
} else if (rssi > -70) {
bars.pop();
} else if (rssi > -85) {
bars.length = 0;
} else {
bars.length = 0;
}
bars.forEach(id => {
const el = document.getElementById(id);
if (el) el.classList.add('active');
});
} catch (error) {
console.error('Error loading status:', error);
}
}
// Sensor data via Socket.IO
socket.on('sensor_update', function(data) {
// Update rainfall
document.getElementById('rainfallToday').textContent = data.rainfall.today.toFixed(1);
document.getElementById('rainfallHourly').textContent = data.rainfall.hourly.toFixed(1);
document.getElementById('rainfallMar').textContent = data.rainfall.mar.toFixed(1);
document.getElementById('rainfallYearly').textContent = data.rainfall.yearly.toFixed(1);
// Update voltage
document.getElementById('solarVoltage').textContent = data.voltage.solar.toFixed(1);
document.getElementById('batteryVoltage').textContent = data.voltage.battery.toFixed(1);
// Update voltage status
const solarStatus = document.getElementById('solarStatus');
solarStatus.textContent = data.voltage.solar_status;
solarStatus.className = 'status-badge status-' + data.voltage.solar_status.toLowerCase();
const batteryStatus = document.getElementById('batteryStatus');
batteryStatus.textContent = data.voltage.battery_status;
batteryStatus.className = 'status-badge status-' + data.voltage.battery_status.toLowerCase();
// Update timestamp
document.getElementById('lastUpdated').textContent = data.last_updated;
});
// Initial load
loadStatus();
// Update status every 30 seconds
setInterval(loadStatus, 30000);
</script>
</body>
</html>

560
src/templates/settings.html Normal file
View File

@@ -0,0 +1,560 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TCKRTUIYO - Settings</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<style>
:root {
--touch-target-min: 48px;
--touch-target-primary: 64px;
--primary-color: #0d6efd;
--bg-color: #f8f9fa;
--card-bg: #ffffff;
}
body {
font-size: 18px;
background-color: var(--bg-color);
}
/* Touch-optimized navigation */
.nav-menu {
display: flex;
gap: 10px;
padding: 15px;
background: var(--card-bg);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
position: sticky;
top: 0;
z-index: 100;
}
.nav-btn {
flex: 1;
min-height: var(--touch-target-primary);
font-size: 1.2rem;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
border-radius: 12px;
transition: transform 0.1s, box-shadow 0.1s;
cursor: pointer;
}
.nav-btn:active {
transform: scale(0.98);
}
.nav-btn.active {
background-color: var(--primary-color);
color: white;
border: none;
}
.nav-btn:not(.active) {
background-color: #e9ecef;
color: #495057;
border: 1px solid #dee2e6;
}
/* Settings sections */
.settings-section {
background: var(--card-bg);
border-radius: 16px;
padding: 20px;
margin: 15px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.settings-section h4 {
font-size: 1.3rem;
color: #212529;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #e9ecef;
display: flex;
align-items: center;
gap: 10px;
}
/* Form controls */
.form-label {
font-weight: 500;
margin-bottom: 8px;
}
.form-control, .form-select {
padding: 12px 15px;
font-size: 1rem;
border-radius: 10px;
min-height: var(--touch-target-min);
}
/* Touch-friendly buttons */
.btn-save {
min-height: var(--touch-target-primary);
font-size: 1.1rem;
font-weight: 600;
padding: 15px 30px;
border-radius: 12px;
}
/* ADC Channel cards */
.adc-channel {
background: #f8f9fa;
border-radius: 12px;
padding: 15px;
margin-bottom: 10px;
}
.adc-channel h6 {
margin-bottom: 10px;
font-weight: 600;
}
/* Tabs for sub-sections */
.settings-tabs {
display: flex;
gap: 5px;
padding: 10px 15px;
background: #e9ecef;
border-radius: 12px;
margin-bottom: 15px;
overflow-x: auto;
}
.settings-tab {
padding: 12px 20px;
border-radius: 10px;
font-weight: 500;
cursor: pointer;
white-space: nowrap;
transition: background 0.2s;
}
.settings-tab.active {
background: var(--primary-color);
color: white;
}
.settings-tab:not(.active) {
background: transparent;
color: #495057;
}
/* Tab content */
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* Alert messages */
.alert {
border-radius: 12px;
padding: 15px 20px;
}
</style>
</head>
<body>
<!-- Navigation Menu -->
<nav class="nav-menu">
<a href="/" class="nav-btn">
<i class="bi bi-speedometer2"></i> Dashboard
</a>
<a href="/settings" class="nav-btn active">
<i class="bi bi-gear"></i> Settings
</a>
<a href="/calibration" class="nav-btn">
<i class="bi bi-sliders"></i> Calibration
</a>
</nav>
<div class="container-fluid p-0">
<!-- Alert for save status -->
<div id="alertMessage" class="alert alert-success m-3" style="display: none;">
Settings saved successfully!
</div>
<!-- Settings Tabs -->
<div class="settings-tabs">
<div class="settings-tab active" onclick="showTab('station')">
<i class="bi bi-building"></i> Station
</div>
<div class="settings-tab" onclick="showTab('datetime')">
<i class="bi bi-clock"></i> Date/Time
</div>
<div class="settings-tab" onclick="showTab('network')">
<i class="bi bi-wifi"></i> Network
</div>
<div class="settings-tab" onclick="showTab('adc')">
<i class="bi bi-gpu"></i> ADC
</div>
<div class="settings-tab" onclick="showTab('sensors')">
<i class="bi bi-thermometer"></i> Sensors
</div>
</div>
<!-- Station Settings -->
<div class="tab-content active" id="tab-station">
<div class="settings-section">
<h4><i class="bi bi-building"></i> Station Information</h4>
<div class="mb-3">
<label class="form-label">Station ID</label>
<input type="text" class="form-control" id="stationId" placeholder="TCKRTUIYO-001">
</div>
<div class="mb-3">
<label class="form-label">Station Name</label>
<input type="text" class="form-control" id="stationName" placeholder="Rainfall Station">
</div>
<div class="mb-3">
<label class="form-label">Location</label>
<input type="text" class="form-control" id="stationLocation" placeholder="Unknown">
</div>
</div>
</div>
<!-- Date/Time Settings -->
<div class="tab-content" id="tab-datetime">
<div class="settings-section">
<h4><i class="bi bi-clock"></i> Date & Time Settings</h4>
<div class="mb-3">
<label class="form-label">Timezone</label>
<select class="form-select" id="timezone">
<option value="UTC">UTC</option>
<option value="GMT">GMT</option>
<option value="EST">EST (UTC-5)</option>
<option value="CST">CST (UTC-6)</option>
<option value="MST">MST (UTC-7)</option>
<option value="PST">PST (UTC-8)</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">
<input type="checkbox" id="ntpEnabled" checked> Enable NTP Sync
</label>
</div>
<div class="mb-3">
<label class="form-label">NTP Server</label>
<input type="text" class="form-control" id="ntpServer" placeholder="pool.ntp.org">
</div>
</div>
</div>
<!-- Network Settings -->
<div class="tab-content" id="tab-network">
<div class="settings-section">
<h4><i class="bi bi-wifi"></i> Network Settings</h4>
<div class="mb-3">
<label class="form-label">IP Address</label>
<input type="text" class="form-control" id="ipAddress" placeholder="192.168.1.100">
</div>
<div class="mb-3">
<label class="form-label">Subnet Mask</label>
<input type="text" class="form-control" id="subnetMask" placeholder="255.255.255.0">
</div>
<div class="mb-3">
<label class="form-label">Gateway</label>
<input type="text" class="form-control" id="gateway" placeholder="192.168.1.1">
</div>
<div class="mb-3">
<label class="form-label">DNS Server</label>
<input type="text" class="form-control" id="dnsServer" placeholder="8.8.8.8">
</div>
<div class="mb-3">
<label class="form-label">MAC Address</label>
<input type="text" class="form-control" id="macAddress" placeholder="00:00:00:00:00:00" readonly>
</div>
</div>
<div class="settings-section">
<h4><i class="bi bi-phone"></i> Mobile Network (4G)</h4>
<div class="mb-3">
<label class="form-label">
<input type="checkbox" id="mobileEnabled"> Enable Mobile Network
</label>
</div>
<div class="mb-3">
<label class="form-label">APN</label>
<input type="text" class="form-control" id="apn" placeholder="internet">
</div>
<div class="mb-3">
<label class="form-label">PIN</label>
<input type="text" class="form-control" id="mobilePin" placeholder="">
</div>
</div>
</div>
<!-- ADC Settings -->
<div class="tab-content" id="tab-adc">
<div class="settings-section">
<h4><i class="bi bi-gpu"></i> ADC Channel Configuration</h4>
<div id="adcChannels">
<!-- ADC channels will be populated by JS -->
</div>
</div>
</div>
<!-- Sensor Settings -->
<div class="tab-content" id="tab-sensors">
<div class="settings-section">
<h4><i class="bi bi-cloud-rain"></i> Rainfall Sensors</h4>
<div class="mb-3">
<label class="form-label">RF1 Sensor ID</label>
<input type="text" class="form-control" id="rf1Id" placeholder="RF1">
</div>
<div class="mb-3">
<label class="form-label">RF2 Sensor ID</label>
<input type="text" class="form-control" id="rf2Id" placeholder="RF2">
</div>
<div class="mb-3">
<label class="form-label">Bucket Size (mm per tip)</label>
<input type="number" class="form-control" id="bucketSize" step="0.01" value="0.2">
</div>
</div>
<div class="settings-section">
<h4><i class="bi bi-water"></i> Water Level Thresholds</h4>
<div class="mb-3">
<label class="form-label">Warning Threshold (%)</label>
<input type="number" class="form-control" id="waterWarning" value="80">
</div>
<div class="mb-3">
<label class="form-label">Danger Threshold (%)</label>
<input type="number" class="form-control" id="waterDanger" value="95">
</div>
</div>
<div class="settings-section">
<h4><i class="bi bi-battery-charging"></i> Voltage Thresholds</h4>
<div class="mb-3">
<label class="form-label">Battery HIGH (V)</label>
<input type="number" class="form-control" id="batteryHigh" step="0.1" value="14.0">
</div>
<div class="mb-3">
<label class="form-label">Battery NORMAL (V)</label>
<input type="number" class="form-control" id="batteryNormal" step="0.1" value="12.0">
</div>
<div class="mb-3">
<label class="form-label">Battery LOW (V)</label>
<input type="number" class="form-control" id="batteryLow" step="0.1" value="11.0">
</div>
</div>
<div class="settings-section">
<h4><i class="bi bi-thermometer-half"></i> EVAP Settings</h4>
<div class="mb-3">
<label class="form-label">
<input type="checkbox" id="evapEnabled"> Enable EVAP
</label>
</div>
<div class="mb-3">
<label class="form-label">Coefficient</label>
<input type="number" class="form-control" id="evapCoefficient" step="0.01" value="0.0">
</div>
</div>
</div>
<!-- Save Button -->
<div class="settings-section">
<button class="btn btn-primary btn-save w-100" onclick="saveSettings()">
<i class="bi bi-check-circle"></i> Save Settings
</button>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
let currentSettings = {};
// Tab navigation
function showTab(tabName) {
// Update tab buttons
document.querySelectorAll('.settings-tab').forEach(tab => {
tab.classList.remove('active');
});
event.target.closest('.settings-tab').classList.add('active');
// Update tab content
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById('tab-' + tabName).classList.add('active');
}
// Load settings
async function loadSettings() {
try {
const response = await fetch('/api/settings');
currentSettings = await response.json();
// Station
document.getElementById('stationId').value = currentSettings.station.id;
document.getElementById('stationName').value = currentSettings.station.name;
document.getElementById('stationLocation').value = currentSettings.station.location;
// Date/Time
document.getElementById('timezone').value = currentSettings.datetime.timezone;
document.getElementById('ntpEnabled').checked = currentSettings.datetime.ntp_enabled;
document.getElementById('ntpServer').value = currentSettings.datetime.ntp_server;
// Network
document.getElementById('ipAddress').value = currentSettings.network.ip;
document.getElementById('subnetMask').value = currentSettings.network.subnet;
document.getElementById('gateway').value = currentSettings.network.gateway;
document.getElementById('dnsServer').value = currentSettings.network.dns;
document.getElementById('macAddress').value = currentSettings.network.mac;
// Mobile
document.getElementById('mobileEnabled').checked = currentSettings.mobile.enabled;
document.getElementById('apn').value = currentSettings.mobile.apn;
document.getElementById('mobilePin').value = currentSettings.mobile.pin;
// ADC Channels
renderAdcChannels();
// Sensors
document.getElementById('rf1Id').value = currentSettings.sensors.rainfall.rf1_id;
document.getElementById('rf2Id').value = currentSettings.sensors.rainfall.rf2_id;
document.getElementById('bucketSize').value = currentSettings.sensors.rainfall.bucket_size;
document.getElementById('waterWarning').value = currentSettings.sensors.water_level.threshold_warning;
document.getElementById('waterDanger').value = currentSettings.sensors.water_level.threshold_danger;
document.getElementById('batteryHigh').value = currentSettings.sensors.voltage.battery_high;
document.getElementById('batteryNormal').value = currentSettings.sensors.voltage.battery_normal;
document.getElementById('batteryLow').value = currentSettings.sensors.voltage.battery_low;
// EVAP
document.getElementById('evapEnabled').checked = currentSettings.evap.enabled;
document.getElementById('evapCoefficient').value = currentSettings.evap.coefficient;
} catch (error) {
console.error('Error loading settings:', error);
}
}
// Render ADC channel configuration
function renderAdcChannels() {
const container = document.getElementById('adcChannels');
container.innerHTML = '';
currentSettings.adc.channels.forEach((channel, index) => {
const div = document.createElement('div');
div.className = 'adc-channel';
div.innerHTML = `
<h6>Channel ${channel.id}: ${channel.name}</h6>
<div class="mb-2">
<label class="form-label">Type</label>
<select class="form-select" id="adcType${channel.id}">
<option value="4-20mA" ${channel.type === '4-20mA' ? 'selected' : ''}>4-20mA</option>
<option value="0-10vDC" ${channel.type === '0-10vDC' ? 'selected' : ''}>0-10vDC</option>
</select>
</div>
<div class="mb-2">
<label class="form-label">Name</label>
<input type="text" class="form-control" id="adcName${channel.id}" value="${channel.name}">
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="adcEnabled${channel.id}" ${channel.enabled ? 'checked' : ''}>
<label class="form-check-label" for="adcEnabled${channel.id}">Enabled</label>
</div>
`;
container.appendChild(div);
});
}
// Save settings
async function saveSettings() {
const settings = {
station: {
id: document.getElementById('stationId').value,
name: document.getElementById('stationName').value,
location: document.getElementById('stationLocation').value
},
datetime: {
timezone: document.getElementById('timezone').value,
ntp_enabled: document.getElementById('ntpEnabled').checked,
ntp_server: document.getElementById('ntpServer').value
},
network: {
ip: document.getElementById('ipAddress').value,
subnet: document.getElementById('subnetMask').value,
gateway: document.getElementById('gateway').value,
dns: document.getElementById('dnsServer').value,
mac: document.getElementById('macAddress').value
},
mobile: {
enabled: document.getElementById('mobileEnabled').checked,
apn: document.getElementById('apn').value,
pin: document.getElementById('mobilePin').value
},
adc: {
channels: currentSettings.adc.channels.map((ch, i) => ({
id: ch.id,
type: document.getElementById('adcType' + ch.id).value,
name: document.getElementById('adcName' + ch.id).value,
enabled: document.getElementById('adcEnabled' + ch.id).checked
}))
},
sensors: {
rainfall: {
rf1_id: document.getElementById('rf1Id').value,
rf2_id: document.getElementById('rf2Id').value,
bucket_size: parseFloat(document.getElementById('bucketSize').value)
},
water_level: {
threshold_warning: parseInt(document.getElementById('waterWarning').value),
threshold_danger: parseInt(document.getElementById('waterDanger').value)
},
voltage: {
battery_high: parseFloat(document.getElementById('batteryHigh').value),
battery_normal: parseFloat(document.getElementById('batteryNormal').value),
battery_low: parseFloat(document.getElementById('batteryLow').value),
solar_high: 15.0,
solar_normal: 12.0
}
},
evap: {
enabled: document.getElementById('evapEnabled').checked,
coefficient: parseFloat(document.getElementById('evapCoefficient').value)
}
};
try {
const response = await fetch('/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings)
});
const result = await response.json();
if (result.success) {
showAlert('Settings saved successfully!');
}
} catch (error) {
console.error('Error saving settings:', error);
showAlert('Error saving settings', 'danger');
}
}
function showAlert(message, type = 'success') {
const alert = document.getElementById('alertMessage');
alert.className = 'alert alert-' + type;
alert.textContent = message;
alert.style.display = 'block';
setTimeout(() => {
alert.style.display = 'none';
}, 3000);
}
// Load settings on page load
loadSettings();
</script>
</body>
</html>