Compare commits

2 Commits
main ... ai

Author SHA1 Message Date
173bf74967 Final Update 2026-03-27 22:32:25 +01:00
e8f90661d3 AI update 2026-03-27 22:27:32 +01:00
9 changed files with 935 additions and 224 deletions

View File

@@ -27,6 +27,7 @@ add_custom_command(
DEPENDS ${CMAKE_SOURCE_DIR}/fs/index.html
DEPENDS ${CMAKE_SOURCE_DIR}/fs/mqtt_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_mqtt
pico_lwip_sntp
hardware_flash
hardware_sync
)
# Erzeugt .uf2 Datei zum Flashen

View File

@@ -1,16 +1,290 @@
<!DOCTYPE html>
<html lang="en">
<html lang="de">
<head>
<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>
<body>
<h1>Sensor Pico</h1>
<br>
<a href="live_stats.html">Live Daten</a>
<br>
<a href="wlan_config.html">WLAN Config</a>
<br>
<a href="mqtt_config.html">MQTT und Messungs Config</a>
<div class="topbar">
<div>
<div class="topbar-title">Sensor Pico</div>
<div class="topbar-sub">BME280</div>
</div>
<div class="settings-btn" onclick="openSettings()">&#9881;</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">&deg;C</span>
</div>
</div>
<span class="metric-icon">&#127777;</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">&#128167;</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">&#128262;</span>
</div>
</div>
<div class="statusbar">
<div class="dot offline" id="dot"></div>
<span class="status-text" id="status">Verbinde&hellip;</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">&#8250;</span>
</a>
<a href="mqtt_config.html" class="sheet-link">
MQTT &amp; Messung
<span class="sheet-arrow">&#8250;</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>
</html>

View File

@@ -0,0 +1,4 @@
<!DOCTYPE html>
<html>
<head><meta http-equiv="refresh" content="0; url=/"></head>
</html>

View File

@@ -1,32 +1,189 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Konfiguration</title>
</head>
<body>
<h1>MQTT Konfiguration</h1>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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>
<body>
<div class="container">
<a href="/" class="back">&#8592; Zurück</a>
<div class="card">
<div class="card-header">
<div class="card-icon">&#9881;</div>
<div>
<h1>MQTT &amp; Messung</h1>
<p class="card-subtitle">Broker und Intervalle konfigurieren</p>
</div>
</div>
<form action="/mqtt-config" method="post">
<label>MQTT Adresse:</label>
<input type="text" name="mqtt-address">
<br>
<label>MQTT Port:</label>
<input type="number" name="mqtt-port">
<br>
<label>MQTT User:</label>
<input type="text" name="mqtt-user">
<br>
<label>MQTT Passwort:</label>
<input type="password" name="mqtt-password">
<br>
<label>Messfrequenz (ms)</label>
<input type="number" name="measure-frequency">
<br>
<label>Pushfrequenz (s)</label>
<input type="number" name="push-frequency">
<br>
<input type="submit" value="Speichern">
<p class="section-label">Broker</p>
<div class="form-row">
<div>
<div class="form-group">
<label for="mqtt-address">Adresse</label>
<input type="text" id="mqtt-address" name="mqtt-address" placeholder="192.168.1.100" autocomplete="off">
</div>
</div>
<div>
<div class="form-group">
<label for="mqtt-port">Port</label>
<input type="number" id="mqtt-port" name="mqtt-port" placeholder="1883" min="1" max="65535">
</div>
</div>
</div>
<div class="form-group">
<label for="mqtt-user">Benutzer</label>
<input type="text" id="mqtt-user" name="mqtt-user" placeholder="Optional" autocomplete="off">
</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>
</body>
</div>
</div>
</body>
</html>

View File

