AI update
This commit is contained in:
@@ -27,6 +27,7 @@ add_custom_command(
|
|||||||
DEPENDS ${CMAKE_SOURCE_DIR}/fs/index.html
|
DEPENDS ${CMAKE_SOURCE_DIR}/fs/index.html
|
||||||
DEPENDS ${CMAKE_SOURCE_DIR}/fs/mqtt_config.html
|
DEPENDS ${CMAKE_SOURCE_DIR}/fs/mqtt_config.html
|
||||||
DEPENDS ${CMAKE_SOURCE_DIR}/fs/wlan_config.html
|
DEPENDS ${CMAKE_SOURCE_DIR}/fs/wlan_config.html
|
||||||
|
DEPENDS ${CMAKE_SOURCE_DIR}/fs/live_stats.html
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -60,6 +61,8 @@ target_link_libraries(sensor-pico
|
|||||||
pico_lwip_http
|
pico_lwip_http
|
||||||
pico_lwip_mqtt
|
pico_lwip_mqtt
|
||||||
pico_lwip_sntp
|
pico_lwip_sntp
|
||||||
|
hardware_flash
|
||||||
|
hardware_sync
|
||||||
)
|
)
|
||||||
|
|
||||||
# Erzeugt .uf2 Datei zum Flashen
|
# Erzeugt .uf2 Datei zum Flashen
|
||||||
|
|||||||
292
fs/index.html
292
fs/index.html
@@ -1,16 +1,290 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Config</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Sensor Pico</title>
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
||||||
|
background: #0d1117;
|
||||||
|
color: #c9d1d9;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 24px 20px;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Top bar ─────────────────────────────── */
|
||||||
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 36px;
|
||||||
|
}
|
||||||
|
.topbar-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f0f6fc;
|
||||||
|
letter-spacing: -0.2px;
|
||||||
|
}
|
||||||
|
.topbar-sub {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #484f58;
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
.settings-btn {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
background: #161b22;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 17px;
|
||||||
|
transition: border-color 0.15s, background 0.15s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.settings-btn:hover { border-color: #8b949e; background: #1c2230; }
|
||||||
|
|
||||||
|
/* ── Metrics ─────────────────────────────── */
|
||||||
|
.metrics { flex: 1; display: flex; flex-direction: column; gap: 10px; }
|
||||||
|
|
||||||
|
.metric {
|
||||||
|
background: #161b22;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px 22px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.metric-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8b949e;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.metric-icon { font-size: 22px; opacity: 0.7; }
|
||||||
|
|
||||||
|
.metric-value {
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 300;
|
||||||
|
color: #f0f6fc;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.metric-value.loading { color: #30363d; }
|
||||||
|
.metric-unit {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #8b949e;
|
||||||
|
font-weight: 400;
|
||||||
|
margin-left: 3px;
|
||||||
|
vertical-align: super;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Status bar ──────────────────────────── */
|
||||||
|
.statusbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #21262d;
|
||||||
|
}
|
||||||
|
.dot {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #3fb950;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.dot.offline { background: #484f58; animation: none; }
|
||||||
|
.dot.live { animation: pulse 2s infinite; }
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.3; }
|
||||||
|
}
|
||||||
|
.status-text { font-size: 12px; color: #484f58; }
|
||||||
|
.status-time { font-size: 12px; color: #484f58; margin-left: auto; }
|
||||||
|
|
||||||
|
/* ── Settings overlay ────────────────────── */
|
||||||
|
.overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0,0,0,0.6);
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.overlay.open { opacity: 1; pointer-events: all; }
|
||||||
|
|
||||||
|
.sheet {
|
||||||
|
position: fixed;
|
||||||
|
left: 0; right: 0; bottom: 0;
|
||||||
|
background: #161b22;
|
||||||
|
border-top: 1px solid #30363d;
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
transform: translateY(100%);
|
||||||
|
transition: transform 0.25s cubic-bezier(0.32,0.72,0,1);
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
.sheet.open { transform: translateY(0); }
|
||||||
|
|
||||||
|
.sheet-handle {
|
||||||
|
width: 32px;
|
||||||
|
height: 3px;
|
||||||
|
background: #30363d;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
}
|
||||||
|
.sheet-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #8b949e;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.7px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.sheet-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: #0d1117;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #c9d1d9;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
.sheet-link:hover { border-color: #8b949e; }
|
||||||
|
.sheet-link:last-of-type { margin-bottom: 0; }
|
||||||
|
.sheet-arrow { color: #484f58; font-size: 16px; }
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Sensor Pico</h1>
|
|
||||||
<br>
|
<div class="topbar">
|
||||||
<a href="live_stats.html">Live Daten</a>
|
<div>
|
||||||
<br>
|
<div class="topbar-title">Sensor Pico</div>
|
||||||
<a href="wlan_config.html">WLAN Config</a>
|
<div class="topbar-sub">BME280</div>
|
||||||
<br>
|
</div>
|
||||||
<a href="mqtt_config.html">MQTT und Messungs Config</a>
|
<div class="settings-btn" onclick="openSettings()">⚙</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="metrics">
|
||||||
|
<div class="metric">
|
||||||
|
<div>
|
||||||
|
<div class="metric-label">Temperatur</div>
|
||||||
|
<div>
|
||||||
|
<span class="metric-value loading" id="temp">--</span>
|
||||||
|
<span class="metric-unit">°C</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="metric-icon">🌡</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric">
|
||||||
|
<div>
|
||||||
|
<div class="metric-label">Luftfeuchtigkeit</div>
|
||||||
|
<div>
|
||||||
|
<span class="metric-value loading" id="hum">--</span>
|
||||||
|
<span class="metric-unit">%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="metric-icon">💧</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric">
|
||||||
|
<div>
|
||||||
|
<div class="metric-label">Luftdruck</div>
|
||||||
|
<div>
|
||||||
|
<span class="metric-value loading" id="press">--</span>
|
||||||
|
<span class="metric-unit">hPa</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="metric-icon">🔆</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="statusbar">
|
||||||
|
<div class="dot offline" id="dot"></div>
|
||||||
|
<span class="status-text" id="status">Verbinde…</span>
|
||||||
|
<span class="status-time" id="time"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings sheet -->
|
||||||
|
<div class="overlay" id="overlay" onclick="closeSettings()"></div>
|
||||||
|
<div class="sheet" id="sheet">
|
||||||
|
<div class="sheet-handle"></div>
|
||||||
|
<div class="sheet-title">Einstellungen</div>
|
||||||
|
<a href="wlan_config.html" class="sheet-link">
|
||||||
|
WLAN konfigurieren
|
||||||
|
<span class="sheet-arrow">›</span>
|
||||||
|
</a>
|
||||||
|
<a href="mqtt_config.html" class="sheet-link">
|
||||||
|
MQTT & Messung
|
||||||
|
<span class="sheet-arrow">›</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function openSettings() {
|
||||||
|
document.getElementById('overlay').classList.add('open');
|
||||||
|
document.getElementById('sheet').classList.add('open');
|
||||||
|
}
|
||||||
|
function closeSettings() {
|
||||||
|
document.getElementById('overlay').classList.remove('open');
|
||||||
|
document.getElementById('sheet').classList.remove('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
function pad(n) { return n < 10 ? '0' + n : n; }
|
||||||
|
|
||||||
|
function fetchData() {
|
||||||
|
fetch('/sensor-data.json')
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(d) {
|
||||||
|
var t = document.getElementById('temp');
|
||||||
|
var h = document.getElementById('hum');
|
||||||
|
var p = document.getElementById('press');
|
||||||
|
t.textContent = d.temperature.toFixed(1);
|
||||||
|
h.textContent = d.humidity.toFixed(1);
|
||||||
|
p.textContent = d.pressure.toFixed(1);
|
||||||
|
t.classList.remove('loading');
|
||||||
|
h.classList.remove('loading');
|
||||||
|
p.classList.remove('loading');
|
||||||
|
|
||||||
|
var dot = document.getElementById('dot');
|
||||||
|
dot.className = 'dot live';
|
||||||
|
document.getElementById('status').textContent = 'Live';
|
||||||
|
|
||||||
|
var now = new Date();
|
||||||
|
document.getElementById('time').textContent =
|
||||||
|
pad(now.getHours()) + ':' + pad(now.getMinutes()) + ':' + pad(now.getSeconds());
|
||||||
|
})
|
||||||
|
.catch(function() {
|
||||||
|
var dot = document.getElementById('dot');
|
||||||
|
dot.className = 'dot offline';
|
||||||
|
document.getElementById('status').textContent = 'Keine Daten';
|
||||||
|
document.getElementById('time').textContent = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
setInterval(fetchData, 3000);
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head><meta http-equiv="refresh" content="0; url=/"></head>
|
||||||
|
</html>
|
||||||
|
|||||||
@@ -1,32 +1,189 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="UTF-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Konfiguration</title>
|
<title>MQTT Konfiguration</title>
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
||||||
|
background: #0d1117;
|
||||||
|
color: #c9d1d9;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
.container { width: 100%; max-width: 420px; }
|
||||||
|
.back {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #8b949e;
|
||||||
|
text-decoration: none;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
.back:hover { color: #388bfd; }
|
||||||
|
.card {
|
||||||
|
background: #161b22;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 28px;
|
||||||
|
}
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.card-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: #2d2016;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f0f6fc;
|
||||||
|
}
|
||||||
|
.card-subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8b949e;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.section-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #484f58;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.8px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.form-group { margin-bottom: 16px; }
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #8b949e;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
input[type="text"],
|
||||||
|
input[type="password"],
|
||||||
|
input[type="number"] {
|
||||||
|
width: 100%;
|
||||||
|
background: #0d1117;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #f0f6fc;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
input:focus { border-color: #388bfd; }
|
||||||
|
input::placeholder { color: #484f58; }
|
||||||
|
.divider {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid #30363d;
|
||||||
|
margin: 22px 0;
|
||||||
|
}
|
||||||
|
.input-hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #484f58;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
button[type="submit"] {
|
||||||
|
width: 100%;
|
||||||
|
background: #238636;
|
||||||
|
border: 1px solid #2ea043;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 11px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, border-color 0.15s;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
button[type="submit"]:hover {
|
||||||
|
background: #2ea043;
|
||||||
|
border-color: #3fb950;
|
||||||
|
}
|
||||||
|
button[type="submit"]:active { background: #1a7f37; }
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>MQTT Konfiguration</h1>
|
<div class="container">
|
||||||
|
<a href="/" class="back">← Zurück</a>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-icon">⚙</div>
|
||||||
|
<div>
|
||||||
|
<h1>MQTT & Messung</h1>
|
||||||
|
<p class="card-subtitle">Broker und Intervalle konfigurieren</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<form action="/mqtt-config" method="post">
|
<form action="/mqtt-config" method="post">
|
||||||
<label>MQTT Adresse:</label>
|
<p class="section-label">Broker</p>
|
||||||
<input type="text" name="mqtt-address">
|
<div class="form-row">
|
||||||
<br>
|
<div>
|
||||||
<label>MQTT Port:</label>
|
<div class="form-group">
|
||||||
<input type="number" name="mqtt-port">
|
<label for="mqtt-address">Adresse</label>
|
||||||
<br>
|
<input type="text" id="mqtt-address" name="mqtt-address" placeholder="192.168.1.100" autocomplete="off">
|
||||||
<label>MQTT User:</label>
|
</div>
|
||||||
<input type="text" name="mqtt-user">
|
</div>
|
||||||
<br>
|
<div>
|
||||||
<label>MQTT Passwort:</label>
|
<div class="form-group">
|
||||||
<input type="password" name="mqtt-password">
|
<label for="mqtt-port">Port</label>
|
||||||
<br>
|
<input type="number" id="mqtt-port" name="mqtt-port" placeholder="1883" min="1" max="65535">
|
||||||
<label>Messfrequenz (ms)</label>
|
</div>
|
||||||
<input type="number" name="measure-frequency">
|
</div>
|
||||||
<br>
|
</div>
|
||||||
<label>Pushfrequenz (s)</label>
|
<div class="form-group">
|
||||||
<input type="number" name="push-frequency">
|
<label for="mqtt-user">Benutzer</label>
|
||||||
<br>
|
<input type="text" id="mqtt-user" name="mqtt-user" placeholder="Optional" autocomplete="off">
|
||||||
<input type="submit" value="Speichern">
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="mqtt-password">Passwort</label>
|
||||||
|
<input type="password" id="mqtt-password" name="mqtt-password" placeholder="Optional" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<hr class="divider">
|
||||||
|
<p class="section-label">Intervalle</p>
|
||||||
|
<div class="form-row">
|
||||||
|
<div>
|
||||||
|
<label for="measure-frequency">Messintervall</label>
|
||||||
|
<input type="number" id="measure-frequency" name="measure-frequency" placeholder="500" min="1">
|
||||||
|
<p class="input-hint">in Millisekunden</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="push-frequency">Sendeintervall</label>
|
||||||
|
<input type="number" id="push-frequency" name="push-frequency" placeholder="60" min="1">
|
||||||
|
<p class="input-hint">in Sekunden</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr class="divider">
|
||||||
|
<button type="submit">Speichern</button>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,20 +1,141 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="UTF-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Konfiguration</title>
|
<title>WLAN Konfiguration</title>
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
||||||
|
background: #0d1117;
|
||||||
|
color: #c9d1d9;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
.container { width: 100%; max-width: 420px; }
|
||||||
|
.back {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #8b949e;
|
||||||
|
text-decoration: none;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
.back:hover { color: #388bfd; }
|
||||||
|
.card {
|
||||||
|
background: #161b22;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 28px;
|
||||||
|
}
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.card-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: #1a2e23;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f0f6fc;
|
||||||
|
}
|
||||||
|
.card-subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8b949e;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.form-group { margin-bottom: 18px; }
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #8b949e;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
input[type="text"],
|
||||||
|
input[type="password"],
|
||||||
|
input[type="number"] {
|
||||||
|
width: 100%;
|
||||||
|
background: #0d1117;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #f0f6fc;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
input:focus { border-color: #388bfd; }
|
||||||
|
input::placeholder { color: #484f58; }
|
||||||
|
.divider {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid #30363d;
|
||||||
|
margin: 22px 0;
|
||||||
|
}
|
||||||
|
button[type="submit"] {
|
||||||
|
width: 100%;
|
||||||
|
background: #238636;
|
||||||
|
border: 1px solid #2ea043;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 11px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, border-color 0.15s;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
button[type="submit"]:hover {
|
||||||
|
background: #2ea043;
|
||||||
|
border-color: #3fb950;
|
||||||
|
}
|
||||||
|
button[type="submit"]:active { background: #1a7f37; }
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>WLAN Einstellungen</h1>
|
<div class="container">
|
||||||
|
<a href="/" class="back">← Zurück</a>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-icon">📶</div>
|
||||||
|
<div>
|
||||||
|
<h1>WLAN</h1>
|
||||||
|
<p class="card-subtitle">Netzwerkverbindung konfigurieren</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<form action="/wlan-config" method="post">
|
<form action="/wlan-config" method="post">
|
||||||
<label>SSID:</label>
|
<div class="form-group">
|
||||||
<input type="text" name="ssid">
|
<label for="ssid">SSID</label>
|
||||||
<br>
|
<input type="text" id="ssid" name="ssid" placeholder="Netzwerkname" autocomplete="off">
|
||||||
<label>Passwort:</label>
|
</div>
|
||||||
<input type="password" name="password">
|
<div class="form-group">
|
||||||
<br>
|
<label for="password">Passwort</label>
|
||||||
<input type="submit" value="Speichern">
|
<input type="password" id="password" name="password" placeholder="••••••••" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<hr class="divider">
|
||||||
|
<button type="submit">Speichern & Verbinden</button>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
#define LWIP_HTTPD_CGI 1
|
#define LWIP_HTTPD_CGI 1
|
||||||
#define LWIP_HTTPD_SSI 1
|
#define LWIP_HTTPD_SSI 1
|
||||||
#define LWIP_HTTPD_SUPPORT_POST 1
|
#define LWIP_HTTPD_SUPPORT_POST 1
|
||||||
|
#define LWIP_HTTPD_CUSTOM_FILES 1
|
||||||
|
|
||||||
#define LWIP_NETIF_HOSTNAME 1
|
#define LWIP_NETIF_HOSTNAME 1
|
||||||
#define LWIP_NETIF_STATUS_CALLBACK 1
|
#define LWIP_NETIF_STATUS_CALLBACK 1
|
||||||
|
|||||||
299
src/main.cpp
299
src/main.cpp
@@ -15,28 +15,21 @@
|
|||||||
|
|
||||||
static dhcp_server_t dhcp_server{};
|
static dhcp_server_t dhcp_server{};
|
||||||
|
|
||||||
void ap_init() {
|
/* ── WiFi ───────────────────────────────────────────────────────────────────
|
||||||
cyw43_arch_enable_ap_mode("SensorAP", "passwort123", CYW43_AUTH_WPA2_AES_PSK);
|
*/
|
||||||
|
|
||||||
ip_addr_t gw{};
|
static void ap_init() {
|
||||||
ip_addr_t mask{};
|
cyw43_arch_enable_ap_mode("SensorAP", "passwort123", CYW43_AUTH_WPA2_AES_PSK);
|
||||||
|
ip_addr_t gw{}, mask{};
|
||||||
IP4_ADDR(&gw, 192, 168, 4, 1);
|
IP4_ADDR(&gw, 192, 168, 4, 1);
|
||||||
IP4_ADDR(&mask, 255, 255, 255, 0);
|
IP4_ADDR(&mask, 255, 255, 255, 0);
|
||||||
|
|
||||||
dhcp_server_deinit(&dhcp_server);
|
dhcp_server_deinit(&dhcp_server);
|
||||||
dhcp_server_init(&dhcp_server, &gw, &mask);
|
dhcp_server_init(&dhcp_server, &gw, &mask);
|
||||||
}
|
}
|
||||||
|
|
||||||
void reset_mqtt_config() {
|
/* Open AP, wait for user to POST credentials, then connect STA.
|
||||||
memset(saved_mqtt_address, 0, sizeof(saved_mqtt_address));
|
Returns 0 on success. */
|
||||||
memset(saved_mqtt_user, 0, sizeof(saved_mqtt_user));
|
static int connect_via_ap() {
|
||||||
memset(saved_mqtt_password, 0, sizeof(saved_mqtt_password));
|
|
||||||
saved_measure_frequency = 0;
|
|
||||||
saved_post_frequency = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int connect_to_wifi() {
|
|
||||||
int ret{};
|
|
||||||
memset(saved_ssid, 0, sizeof(saved_ssid));
|
memset(saved_ssid, 0, sizeof(saved_ssid));
|
||||||
memset(saved_password, 0, sizeof(saved_password));
|
memset(saved_password, 0, sizeof(saved_password));
|
||||||
ap_init();
|
ap_init();
|
||||||
@@ -49,33 +42,41 @@ int connect_to_wifi() {
|
|||||||
cyw43_arch_disable_sta_mode();
|
cyw43_arch_disable_sta_mode();
|
||||||
sleep_ms(500);
|
sleep_ms(500);
|
||||||
cyw43_arch_enable_sta_mode();
|
cyw43_arch_enable_sta_mode();
|
||||||
|
return cyw43_arch_wifi_connect_timeout_ms(saved_ssid, saved_password,
|
||||||
ret = cyw43_arch_wifi_connect_timeout_ms(saved_ssid, saved_password,
|
|
||||||
CYW43_AUTH_WPA2_MIXED_PSK, 30000);
|
CYW43_AUTH_WPA2_MIXED_PSK, 30000);
|
||||||
if (ret == 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void mqtt_cb(mqtt_client_t *client, void *arg,
|
/* Connect STA with current saved credentials (no AP fallback). */
|
||||||
|
static int connect_sta() {
|
||||||
|
cyw43_arch_disable_sta_mode();
|
||||||
|
sleep_ms(500);
|
||||||
|
cyw43_arch_enable_sta_mode();
|
||||||
|
return cyw43_arch_wifi_connect_timeout_ms(saved_ssid, saved_password,
|
||||||
|
CYW43_AUTH_WPA2_MIXED_PSK, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── MQTT ───────────────────────────────────────────────────────────────────
|
||||||
|
*/
|
||||||
|
|
||||||
|
static void mqtt_cb(mqtt_client_t *client, void *arg,
|
||||||
mqtt_connection_status_t status) {
|
mqtt_connection_status_t status) {
|
||||||
|
(void)client;
|
||||||
int *mqtt_status{static_cast<int *>(arg)};
|
int *mqtt_status{static_cast<int *>(arg)};
|
||||||
*mqtt_status = (status == MQTT_CONNECT_ACCEPTED) ? 0 : 1;
|
*mqtt_status = (status == MQTT_CONNECT_ACCEPTED) ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
mqtt_client_t *connect_to_mqtt() {
|
static mqtt_client_t *connect_to_mqtt() {
|
||||||
static int mqtt_status{-1};
|
static int mqtt_status{-1};
|
||||||
ip_addr_t broker_ip;
|
|
||||||
static mqtt_client_t *client{};
|
static mqtt_client_t *client{};
|
||||||
static mqtt_connect_client_info_t info{};
|
static mqtt_connect_client_info_t info{};
|
||||||
|
ip_addr_t broker_ip;
|
||||||
|
|
||||||
if (!ipaddr_aton(saved_mqtt_address, &broker_ip)) {
|
if (!ipaddr_aton(saved_mqtt_address, &broker_ip))
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
|
||||||
if (!client) {
|
if (!client)
|
||||||
client = mqtt_client_new();
|
client = mqtt_client_new();
|
||||||
}
|
|
||||||
info.client_id = "sensor-pico";
|
info.client_id = "sensor-pico";
|
||||||
info.client_user = saved_mqtt_user;
|
info.client_user = saved_mqtt_user;
|
||||||
info.client_pass = saved_mqtt_password;
|
info.client_pass = saved_mqtt_password;
|
||||||
@@ -87,70 +88,62 @@ mqtt_client_t *connect_to_mqtt() {
|
|||||||
cyw43_arch_poll();
|
cyw43_arch_poll();
|
||||||
sleep_ms(100);
|
sleep_ms(100);
|
||||||
}
|
}
|
||||||
if (mqtt_status == 0) {
|
return (mqtt_status == 0) ? client : nullptr;
|
||||||
return client;
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BME280_READING_INTERVALS_MS convert_Interval(int interval) {
|
static void publish_cb(void *arg, err_t err) {
|
||||||
if (interval < 1) {
|
(void)arg;
|
||||||
return INTERVAL_0_5MS;
|
(void)err;
|
||||||
} else if (interval < 20 && interval > 1) {
|
printf("Publish successful!\n");
|
||||||
return INTERVAL_10MS;
|
|
||||||
} else if (interval < 30 && interval > 10) {
|
|
||||||
return INTERVAL_20MS;
|
|
||||||
} else if (interval < 125 && interval > 20) {
|
|
||||||
return INTERVAL_62_5MS;
|
|
||||||
} else if (interval < 250 && interval > 62.5) {
|
|
||||||
return INTERVAL_125MS;
|
|
||||||
} else if (interval < 500 && interval > 125) {
|
|
||||||
return INTERVAL_250MS;
|
|
||||||
} else if (interval < 1000 && interval > 250) {
|
|
||||||
return INTERVAL_500MS;
|
|
||||||
} else if (interval > 1000) {
|
|
||||||
return INTERVAL_1000MS;
|
|
||||||
} else {
|
|
||||||
return INTERVAL_500MS;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void publish_cb(void *arg, err_t err) { printf("Publish succesfull!\n"); }
|
static void publish_mqtt(mqtt_client_t *client, const char *payload) {
|
||||||
|
|
||||||
void publish_mqtt(mqtt_client_t *client, const char *payload) {
|
|
||||||
uint payload_len{strlen(payload)};
|
uint payload_len{strlen(payload)};
|
||||||
mqtt_publish(client, "tele/sensor-pico/SENSOR", payload, payload_len, 1, 1,
|
mqtt_publish(client, "tele/sensor-pico/SENSOR", payload, payload_len, 1, 1,
|
||||||
publish_cb, nullptr);
|
publish_cb, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool parse_json(const char *raw, float &temperature, float &humidity,
|
/* ── BME280 ─────────────────────────────────────────────────────────────────
|
||||||
|
*/
|
||||||
|
|
||||||
|
static BME280_READING_INTERVALS_MS convert_interval(int ms) {
|
||||||
|
if (ms < 1)
|
||||||
|
return INTERVAL_0_5MS;
|
||||||
|
if (ms < 20)
|
||||||
|
return INTERVAL_10MS;
|
||||||
|
if (ms < 30)
|
||||||
|
return INTERVAL_20MS;
|
||||||
|
if (ms < 125)
|
||||||
|
return INTERVAL_62_5MS;
|
||||||
|
if (ms < 250)
|
||||||
|
return INTERVAL_125MS;
|
||||||
|
if (ms < 500)
|
||||||
|
return INTERVAL_250MS;
|
||||||
|
if (ms < 1000)
|
||||||
|
return INTERVAL_500MS;
|
||||||
|
return INTERVAL_1000MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool parse_json(const char *raw, float &temperature, float &humidity,
|
||||||
float &pressure) {
|
float &pressure) {
|
||||||
if (!raw)
|
if (!raw)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
const char *t_ptr = std::strstr(raw, "\"temperature\":");
|
const char *t_ptr = std::strstr(raw, "\"temperature\":");
|
||||||
const char *h_ptr = std::strstr(raw, "\"humidity\":");
|
const char *h_ptr = std::strstr(raw, "\"humidity\":");
|
||||||
const char *p_ptr = std::strstr(raw, "\"pressure\":");
|
const char *p_ptr = std::strstr(raw, "\"pressure\":");
|
||||||
|
|
||||||
if (!t_ptr || !h_ptr || !p_ptr)
|
if (!t_ptr || !h_ptr || !p_ptr)
|
||||||
return false;
|
return false;
|
||||||
|
temperature = std::strtol(t_ptr + 14, nullptr, 10) / 100.0f;
|
||||||
long t_raw = std::strtol(t_ptr + 14, nullptr, 10);
|
humidity = std::strtol(h_ptr + 11, nullptr, 10) / 1024.0f;
|
||||||
long h_raw = std::strtol(h_ptr + 11, nullptr, 10);
|
pressure = std::strtol(p_ptr + 11, nullptr, 10) / 25600.0f;
|
||||||
long p_raw = std::strtol(p_ptr + 11, nullptr, 10);
|
|
||||||
|
|
||||||
temperature = t_raw / 100.0f;
|
|
||||||
humidity = h_raw / 1024.0f;
|
|
||||||
pressure = p_raw / 25600.0f;
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void build_sensor_payload(char *buffer, size_t size, bme280_handle_t handle,
|
static void build_sensor_payload(char *buffer, size_t size,
|
||||||
|
bme280_handle_t handle,
|
||||||
const char *sensor_name) {
|
const char *sensor_name) {
|
||||||
bme280_read_data(handle);
|
bme280_read_data(handle);
|
||||||
const char *raw = bme280_get_json(handle);
|
const char *raw = bme280_get_json(handle);
|
||||||
|
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
snprintf(buffer, size, "{\"error\":\"no data\"}");
|
snprintf(buffer, size, "{\"error\":\"no data\"}");
|
||||||
return;
|
return;
|
||||||
@@ -158,37 +151,32 @@ void build_sensor_payload(char *buffer, size_t size, bme280_handle_t handle,
|
|||||||
|
|
||||||
float temperature, humidity, pressure;
|
float temperature, humidity, pressure;
|
||||||
parse_json(raw, temperature, humidity, pressure);
|
parse_json(raw, temperature, humidity, pressure);
|
||||||
|
webserver_update_sensor(temperature, humidity, pressure);
|
||||||
|
|
||||||
time_t now;
|
time_t now;
|
||||||
time(&now);
|
time(&now);
|
||||||
struct tm *timeinfo = localtime(&now);
|
|
||||||
|
|
||||||
char time_str[32];
|
char time_str[32];
|
||||||
strftime(time_str, sizeof(time_str), "%Y-%m-%dT%H:%M:%S", timeinfo);
|
strftime(time_str, sizeof(time_str), "%Y-%m-%dT%H:%M:%S", localtime(&now));
|
||||||
|
|
||||||
snprintf(buffer, size,
|
snprintf(buffer, size,
|
||||||
"{"
|
"{\"Time\":\"%s\",\"%s\":{"
|
||||||
"\"Time\":\"%s\","
|
"\"Temperature\":%.2f,\"Humidity\":%.2f,\"Pressure\":%.2f"
|
||||||
"\"%s\":{"
|
"},\"TempUnit\":\"C\"}",
|
||||||
"\"Temperature\":%.2f,"
|
|
||||||
"\"Humidity\":%.2f,"
|
|
||||||
"\"Pressure\":%.2f"
|
|
||||||
"},"
|
|
||||||
"\"TempUnit\":\"C\""
|
|
||||||
"}",
|
|
||||||
time_str, sensor_name, temperature, humidity, pressure);
|
time_str, sensor_name, temperature, humidity, pressure);
|
||||||
}
|
}
|
||||||
bool is_time_synced() {
|
|
||||||
time_t now = time(NULL);
|
/* ── SNTP ───────────────────────────────────────────────────────────────────
|
||||||
struct tm *timeinfo = localtime(&now);
|
*/
|
||||||
return timeinfo->tm_year > 70;
|
|
||||||
|
static bool is_time_synced() {
|
||||||
|
time_t now = time(nullptr);
|
||||||
|
return localtime(&now)->tm_year > 70;
|
||||||
}
|
}
|
||||||
|
|
||||||
void start_sntp() {
|
static void start_sntp() {
|
||||||
cyw43_arch_lwip_begin();
|
cyw43_arch_lwip_begin();
|
||||||
if (sntp_enabled()) {
|
if (sntp_enabled())
|
||||||
sntp_stop();
|
sntp_stop();
|
||||||
}
|
|
||||||
sntp_setoperatingmode(SNTP_OPMODE_POLL);
|
sntp_setoperatingmode(SNTP_OPMODE_POLL);
|
||||||
#if SNTP_SERVER_DNS
|
#if SNTP_SERVER_DNS
|
||||||
sntp_setservername(0, "de.pool.ntp.org");
|
sntp_setservername(0, "de.pool.ntp.org");
|
||||||
@@ -197,15 +185,10 @@ void start_sntp() {
|
|||||||
cyw43_arch_lwip_end();
|
cyw43_arch_lwip_end();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Main ───────────────────────────────────────────────────────────────────
|
||||||
|
*/
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
uint32_t last_publish{};
|
|
||||||
uint32_t last_sntp_retry_ms{};
|
|
||||||
int mqtt_ret{-1};
|
|
||||||
int wifi_status{1};
|
|
||||||
int bme_status{-1};
|
|
||||||
char payload[256];
|
|
||||||
mqtt_client_t *client{};
|
|
||||||
bme280_handle_t handle{};
|
|
||||||
stdio_init_all();
|
stdio_init_all();
|
||||||
setenv("TZ", "CET-1CEST,M3.5.0,M10.5.0", 1);
|
setenv("TZ", "CET-1CEST,M3.5.0,M10.5.0", 1);
|
||||||
tzset();
|
tzset();
|
||||||
@@ -213,55 +196,117 @@ int main() {
|
|||||||
cyw43_arch_init();
|
cyw43_arch_init();
|
||||||
httpd_init();
|
httpd_init();
|
||||||
|
|
||||||
while (wifi_status != 0) {
|
/* ── 1. Load config from flash ── */
|
||||||
wifi_status = (connect_to_wifi() == 0) ? 0 : 1;
|
bool has_saved_config = config_load();
|
||||||
printf("Wifi status: %d\n", wifi_status);
|
|
||||||
}
|
|
||||||
printf("Connected to wifi!\n");
|
|
||||||
printf("IP: %s\n", ip4addr_ntoa(netif_ip4_addr(netif_default)));
|
|
||||||
start_sntp();
|
|
||||||
last_sntp_retry_ms = to_ms_since_boot(get_absolute_time());
|
|
||||||
|
|
||||||
while (true) {
|
/* ── 2. Connect to WiFi ── */
|
||||||
|
if (has_saved_config && saved_ssid[0] != '\0') {
|
||||||
|
printf("Trying saved WiFi: %s\n", saved_ssid);
|
||||||
|
if (connect_sta() != 0) {
|
||||||
|
printf("Saved WiFi failed, opening AP...\n");
|
||||||
|
while (connect_via_ap() != 0) {
|
||||||
|
printf("AP connect failed, retrying...\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while (connect_via_ap() != 0) {
|
||||||
|
printf("AP connect failed, retrying...\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
printf("WiFi connected! IP: %s\n",
|
||||||
|
ip4addr_ntoa(netif_ip4_addr(netif_default)));
|
||||||
|
|
||||||
|
start_sntp();
|
||||||
|
uint32_t last_sntp_retry_ms = to_ms_since_boot(get_absolute_time());
|
||||||
|
|
||||||
|
/* ── 3. Wait for MQTT config if not saved ── */
|
||||||
while (saved_mqtt_address[0] == '\0') {
|
while (saved_mqtt_address[0] == '\0') {
|
||||||
cyw43_arch_poll();
|
cyw43_arch_poll();
|
||||||
sleep_ms(200);
|
sleep_ms(200);
|
||||||
}
|
}
|
||||||
client = connect_to_mqtt();
|
|
||||||
if (client == nullptr) {
|
/* ── 4. Connect to MQTT ── */
|
||||||
printf("Mqtt Status: %d\n", mqtt_ret);
|
mqtt_client_t *client{};
|
||||||
reset_mqtt_config();
|
|
||||||
} else {
|
|
||||||
printf("Connected to mqtt!\n");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bme_status =
|
|
||||||
bme280_init(&handle, 0x76, convert_Interval(saved_measure_frequency));
|
|
||||||
if (!bme_status) {
|
|
||||||
last_publish = to_ms_since_boot(get_absolute_time());
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if ((to_ms_since_boot(get_absolute_time()) - last_publish) >=
|
client = connect_to_mqtt();
|
||||||
saved_post_frequency * 1000) {
|
if (client)
|
||||||
|
break;
|
||||||
|
printf("MQTT connect failed, waiting for new config...\n");
|
||||||
|
memset(saved_mqtt_address, 0, sizeof(saved_mqtt_address));
|
||||||
|
while (saved_mqtt_address[0] == '\0') {
|
||||||
|
cyw43_arch_poll();
|
||||||
|
sleep_ms(200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
printf("MQTT connected!\n");
|
||||||
|
|
||||||
|
/* ── 5. Init BME280 ── */
|
||||||
|
bme280_handle_t handle{};
|
||||||
|
bool bme_ok = (bme280_init(&handle, 0x76,
|
||||||
|
convert_interval(saved_measure_frequency)) == 0);
|
||||||
|
|
||||||
|
char payload[256];
|
||||||
|
uint32_t last_publish = to_ms_since_boot(get_absolute_time());
|
||||||
|
|
||||||
|
/* ── 6. Main loop ── */
|
||||||
|
while (true) {
|
||||||
|
|
||||||
|
/* ── Handle WiFi config change ── */
|
||||||
|
if (wlan_config_updated) {
|
||||||
|
wlan_config_updated = false;
|
||||||
|
config_save();
|
||||||
|
printf("WiFi config updated, reconnecting...\n");
|
||||||
|
if (mqtt_client_is_connected(client))
|
||||||
|
mqtt_disconnect(client);
|
||||||
|
if (connect_sta() == 0) {
|
||||||
|
printf("WiFi reconnected!\n");
|
||||||
|
start_sntp();
|
||||||
|
last_sntp_retry_ms = to_ms_since_boot(get_absolute_time());
|
||||||
|
} else {
|
||||||
|
printf("WiFi reconnect failed!\n");
|
||||||
|
}
|
||||||
|
/* Force MQTT reconnect after WiFi change */
|
||||||
|
mqtt_config_updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Handle MQTT / interval config change ── */
|
||||||
|
if (mqtt_config_updated) {
|
||||||
|
mqtt_config_updated = false;
|
||||||
|
config_save();
|
||||||
|
printf("MQTT config updated, reconnecting...\n");
|
||||||
|
if (mqtt_client_is_connected(client))
|
||||||
|
mqtt_disconnect(client);
|
||||||
|
client = connect_to_mqtt();
|
||||||
|
if (client) {
|
||||||
|
printf("MQTT reconnected!\n");
|
||||||
|
bme_ok = (bme280_init(&handle, 0x76,
|
||||||
|
convert_interval(saved_measure_frequency)) == 0);
|
||||||
|
last_publish = to_ms_since_boot(get_absolute_time());
|
||||||
|
} else {
|
||||||
|
printf("MQTT reconnect failed!\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Periodic publish ── */
|
||||||
|
uint32_t now_ms = to_ms_since_boot(get_absolute_time());
|
||||||
|
if (client && bme_ok &&
|
||||||
|
(now_ms - last_publish) >= uint32_t(saved_post_frequency) * 1000) {
|
||||||
if (is_time_synced()) {
|
if (is_time_synced()) {
|
||||||
build_sensor_payload(payload, sizeof(payload), handle, "BME280");
|
build_sensor_payload(payload, sizeof(payload), handle, "BME280");
|
||||||
publish_mqtt(client, payload);
|
publish_mqtt(client, payload);
|
||||||
printf("Payload: %s\n", payload);
|
printf("Payload: %s\n", payload);
|
||||||
last_publish = to_ms_since_boot(get_absolute_time());
|
last_publish = now_ms;
|
||||||
} else {
|
} else {
|
||||||
printf("Waiting for SNTP sync (Current year: 1970)...\n");
|
printf("Waiting for SNTP sync...\n");
|
||||||
uint32_t now_ms = to_ms_since_boot(get_absolute_time());
|
|
||||||
if (now_ms - last_sntp_retry_ms > 30000) {
|
if (now_ms - last_sntp_retry_ms > 30000) {
|
||||||
printf("Retrying SNTP...\n");
|
|
||||||
start_sntp();
|
start_sntp();
|
||||||
last_sntp_retry_ms = now_ms;
|
last_sntp_retry_ms = now_ms;
|
||||||
}
|
}
|
||||||
last_publish = to_ms_since_boot(get_absolute_time()) -
|
last_publish = now_ms - uint32_t(saved_post_frequency) * 1000 + 5000;
|
||||||
(saved_post_frequency * 1000) + 5000;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cyw43_arch_poll();
|
cyw43_arch_poll();
|
||||||
sleep_ms(100);
|
sleep_ms(100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
144
src/webserver.c
144
src/webserver.c
@@ -1,7 +1,52 @@
|
|||||||
#include "lwip/apps/httpd.h"
|
#include "lwip/apps/httpd.h"
|
||||||
|
#include "lwip/apps/fs.h"
|
||||||
|
#include "hardware/flash.h"
|
||||||
|
#include "hardware/sync.h"
|
||||||
|
#include "pico/platform.h"
|
||||||
#include "string.h"
|
#include "string.h"
|
||||||
|
#include <stdbool.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
/* ── Sensor data endpoint ─────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
static char sensor_response[256];
|
||||||
|
static int sensor_response_len = 0;
|
||||||
|
|
||||||
|
void webserver_update_sensor(float temperature, float humidity, float pressure) {
|
||||||
|
const char *hdr = "HTTP/1.0 200 OK\r\n"
|
||||||
|
"Content-Type: application/json\r\n"
|
||||||
|
"\r\n";
|
||||||
|
char body[96];
|
||||||
|
int blen = snprintf(body, sizeof(body),
|
||||||
|
"{\"temperature\":%.2f,\"humidity\":%.2f,\"pressure\":%.2f}",
|
||||||
|
temperature, humidity, pressure);
|
||||||
|
int hlen = strlen(hdr);
|
||||||
|
if (hlen + blen < (int)sizeof(sensor_response)) {
|
||||||
|
memcpy(sensor_response, hdr, hlen);
|
||||||
|
memcpy(sensor_response + hlen, body, blen);
|
||||||
|
sensor_response_len = hlen + blen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int fs_open_custom(struct fs_file *file, const char *name) {
|
||||||
|
if (strcmp(name, "/sensor-data.json") == 0) {
|
||||||
|
memset(file, 0, sizeof(struct fs_file));
|
||||||
|
file->data = sensor_response;
|
||||||
|
file->len = sensor_response_len;
|
||||||
|
file->index = sensor_response_len;
|
||||||
|
file->flags = FS_FILE_FLAGS_HEADER_INCLUDED | FS_FILE_FLAGS_HEADER_PERSISTENT;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void fs_close_custom(struct fs_file *file) {
|
||||||
|
(void)file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Saved config variables ───────────────────────────────────────────────── */
|
||||||
|
|
||||||
char saved_ssid[33];
|
char saved_ssid[33];
|
||||||
char saved_password[65];
|
char saved_password[65];
|
||||||
@@ -12,8 +57,84 @@ int saved_measure_frequency;
|
|||||||
int saved_post_frequency;
|
int saved_post_frequency;
|
||||||
int saved_mqtt_port;
|
int saved_mqtt_port;
|
||||||
|
|
||||||
static int post_state;
|
/* ── Config update flags (set by POST handlers, read in main loop) ────────── */
|
||||||
|
|
||||||
|
volatile bool wlan_config_updated = false;
|
||||||
|
volatile bool mqtt_config_updated = false;
|
||||||
|
|
||||||
|
/* ── Flash persistence ────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
#define FLASH_CONFIG_MAGIC 0xC0DECAFEu
|
||||||
|
#define FLASH_CONFIG_OFFSET (PICO_FLASH_SIZE_BYTES - FLASH_SECTOR_SIZE)
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint32_t magic;
|
||||||
|
char ssid[33];
|
||||||
|
char password[65];
|
||||||
|
char mqtt_address[65];
|
||||||
|
char mqtt_user[33];
|
||||||
|
char mqtt_password[65];
|
||||||
|
int32_t mqtt_port;
|
||||||
|
int32_t measure_frequency;
|
||||||
|
int32_t post_frequency;
|
||||||
|
} config_t;
|
||||||
|
|
||||||
|
_Static_assert(sizeof(config_t) <= FLASH_PAGE_SIZE * 2,
|
||||||
|
"config_t does not fit in 2 flash pages");
|
||||||
|
|
||||||
|
void config_save(void) {
|
||||||
|
static uint8_t buf[FLASH_PAGE_SIZE * 2];
|
||||||
|
memset(buf, 0xff, sizeof(buf));
|
||||||
|
|
||||||
|
config_t *cfg = (config_t *)buf;
|
||||||
|
cfg->magic = FLASH_CONFIG_MAGIC;
|
||||||
|
strncpy(cfg->ssid, saved_ssid, sizeof(cfg->ssid) - 1);
|
||||||
|
strncpy(cfg->password, saved_password, sizeof(cfg->password) - 1);
|
||||||
|
strncpy(cfg->mqtt_address, saved_mqtt_address, sizeof(cfg->mqtt_address) - 1);
|
||||||
|
strncpy(cfg->mqtt_user, saved_mqtt_user, sizeof(cfg->mqtt_user) - 1);
|
||||||
|
strncpy(cfg->mqtt_password, saved_mqtt_password, sizeof(cfg->mqtt_password) - 1);
|
||||||
|
cfg->mqtt_port = (int32_t)saved_mqtt_port;
|
||||||
|
cfg->measure_frequency = (int32_t)saved_measure_frequency;
|
||||||
|
cfg->post_frequency = (int32_t)saved_post_frequency;
|
||||||
|
|
||||||
|
uint32_t ints = save_and_disable_interrupts();
|
||||||
|
flash_range_erase(FLASH_CONFIG_OFFSET, FLASH_SECTOR_SIZE);
|
||||||
|
flash_range_program(FLASH_CONFIG_OFFSET, buf, sizeof(buf));
|
||||||
|
restore_interrupts(ints);
|
||||||
|
|
||||||
|
printf("Config saved to flash\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool config_load(void) {
|
||||||
|
const config_t *cfg =
|
||||||
|
(const config_t *)(uintptr_t)(XIP_BASE + FLASH_CONFIG_OFFSET);
|
||||||
|
|
||||||
|
if (cfg->magic != FLASH_CONFIG_MAGIC) {
|
||||||
|
printf("No valid config in flash\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
strncpy(saved_ssid, cfg->ssid, sizeof(saved_ssid) - 1);
|
||||||
|
strncpy(saved_password, cfg->password, sizeof(saved_password) - 1);
|
||||||
|
strncpy(saved_mqtt_address, cfg->mqtt_address, sizeof(saved_mqtt_address) - 1);
|
||||||
|
strncpy(saved_mqtt_user, cfg->mqtt_user, sizeof(saved_mqtt_user) - 1);
|
||||||
|
strncpy(saved_mqtt_password, cfg->mqtt_password, sizeof(saved_mqtt_password) - 1);
|
||||||
|
saved_ssid[sizeof(saved_ssid) - 1] = '\0';
|
||||||
|
saved_password[sizeof(saved_password) - 1] = '\0';
|
||||||
|
saved_mqtt_address[sizeof(saved_mqtt_address) - 1] = '\0';
|
||||||
|
saved_mqtt_user[sizeof(saved_mqtt_user) - 1] = '\0';
|
||||||
|
saved_mqtt_password[sizeof(saved_mqtt_password) - 1] = '\0';
|
||||||
|
saved_mqtt_port = (int)cfg->mqtt_port;
|
||||||
|
saved_measure_frequency = (int)cfg->measure_frequency;
|
||||||
|
saved_post_frequency = (int)cfg->post_frequency;
|
||||||
|
|
||||||
|
printf("Config loaded from flash\n");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── POST parsing helper ──────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
static int post_state;
|
||||||
static char post_buffer[400];
|
static char post_buffer[400];
|
||||||
static uint16_t post_buffer_len = 0;
|
static uint16_t post_buffer_len = 0;
|
||||||
|
|
||||||
@@ -27,24 +148,24 @@ void parse_post(const char *key, char delimiter, char *dest, int max_len) {
|
|||||||
char *pos = start + strlen(key);
|
char *pos = start + strlen(key);
|
||||||
char *end = (delimiter == '\0') ? (post_buffer + strlen(post_buffer))
|
char *end = (delimiter == '\0') ? (post_buffer + strlen(post_buffer))
|
||||||
: strchr(pos, delimiter);
|
: strchr(pos, delimiter);
|
||||||
|
if (!end)
|
||||||
if (!end) {
|
|
||||||
end = post_buffer + strlen(post_buffer);
|
end = post_buffer + strlen(post_buffer);
|
||||||
}
|
|
||||||
|
|
||||||
int len = end - pos;
|
int len = end - pos;
|
||||||
if (len > max_len) {
|
if (len > max_len)
|
||||||
len = max_len;
|
len = max_len;
|
||||||
}
|
|
||||||
|
|
||||||
strncpy(dest, pos, len);
|
strncpy(dest, pos, len);
|
||||||
dest[len] = '\0';
|
dest[len] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── lwip httpd POST callbacks ────────────────────────────────────────────── */
|
||||||
|
|
||||||
err_t httpd_post_begin(void *connection, const char *uri,
|
err_t httpd_post_begin(void *connection, const char *uri,
|
||||||
const char *http_request, u16_t http_request_len,
|
const char *http_request, u16_t http_request_len,
|
||||||
int content_len, char *response_uri,
|
int content_len, char *response_uri,
|
||||||
u16_t response_uri_len, u8_t *post_auto_wnd) {
|
u16_t response_uri_len, u8_t *post_auto_wnd) {
|
||||||
|
(void)connection; (void)http_request; (void)http_request_len; (void)content_len;
|
||||||
if (strcmp(uri, "/wlan-config") == 0) {
|
if (strcmp(uri, "/wlan-config") == 0) {
|
||||||
strncpy(response_uri, "/wlan_config.html", response_uri_len);
|
strncpy(response_uri, "/wlan_config.html", response_uri_len);
|
||||||
*post_auto_wnd = 1;
|
*post_auto_wnd = 1;
|
||||||
@@ -52,15 +173,16 @@ err_t httpd_post_begin(void *connection, const char *uri,
|
|||||||
return ERR_OK;
|
return ERR_OK;
|
||||||
}
|
}
|
||||||
if (strcmp(uri, "/mqtt-config") == 0) {
|
if (strcmp(uri, "/mqtt-config") == 0) {
|
||||||
post_state = 1;
|
|
||||||
strncpy(response_uri, "/mqtt_config.html", response_uri_len);
|
strncpy(response_uri, "/mqtt_config.html", response_uri_len);
|
||||||
*post_auto_wnd = 1;
|
*post_auto_wnd = 1;
|
||||||
|
post_state = 1;
|
||||||
return ERR_OK;
|
return ERR_OK;
|
||||||
}
|
}
|
||||||
return ERR_VAL;
|
return ERR_VAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
err_t httpd_post_receive_data(void *connection, struct pbuf *p) {
|
err_t httpd_post_receive_data(void *connection, struct pbuf *p) {
|
||||||
|
(void)connection;
|
||||||
post_buffer_len =
|
post_buffer_len =
|
||||||
pbuf_copy_partial(p, post_buffer, sizeof(post_buffer) - 1, 0);
|
pbuf_copy_partial(p, post_buffer, sizeof(post_buffer) - 1, 0);
|
||||||
post_buffer[post_buffer_len] = '\0';
|
post_buffer[post_buffer_len] = '\0';
|
||||||
@@ -70,27 +192,25 @@ err_t httpd_post_receive_data(void *connection, struct pbuf *p) {
|
|||||||
|
|
||||||
void httpd_post_finished(void *connection, char *response_uri,
|
void httpd_post_finished(void *connection, char *response_uri,
|
||||||
u16_t response_uri_len) {
|
u16_t response_uri_len) {
|
||||||
|
(void)connection;
|
||||||
if (post_state == 0) {
|
if (post_state == 0) {
|
||||||
strncpy(response_uri, "/wlan_config.html", response_uri_len);
|
strncpy(response_uri, "/wlan_config.html", response_uri_len);
|
||||||
|
|
||||||
parse_post("ssid=", '&', saved_ssid, 32);
|
parse_post("ssid=", '&', saved_ssid, 32);
|
||||||
parse_post("password=", '\0', saved_password, 64);
|
parse_post("password=", '\0', saved_password, 64);
|
||||||
|
wlan_config_updated = true;
|
||||||
|
|
||||||
} else if (post_state == 1) {
|
} else if (post_state == 1) {
|
||||||
char temp[16];
|
char temp[16];
|
||||||
strncpy(response_uri, "/mqtt_config.html", response_uri_len);
|
strncpy(response_uri, "/mqtt_config.html", response_uri_len);
|
||||||
|
|
||||||
parse_post("mqtt-address=", '&', saved_mqtt_address, 64);
|
parse_post("mqtt-address=", '&', saved_mqtt_address, 64);
|
||||||
parse_post("mqtt-user=", '&', saved_mqtt_user, 32);
|
parse_post("mqtt-user=", '&', saved_mqtt_user, 32);
|
||||||
parse_post("mqtt-password=", '&', saved_mqtt_password, 64);
|
parse_post("mqtt-password=", '&', saved_mqtt_password, 64);
|
||||||
|
|
||||||
parse_post("mqtt-port=", '&', temp, 15);
|
parse_post("mqtt-port=", '&', temp, 15);
|
||||||
saved_mqtt_port = atoi(temp);
|
saved_mqtt_port = atoi(temp);
|
||||||
|
|
||||||
parse_post("measure-frequency=", '&', temp, 15);
|
parse_post("measure-frequency=", '&', temp, 15);
|
||||||
saved_measure_frequency = atoi(temp);
|
saved_measure_frequency = atoi(temp);
|
||||||
|
|
||||||
parse_post("push-frequency=", '\0', temp, 15);
|
parse_post("push-frequency=", '\0', temp, 15);
|
||||||
saved_post_frequency = atoi(temp);
|
saved_post_frequency = atoi(temp);
|
||||||
|
mqtt_config_updated = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
#ifndef WEBSERVER_H
|
#ifndef WEBSERVER_H
|
||||||
#define WEBSERVER_H
|
#define WEBSERVER_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/* Saved configuration (written by POST handlers, read by main) */
|
||||||
extern char saved_ssid[33];
|
extern char saved_ssid[33];
|
||||||
extern char saved_password[65];
|
extern char saved_password[65];
|
||||||
extern char saved_mqtt_address[65];
|
extern char saved_mqtt_address[65];
|
||||||
@@ -14,6 +17,17 @@ extern int saved_measure_frequency;
|
|||||||
extern int saved_post_frequency;
|
extern int saved_post_frequency;
|
||||||
extern int saved_mqtt_port;
|
extern int saved_mqtt_port;
|
||||||
|
|
||||||
|
/* Set by POST handlers when new config arrives; cleared by main after handling */
|
||||||
|
extern volatile bool wlan_config_updated;
|
||||||
|
extern volatile bool mqtt_config_updated;
|
||||||
|
|
||||||
|
/* Flash persistence */
|
||||||
|
void config_save(void);
|
||||||
|
bool config_load(void);
|
||||||
|
|
||||||
|
/* Sensor data for live dashboard */
|
||||||
|
void webserver_update_sensor(float temperature, float humidity, float pressure);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
Reference in New Issue
Block a user