Add additional configuration and utility files

- Add install script and message configuration
- Add settings controller and templates
- Add ESP-NOW message utility
- Update API documentation
This commit is contained in:
2026-01-27 13:05:09 +13:00
parent e74ef6d64f
commit 7e33f7db6a
9 changed files with 1064 additions and 450 deletions

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

@@ -0,0 +1,430 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LED Controller - Settings</title>
<link rel="stylesheet" href="/static/style.css">
<style>
.settings-container {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
overflow-y: auto;
height: 100%;
}
.settings-header {
margin-bottom: 2rem;
}
.settings-header h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.settings-header p {
color: #aaa;
}
.settings-section {
background-color: #1a1a1a;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid #4a4a4a;
}
.settings-section h2 {
font-size: 1.3rem;
margin-bottom: 1rem;
color: #fff;
border-bottom: 2px solid #4a4a4a;
padding-bottom: 0.5rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
color: #ccc;
font-weight: 500;
}
.form-group input[type="text"],
.form-group input[type="password"],
.form-group input[type="number"],
.form-group select {
width: 100%;
padding: 0.75rem;
background-color: #2e2e2e;
border: 1px solid #4a4a4a;
border-radius: 4px;
color: white;
font-size: 1rem;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #5a5a5a;
}
.form-group small {
display: block;
margin-top: 0.25rem;
color: #888;
font-size: 0.875rem;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.status-info {
background-color: #2e2e2e;
border: 1px solid #4a4a4a;
border-radius: 4px;
padding: 1rem;
margin-bottom: 1rem;
}
.status-info h3 {
font-size: 1rem;
margin-bottom: 0.5rem;
color: #fff;
}
.status-info p {
color: #aaa;
margin: 0.25rem 0;
font-size: 0.9rem;
}
.status-connected {
color: #4caf50;
}
.status-disconnected {
color: #f44336;
}
.btn-group {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
}
.btn-full {
flex: 1;
}
.back-link {
display: inline-block;
margin-bottom: 1rem;
color: #aaa;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 4px;
transition: background-color 0.2s;
}
.back-link:hover {
background-color: #2e2e2e;
color: white;
}
.message {
padding: 0.75rem;
border-radius: 4px;
margin-bottom: 1rem;
display: none;
}
.message.success {
background-color: #1b5e20;
color: #4caf50;
border: 1px solid #4caf50;
}
.message.error {
background-color: #5e1b1b;
color: #f44336;
border: 1px solid #f44336;
}
.message.show {
display: block;
}
</style>
</head>
<body>
<div class="app-container">
<div class="settings-container">
<a href="/" class="back-link">← Back to Dashboard</a>
<div class="settings-header">
<h1>Device Settings</h1>
<p>Configure WiFi and device settings</p>
</div>
<div id="message" class="message"></div>
<!-- WiFi Station Settings -->
<div class="settings-section">
<h2>WiFi Station (Client) Settings</h2>
<div id="station-status" class="status-info">
<h3>Connection Status</h3>
<p>Loading...</p>
</div>
<form id="station-form">
<div class="form-group">
<label for="station-ssid">SSID (Network Name)</label>
<input type="text" id="station-ssid" name="ssid" placeholder="Enter WiFi network name" required>
<small>The name of the WiFi network to connect to</small>
</div>
<div class="form-group">
<label for="station-password">Password</label>
<input type="password" id="station-password" name="password" placeholder="Enter WiFi password">
<small>Leave empty for open networks</small>
</div>
<div class="form-row">
<div class="form-group">
<label for="station-ip">IP Address (Optional)</label>
<input type="text" id="station-ip" name="ip" placeholder="192.168.1.100">
<small>Static IP address (leave empty for DHCP)</small>
</div>
<div class="form-group">
<label for="station-gateway">Gateway (Optional)</label>
<input type="text" id="station-gateway" name="gateway" placeholder="192.168.1.1">
<small>Gateway/router IP address</small>
</div>
</div>
<div class="btn-group">
<button type="submit" class="btn btn-primary btn-full">Connect</button>
</div>
</form>
</div>
<!-- WiFi Access Point Settings -->
<div class="settings-section">
<h2>WiFi Access Point Settings</h2>
<div id="ap-status" class="status-info">
<h3>AP Status</h3>
<p>Loading...</p>
</div>
<form id="ap-form">
<div class="form-group">
<label for="ap-ssid">AP SSID (Network Name)</label>
<input type="text" id="ap-ssid" name="ssid" placeholder="Enter AP name" required>
<small>The name of the WiFi access point this device creates</small>
</div>
<div class="form-group">
<label for="ap-password">AP Password</label>
<input type="password" id="ap-password" name="password" placeholder="Enter AP password (min 8 chars)">
<small>Leave empty for open network (min 8 characters if set)</small>
</div>
<div class="form-group">
<label for="ap-channel">Channel (1-11)</label>
<input type="number" id="ap-channel" name="channel" min="1" max="11" placeholder="Auto">
<small>WiFi channel (1-11 for 2.4GHz). Leave empty for auto.</small>
</div>
<div class="btn-group">
<button type="submit" class="btn btn-primary btn-full">Configure AP</button>
</div>
</form>
</div>
</div>
</div>
<script>
// Show message helper
function showMessage(text, type = 'success') {
const messageEl = document.getElementById('message');
messageEl.textContent = text;
messageEl.className = `message ${type} show`;
setTimeout(() => {
messageEl.classList.remove('show');
}, 5000);
}
// Load station status
async function loadStationStatus() {
try {
const response = await fetch('/settings/wifi/station');
const status = await response.json();
const statusEl = document.getElementById('station-status');
if (status.connected) {
statusEl.innerHTML = `
<h3>Connection Status: <span class="status-connected">Connected</span></h3>
<p><strong>SSID:</strong> ${status.ssid || 'N/A'}</p>
<p><strong>IP Address:</strong> ${status.ip || 'N/A'}</p>
<p><strong>Gateway:</strong> ${status.gateway || 'N/A'}</p>
<p><strong>Netmask:</strong> ${status.netmask || 'N/A'}</p>
<p><strong>DNS:</strong> ${status.dns || 'N/A'}</p>
`;
} else {
statusEl.innerHTML = `
<h3>Connection Status: <span class="status-disconnected">Disconnected</span></h3>
<p>Not connected to any WiFi network</p>
`;
}
} catch (error) {
console.error('Error loading station status:', error);
}
}
// Load saved station credentials
async function loadStationCredentials() {
try {
const response = await fetch('/settings/wifi/station/credentials');
const creds = await response.json();
if (creds.ssid) document.getElementById('station-ssid').value = creds.ssid;
if (creds.ip) document.getElementById('station-ip').value = creds.ip;
if (creds.gateway) document.getElementById('station-gateway').value = creds.gateway;
} catch (error) {
console.error('Error loading station credentials:', error);
}
}
// Load AP status and config
async function loadAPStatus() {
try {
const response = await fetch('/settings/wifi/ap');
const config = await response.json();
const statusEl = document.getElementById('ap-status');
if (config.active) {
statusEl.innerHTML = `
<h3>AP Status: <span class="status-connected">Active</span></h3>
<p><strong>SSID:</strong> ${config.ssid || 'N/A'}</p>
<p><strong>Channel:</strong> ${config.channel || 'Auto'}</p>
<p><strong>IP Address:</strong> ${config.ip || 'N/A'}</p>
`;
} else {
statusEl.innerHTML = `
<h3>AP Status: <span class="status-disconnected">Inactive</span></h3>
<p>Access Point is not currently active</p>
`;
}
// Load saved values
if (config.saved_ssid) document.getElementById('ap-ssid').value = config.saved_ssid;
if (config.saved_channel) document.getElementById('ap-channel').value = config.saved_channel;
} catch (error) {
console.error('Error loading AP status:', error);
}
}
// Station form submission
document.getElementById('station-form').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = {
ssid: document.getElementById('station-ssid').value,
password: document.getElementById('station-password').value,
ip: document.getElementById('station-ip').value || null,
gateway: document.getElementById('station-gateway').value || null
};
try {
const response = await fetch('/settings/wifi/station', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
const result = await response.json();
if (response.ok) {
showMessage('WiFi station connected successfully!', 'success');
setTimeout(loadStationStatus, 1000);
} else {
showMessage(`Error: ${result.error || 'Failed to connect'}`, 'error');
}
} catch (error) {
showMessage(`Error: ${error.message}`, 'error');
}
});
// AP form submission
document.getElementById('ap-form').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = {
ssid: document.getElementById('ap-ssid').value,
password: document.getElementById('ap-password').value,
channel: document.getElementById('ap-channel').value || null
};
// Validate password length if provided
if (formData.password && formData.password.length > 0 && formData.password.length < 8) {
showMessage('AP password must be at least 8 characters', 'error');
return;
}
// Convert channel to number if provided
if (formData.channel) {
formData.channel = parseInt(formData.channel);
if (formData.channel < 1 || formData.channel > 11) {
showMessage('Channel must be between 1 and 11', 'error');
return;
}
}
try {
const response = await fetch('/settings/wifi/ap', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
const result = await response.json();
if (response.ok) {
showMessage('Access Point configured successfully!', 'success');
setTimeout(loadAPStatus, 1000);
} else {
showMessage(`Error: ${result.error || 'Failed to configure AP'}`, 'error');
}
} catch (error) {
showMessage(`Error: ${error.message}`, 'error');
}
});
// Load all data on page load
loadStationStatus();
loadStationCredentials();
loadAPStatus();
// Refresh status every 10 seconds
setInterval(() => {
loadStationStatus();
loadAPStatus();
}, 10000);
</script>
</body>
</html>