@@ -1,20 +1,141 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Konfiguration</title>
</head>
<body>
<h1>WLAN Einstellungen</h1>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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>
<body>
<div class="container">
<a href="/" class="back">&#8592; Zurück</a>
<div class="card">
<div class="card-header">
<div class="card-icon">&#128246;</div>
<div>
<h1>WLAN</h1>
<p class="card-subtitle">Netzwerkverbindung konfigurieren</p>
</div>
</div>
<form action="/wlan-config" method="post">
<label>SSID:</label>
<input type="text" name="ssid">
<br>
<label>Passwort:</label>
<input type="password" name="password">
<br>
<input type="submit" value="Speichern">
<div class="form-group">
<label for="ssid">SSID</label>
<input type="text" id="ssid" name="ssid" placeholder="Netzwerkname" autocomplete="off">
</div>
<div class="form-group">
<label for="password">Passwort</label>
<input type="password" id="password" name="password" placeholder="&#8226;&#8226;&#8226;&#8226;&#8226;&#8226;&#8226;&#8226;" autocomplete="off">
</div>
<hr class="divider">
<button type="submit">Speichern &amp; Verbinden</button>
</form>
</body>
</div>
</div>
</body>
</html>

View File

@@ -23,6 +23,7 @@
#define LWIP_HTTPD_CGI 1
#define LWIP_HTTPD_SSI 1
#define LWIP_HTTPD_SUPPORT_POST 1
#define LWIP_HTTPD_CUSTOM_FILES 1
#define LWIP_NETIF_HOSTNAME 1
#define LWIP_NETIF_STATUS_CALLBACK 1

View File

@@ -15,28 +15,16 @@
static dhcp_server_t dhcp_server{};
void ap_init() {
static void ap_init() {
cyw43_arch_enable_ap_mode("SensorAP", "passwort123", CYW43_AUTH_WPA2_AES_PSK);
ip_addr_t gw{};
ip_addr_t mask{};
ip_addr_t gw{}, mask{};
IP4_ADDR(&gw, 192, 168, 4, 1);
IP4_ADDR(&mask, 255, 255, 255, 0);
dhcp_server_deinit(&dhcp_server);
dhcp_server_init(&dhcp_server, &gw, &mask);
}
void reset_mqtt_config() {
memset(saved_mqtt_address, 0, sizeof(saved_mqtt_address));
memset(saved_mqtt_user, 0, sizeof(saved_mqtt_user));
memset(saved_mqtt_password, 0, sizeof(saved_mqtt_password));
saved_measure_frequency = 0;
saved_post_frequency = 0;
}
int connect_to_wifi() {
int ret{};
static int connect_via_ap() {
memset(saved_ssid, 0, sizeof(saved_ssid));
memset(saved_password, 0, sizeof(saved_password));
ap_init();
@@ -49,33 +37,37 @@ int connect_to_wifi() {
cyw43_arch_disable_sta_mode();
sleep_ms(500);
cyw43_arch_enable_sta_mode();
ret = cyw43_arch_wifi_connect_timeout_ms(saved_ssid, saved_password,
return cyw43_arch_wifi_connect_timeout_ms(saved_ssid, saved_password,
CYW43_AUTH_WPA2_MIXED_PSK, 30000);
if (ret == 0) {
return 0;
}
return -1;
}
void mqtt_cb(mqtt_client_t *client, void *arg,
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);
}
static void mqtt_cb(mqtt_client_t *client, void *arg,
mqtt_connection_status_t status) {
(void)client;
int *mqtt_status{static_cast<int *>(arg)};
*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};
ip_addr_t broker_ip;
static mqtt_client_t *client{};
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;
}
if (!client) {
if (!client)
client = mqtt_client_new();
}
info.client_id = "sensor-pico";
info.client_user = saved_mqtt_user;
info.client_pass = saved_mqtt_password;
@@ -87,70 +79,59 @@ mqtt_client_t *connect_to_mqtt() {
cyw43_arch_poll();
sleep_ms(100);
}
if (mqtt_status == 0) {
return client;
}
return nullptr;
return (mqtt_status == 0) ? client : nullptr;
}
BME280_READING_INTERVALS_MS convert_Interval(int interval) {
if (interval < 1) {
return INTERVAL_0_5MS;
} else if (interval < 20 && interval > 1) {
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;
}
static void publish_cb(void *arg, err_t err) {
(void)arg;
(void)err;
printf("Publish successful!\n");
}
void publish_cb(void *arg, err_t err) { printf("Publish succesfull!\n"); }
void publish_mqtt(mqtt_client_t *client, const char *payload) {
static void publish_mqtt(mqtt_client_t *client, const char *payload) {
uint payload_len{strlen(payload)};
mqtt_publish(client, "tele/sensor-pico/SENSOR", payload, payload_len, 1, 1,
publish_cb, nullptr);
}
bool parse_json(const char *raw, float &temperature, float &humidity,
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) {
if (!raw)
return false;
const char *t_ptr = std::strstr(raw, "\"temperature\":");
const char *h_ptr = std::strstr(raw, "\"humidity\":");
const char *p_ptr = std::strstr(raw, "\"pressure\":");
if (!t_ptr || !h_ptr || !p_ptr)
return false;
long t_raw = std::strtol(t_ptr + 14, nullptr, 10);
long h_raw = std::strtol(h_ptr + 11, nullptr, 10);
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;
temperature = std::strtol(t_ptr + 14, nullptr, 10) / 100.0f;
humidity = std::strtol(h_ptr + 11, nullptr, 10) / 1024.0f;
pressure = std::strtol(p_ptr + 11, nullptr, 10) / 25600.0f;
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) {
bme280_read_data(handle);
const char *raw = bme280_get_json(handle);
if (!raw) {
snprintf(buffer, size, "{\"error\":\"no data\"}");
return;
@@ -158,37 +139,29 @@ void build_sensor_payload(char *buffer, size_t size, bme280_handle_t handle,
float temperature, humidity, pressure;
parse_json(raw, temperature, humidity, pressure);
webserver_update_sensor(temperature, humidity, pressure);
time_t now;
time(&now);
struct tm *timeinfo = localtime(&now);
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,
"{"
"\"Time\":\"%s\","
"\"%s\":{"
"\"Temperature\":%.2f,"
"\"Humidity\":%.2f,"
"\"Pressure\":%.2f"
"},"
"\"TempUnit\":\"C\""
"}",
"{\"Time\":\"%s\",\"%s\":{"
"\"Temperature\":%.2f,\"Humidity\":%.2f,\"Pressure\":%.2f"
"},\"TempUnit\":\"C\"}",
time_str, sensor_name, temperature, humidity, pressure);
}
bool is_time_synced() {
time_t now = time(NULL);
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();
if (sntp_enabled()) {
if (sntp_enabled())
sntp_stop();
}
sntp_setoperatingmode(SNTP_OPMODE_POLL);
#if SNTP_SERVER_DNS
sntp_setservername(0, "de.pool.ntp.org");
@@ -198,14 +171,6 @@ void start_sntp() {
}
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();
setenv("TZ", "CET-1CEST,M3.5.0,M10.5.0", 1);
tzset();
@@ -213,55 +178,107 @@ int main() {
cyw43_arch_init();
httpd_init();
while (wifi_status != 0) {
wifi_status = (connect_to_wifi() == 0) ? 0 : 1;
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());
bool has_saved_config = config_load();
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());
while (true) {
while (saved_mqtt_address[0] == '\0') {
cyw43_arch_poll();
sleep_ms(200);
}
client = connect_to_mqtt();
if (client == nullptr) {
printf("Mqtt Status: %d\n", mqtt_ret);
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());
mqtt_client_t *client{};
while (true) {
if ((to_ms_since_boot(get_absolute_time()) - last_publish) >=
saved_post_frequency * 1000) {
client = connect_to_mqtt();
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");
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());
while (true) {
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");
}
mqtt_config_updated = true;
}
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");
}
}
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()) {
build_sensor_payload(payload, sizeof(payload), handle, "BME280");
publish_mqtt(client, payload);
printf("Payload: %s\n", payload);
last_publish = to_ms_since_boot(get_absolute_time());
last_publish = now_ms;
} else {
printf("Waiting for SNTP sync (Current year: 1970)...\n");
uint32_t now_ms = to_ms_since_boot(get_absolute_time());
printf("Waiting for SNTP sync...\n");
if (now_ms - last_sntp_retry_ms > 30000) {
printf("Retrying SNTP...\n");
start_sntp();
last_sntp_retry_ms = now_ms;
}
last_publish = to_ms_since_boot(get_absolute_time()) -
(saved_post_frequency * 1000) + 5000;
last_publish = now_ms - uint32_t(saved_post_frequency) * 1000 + 5000;
}
}
cyw43_arch_poll();
sleep_ms(100);
}
}
}

View File

@@ -1,7 +1,52 @@
#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 <stdbool.h>
#include <stdint.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_password[65];
@@ -12,8 +57,84 @@ int saved_measure_frequency;
int saved_post_frequency;
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 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 *end = (delimiter == '\0') ? (post_buffer + strlen(post_buffer))
: strchr(pos, delimiter);
if (!end) {
if (!end)
end = post_buffer + strlen(post_buffer);
}
int len = end - pos;
if (len > max_len) {
if (len > max_len)
len = max_len;
}
strncpy(dest, pos, len);
dest[len] = '\0';
}
/* ── lwip httpd POST callbacks ────────────────────────────────────────────── */
err_t httpd_post_begin(void *connection, const char *uri,
const char *http_request, u16_t http_request_len,
int content_len, char *response_uri,
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) {
strncpy(response_uri, "/wlan_config.html", response_uri_len);
*post_auto_wnd = 1;
@@ -52,15 +173,16 @@ err_t httpd_post_begin(void *connection, const char *uri,
return ERR_OK;
}
if (strcmp(uri, "/mqtt-config") == 0) {
post_state = 1;
strncpy(response_uri, "/mqtt_config.html", response_uri_len);
*post_auto_wnd = 1;
post_state = 1;
return ERR_OK;
}
return ERR_VAL;
}
err_t httpd_post_receive_data(void *connection, struct pbuf *p) {
(void)connection;
post_buffer_len =
pbuf_copy_partial(p, post_buffer, sizeof(post_buffer) - 1, 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,
u16_t response_uri_len) {
(void)connection;
if (post_state == 0) {
strncpy(response_uri, "/wlan_config.html", response_uri_len);
parse_post("ssid=", '&', saved_ssid, 32);
parse_post("password=", '\0', saved_password, 64);
wlan_config_updated = true;
} else if (post_state == 1) {
char temp[16];
strncpy(response_uri, "/mqtt_config.html", response_uri_len);
parse_post("mqtt-address=", '&', saved_mqtt_address, 64);
parse_post("mqtt-user=", '&', saved_mqtt_user, 32);
parse_post("mqtt-password=", '&', saved_mqtt_password, 64);
parse_post("mqtt-port=", '&', temp, 15);
saved_mqtt_port = atoi(temp);
parse_post("measure-frequency=", '&', temp, 15);
saved_measure_frequency = atoi(temp);
parse_post("push-frequency=", '\0', temp, 15);
saved_post_frequency = atoi(temp);
mqtt_config_updated = true;
}
}

View File

@@ -1,10 +1,13 @@
#ifndef WEBSERVER_H
#define WEBSERVER_H
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Saved configuration (written by POST handlers, read by main) */
extern char saved_ssid[33];
extern char saved_password[65];
extern char saved_mqtt_address[65];
@@ -14,6 +17,17 @@ extern int saved_measure_frequency;
extern int saved_post_frequency;
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
}
#endif