Files
sensor-pico/PROJEKT.md

435 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Raspberry Pi Pico W Sensor Projekt
## Regeln
- Claude erklärt Konzepte und gibt Hinweise, aber **kein fertiger Code**
- Ich schreibe den Code selbst
- Bei Fehlern analysieren wir gemeinsam
- Ausnahme: Setup und Boilerplate darf vorgegeben werden
---
## Hardware
| Komponente | Protokoll | Misst |
|---|---|---|
| Raspberry Pi Pico W | | Mikrocontroller mit WLAN |
| BME280 (GY-BME280) | I2C (Adresse 0x76) | Temperatur + Luftdruck + Luftfeuchtigkeit |
> AM2302 und DS18B20 werden nicht verwendet BME280 liefert alle benötigten Messwerte.
---
## Entwicklungsumgebung
### System
- CachyOS (Arch-basiert)
- VS Code
### Installierte Tools
```bash
sudo pacman -S arm-none-eabi-gcc arm-none-eabi-newlib cmake ninja git python
sudo pacman -S minicom
```
### VS Code Extensions
- C/C++ (ms-vscode.cpptools)
- CMake Tools (ms-vscode.cmake-tools)
- CMake (twxs.cmake)
- Serial Monitor (ms-vscode.vscode-serial-monitor)
- Raspberry Pi Pico (raspberry-pi.raspberry-pi-pico)
### Serieller Monitor
- Port: `/dev/ttyACM0`
- Baud Rate: `115200`
- Gruppe für Zugriff: `uucp`
```bash
sudo usermod -aG uucp $USER
# Dann neu einloggen!
```
---
## Projekt Struktur
Das Repo heißt `sensor-pico` und ist gleichzeitig der Root keine extra Ebene darüber.
```
sensor-pico/ ← Git Repo Root
├── pico-sdk/ ← Git Submodule (Raspberry Pi Pico SDK)
├── lib/
│ ├── bme280/ ← Git Submodule (lafftale1999/bme280_driver)
│ └── dhcp_server/ ← DHCP Server (von pico-examples, kein Submodule)
├── src/
│ ├── main.cpp ← Hauptprogramm (WiFi + MQTT Verbindungslogik)
│ ├── webserver.c ← lwIP POST-Handler + Formular-Parsing
│ ├── webserver.h ← Exports: saved_ssid, saved_mqtt_* etc.
│ └── fsdata.c ← Generiert von makefsdata, nicht im Git!
├── fs/
│ ├── index.html ← Navigations-Hub (Links zu den Unterseiten)
│ ├── wlan_config.html ← WLAN SSID + Passwort Formular (POST /config)
│ ├── mqtt_config.html ← MQTT + Frequenz Formular (POST /mqtt-config)
│ └── live_stats.html ← (geplant) Live-Sensordaten
├── build/ ← Von cmake generiert, nicht im Git
├── CMakeLists.txt
├── lwipopts.h ← lwIP Konfiguration
└── prepend_include.cmake ← Hilfsskript für makefsdata-Build-Schritt
```
### Git Submodule
```
[submodule "lib/bme280"]
path = lib/bme280
url = git@github.com:lafftale1999/bme280_driver.git
[submodule "pico-sdk"]
path = pico-sdk
url = https://github.com/raspberrypi/pico-sdk.git
```
### .gitignore
```
build/
.vscode/
CMakeCache.txt
CMakeFiles/
cmake_install.cmake
Makefile
*.uf2
*.elf
*.bin
*.hex
*.map
*.dis
__pycache__/
*.pyc
src/fsdata.c
```
> `src/fsdata.c` wird beim Build automatisch generiert und gehört nicht ins Repo.
---
## CMakeLists.txt (aktueller Stand)
```cmake
cmake_minimum_required(VERSION 3.13)
set(PICO_BOARD pico_w)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(PICO_SDK_PATH ${CMAKE_SOURCE_DIR}/pico-sdk)
include(${PICO_SDK_PATH}/external/pico_sdk_import.cmake)
project(sensor-pico C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
pico_sdk_init()
add_subdirectory(lib/bme280 bme280_build)
add_subdirectory(lib/dhcp_server dhcp_server_build)
# makefsdata: fs/ → src/fsdata.c (wird von fs.c via #include eingebunden)
add_custom_command(
OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/src/fsdata.c
COMMAND perl ${PICO_SDK_PATH}/lib/lwip/src/apps/http/makefsdata/makefsdata
COMMAND ${CMAKE_COMMAND} -E rename fsdata.c src/fsdata.c
COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_SOURCE_DIR}/prepend_include.cmake
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
DEPENDS ${CMAKE_SOURCE_DIR}/fs/index.html
${CMAKE_SOURCE_DIR}/fs/wlan_config.html
${CMAKE_SOURCE_DIR}/fs/mqtt_config.html
)
# fsdata.c wird NICHT in add_executable gelistet fs.c inkludiert sie via
# #include HTTPD_FSDATA_FILE (definiert in lwipopts.h als "src/fsdata.c")
add_executable(sensor-pico
src/main.cpp
src/webserver.c
)
pico_enable_stdio_usb(sensor-pico 1)
pico_enable_stdio_uart(sensor-pico 0)
target_include_directories(sensor-pico PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(sensor-pico
pico_stdlib
pico_cyw43_arch_lwip_threadsafe_background
pico_lwip
pico_lwip_http
pico_lwip_mqtt
bme280
dhcp_server
)
pico_add_extra_outputs(sensor-pico)
```
---
## prepend_include.cmake
Dieses Script wird von CMake als Build-Schritt ausgeführt und fügt den fehlenden `#include` an den Anfang der generierten `fsdata.c` ein. Nötig weil das alte Perl-Script von lwIP keinen Header einfügt.
```cmake
file(READ "src/fsdata.c" CONTENT)
file(WRITE "src/fsdata.c" "#include \"lwip/apps/fs.h\"\n${CONTENT}")
```
> Warum nicht direkt Perl dafür? CMake-Scripts haben kein Shell-Quoting-Problem `file(READ/WRITE)` arbeitet direkt mit dem Dateisystem, ohne Shell-Umweg.
---
## Build & Flash Workflow
```bash
# Erstes Setup (nur einmal)
mkdir build
# Bauen
cd build
cmake .. -DPICO_BOARD=pico_w # PICO_BOARD muss explizit angegeben werden!
make -j$(nproc)
# Was passiert beim Build automatisch:
# 1. makefsdata liest fs/index.html
# 2. Generiert fsdata.c mit HTML-Inhalt als Byte-Array
# 3. Verschiebt fsdata.c nach src/fsdata.c
# 4. prepend_include.cmake fügt #include "lwip/apps/fs.h" ein
# 5. fsdata.c wird mitkompiliert
# Flashen
# 1. BOOTSEL gedrückt halten
# 2. USB einstecken
# 3. Taste loslassen
# 4. RPI-RP2 erscheint als Laufwerk
cp sensor-pico.uf2 /run/media/$USER/RPI-RP2/
```
---
## Verkabelung
**I2C (BME280):**
```
VCC → 3.3V (Pin 36)
GND → GND
SDA → GPIO 14 (Pin 19) ← Vorgabe der bme280-Bibliothek (i2c1)
SCL → GPIO 15 (Pin 20) ← Vorgabe der bme280-Bibliothek (i2c1)
```
---
## BME280 Messwerte
Die Bibliothek gibt skalierte Integer zurück kein Floating Point, üblich auf Embedded-Systemen wegen Speicher und Geschwindigkeit.
| Wert | Skalierung | Beispiel | Umrechnung |
|---|---|---|---|
| Temperatur | × 100 | 2494 | `/ 100` → 24.94°C |
| Luftfeuchtigkeit | × 1024 | 30759 | `/ 1024` → 30.0% |
| Luftdruck | × 256 (Pa) | 26074767 | `/ 256 / 100` → 1018.54 hPa |
JSON-Ausgabe per `bme280_get_json(handle)`:
```json
{"temperature":2494,"humidity":30759,"pressure":26074767}
```
Die Umrechnung kann auf dem Empfänger (z.B. Home Assistant) gemacht werden der Pico sendet die Rohwerte.
Lesbare Ausgabe per printf möglich:
```c
printf("Temp: %d.%02d C\n", temperature / 100, temperature % 100);
```
---
## Netzwerk / Webserver
### Architektur
Der Pico hat zwei Betriebsmodi:
**Modus 1 Erststart / Konfiguration (Access Point)**
- Pico öffnet einen eigenen Access Point (`SensorAP`, WPA2)
- DHCP-Server läuft Clients bekommen automatisch eine IP (`192.168.4.x`)
- Pico ist erreichbar unter `192.168.4.1`
- Weboberfläche zeigt Konfigurationsformular
**Modus 2 Produktionsbetrieb (Station Mode)**
- Pico verbindet sich mit bestehendem WLAN (gespeicherte SSID + Passwort)
- Publisht Sensordaten per MQTT an Broker
- Webserver läuft weiterhin: Live-Daten + Einstellungen änderbar
### Aktueller Stand
- ✅ Access Point (`SensorAP`, WPA2) öffnet sich
- ✅ DHCP-Server läuft Clients bekommen IP
- ✅ Pico erreichbar unter `192.168.4.1`
- ✅ lwIP httpd läuft Seiten werden ausgeliefert
-`makefsdata` automatisch im Build-Prozess eingebunden
- ✅ Alle HTML-Seiten aus `fs/` werden als Byte-Array in Firmware eingebaut
- ✅ WLAN-Konfiguration über Captive Portal (POST Handler)
- ✅ Pico verbindet sich mit bestehendem WLAN (Station Mode)
- ✅ MQTT-Konfiguration über Webformular (Adresse, Port, User, Passwort, Frequenzen)
- ✅ MQTT-Verbindung zum Broker steht
- ✅ BME280 Sensor liest Messwerte aus und gibt JSON per printf aus
- ⬜ Sensordaten per MQTT publishen
- ⬜ Live-Daten Seite (`live_stats.html`)
### Programm-Ablauf (main.cpp)
```
1. cyw43_arch_init() + httpd_init()
2. WiFi-Schleife:
└─ ap_init() → warte auf SSID+Passwort → verbinde mit WLAN
└─ bei Erfolg: weiter; bei Fehler: neu versuchen
3. MQTT-Config-Schleife:
└─ warte bis saved_mqtt_address gesetzt ist (Formular abgeschickt)
└─ connect_to_mqtt() → bei Fehler: Config zurücksetzen + neu warten
4. Sensor-Loop (TODO):
└─ BME280 auslesen → per MQTT publishen
```
### Geplante Anforderungen
1. ✅ Beim Erststart: Access Point für Erstkonfiguration
2. ✅ Weboberfläche zum Einstellen von:
- ✅ WLAN SSID + Passwort
- ✅ MQTT Broker (Adresse, Port, User, Passwort)
- ✅ Messfrequenz + Pushfrequenz
3. Im Produktionsbetrieb: Live-Daten + Einstellungen änderbar
### makefsdata wie es funktioniert
`makefsdata` ist ein Perl-Script im lwIP-Quellcode:
```
pico-sdk/lib/lwip/src/apps/http/makefsdata/makefsdata
```
Es liest alle Dateien aus dem `fs/`-Ordner und erzeugt daraus eine `fsdata.c` mit dem Dateiinhalt als statische Byte-Arrays. lwIP httpd schaut zur Laufzeit in dieser Datei nach statt auf einem echten Dateisystem.
**Problem:** Das Script generiert `fsdata.c` ohne den nötigen `#include "lwip/apps/fs.h"` der Compiler kennt dann weder `struct fsdata_file` noch `FS_FILE_FLAGS_*`.
**Lösung:** `prepend_include.cmake` fügt den Include als CMake-Build-Schritt nach der Generierung ein.
### lwipopts.h
```c
// fsdata.c wird von lwIP's fs.c via #include eingebunden Pfad relativ zum Include-Root
#define HTTPD_FSDATA_FILE "src/fsdata.c"
#define NO_SYS 1
#define LWIP_SOCKET 0
#define LWIP_NETCONN 0
#define LWIP_IPV4 1
#define LWIP_TCP 1
#define LWIP_UDP 1
#define LWIP_DHCP 1
#define LWIP_DNS 1
#define LWIP_RAW 1
#define LWIP_ARP 1
#define LWIP_ETHERNET 1
#define LWIP_ICMP 1
#define LWIP_NETIF_HOSTNAME 1
#define LWIP_NETIF_STATUS_CALLBACK 1
#define MEM_ALIGNMENT 4
#define MEM_SIZE 4000
#define MEMP_NUM_TCP_SEG 32
#define PBUF_POOL_SIZE 24
#define TCP_MSS 1460
#define TCP_WND (8 * TCP_MSS)
#define TCP_SND_BUF (8 * TCP_MSS)
#define LWIP_HTTPD 1
#define LWIP_HTTPD_CGI 1
#define LWIP_HTTPD_SSI 1
#define LWIP_HTTPD_SUPPORT_POST 1 // POST-Body Callbacks aktivieren
```
---
## Nächste Schritte
- [x] `makefsdata` automatisch in cmake einbinden
- [x] WLAN Konfigurationsformular + POST-Handler
- [x] WLAN Station Mode
- [x] MQTT Konfigurationsformular + POST-Handler
- [x] MQTT Verbindung zum Broker
- [x] BME280 Sensor auslesen
- [ ] BME280 Sensordaten per MQTT publishen
- [ ] Live-Daten Seite (`live_stats.html`)
- [ ] Einstellungen im Flash speichern (Neustart ohne Neukonfiguration)
---
## Gelerntes
### Konzepte
- **I2C** Bus-Protokoll, alle Geräte an 2 Drähten (SDA + SCL), jedes Gerät hat eine Adresse
- **Hexadezimal** `0x` Prefix, 16 Ziffern (0-9, A-F)
- **ACK/NACK** Antwort/keine Antwort bei I2C Kommunikation
- **Handle** "Ticket" auf ein intern verwaltetes Objekt (Garderobe-Analogie)
- **Pull-up Widerstand** nötig bei I2C und OneWire
- **printf** Formatstring mit Platzhaltern (`%d`, `%s`, `%02X`), Zeilenumbruch mit `\n`
- **&variable** Adressoperator, gibt Speicheradresse zurück
- **uint8_t** 8-bit unsigned integer, genau 1 Byte
- **void** Rückgabetyp wenn Funktion nichts zurückgibt
- **Embedded** kein `std::cout`, `printf` bevorzugen wegen Speicher
- **IP-Adresse** eindeutige Adresse im Netzwerk, 4 Zahlen (0-255)
- **Netzmaske** definiert welcher Teil der IP das Netzwerk ist (`255.255.255.0`)
- **DHCP** verteilt automatisch IP-Adressen an Geräte im Netzwerk
- **lwIP** schlanker TCP/IP Stack für Embedded-Systeme
- **fsdata** HTML-Dateien werden beim Build als C-Bytes in die Firmware eingebaut
- **makefsdata** Perl-Script das `fs/`-Ordner in `fsdata.c` Byte-Array konvertiert
- **extern "C"** verhindert C++ Name Mangling bei C-Bibliotheken
- **Name Mangling** C++ kodiert Funktionsnamen mit Typinfo im Symbol (für Overloading); C nicht das verursacht Linker-Fehler wenn man C-Bibliotheken aus C++ nutzt ohne `extern "C"`
- **-j$(nproc)** paralleles Bauen mit allen CPU-Kernen
- **add_custom_command** CMake-Befehl um externe Programme als Build-Schritt auszuführen; mit `OUTPUT` weiß CMake welche Datei erzeugt wird und wann neu gebaut werden muss
- **${CMAKE_COMMAND} -E** plattformunabhängige CMake-Dateibefehle (rename, copy, remove) ohne Shell-Abhängigkeit
- **${CMAKE_COMMAND} -P** führt ein CMake-Script direkt aus, ohne Shell-Umweg und ohne Quoting-Probleme
- **GET vs POST** GET-Parameter landen in der URL (sichtbar, in Logs), POST-Daten im Body für Passwörter immer POST
- **Buffer** Array als temporärer Zwischenspeicher; in C immer manuell null-terminieren (`buf[len] = '\0'`)
- **pbuf** lwIPs internes Paket-Buffer-Format; Inhalt mit `pbuf_copy_partial()` extrahieren, danach `pbuf_free()` aufrufen
- **strstr** sucht Substring in String, gibt Zeiger auf Fundstelle zurück oder NULL
- **strchr** sucht einzelnes Zeichen in String, gibt Zeiger darauf zurück
- **Zeigerarithmetik** zwei Zeiger auf denselben String subtrahieren gibt die Länge zwischen ihnen
- **strlen** Länge eines C-Strings ohne Null-Terminator
- **atoi** wandelt C-String (`"30"`) in Integer (`30`) um; aus `<stdlib.h>`
- **memset** setzt alle Bytes eines Arrays auf einen Wert; `memset(arr, 0, sizeof(arr))` zum Nullen
- **static (lokale Variable)** lebt für die gesamte Programmlaufzeit, wird nicht bei jedem Aufruf neu initialisiert
- **Asynchrone Callbacks** Funktion wird nicht direkt aufgerufen sondern von lwIP registriert und später automatisch aufgerufen (z.B. bei MQTT-Verbindung)
- **void *arg** generischer Kontext-Zeiger in C-Callbacks; übergib `&deine_variable`, caste in Callback zurück mit `static_cast<typ*>(arg)`
- **static_cast<>** C++ Cast-Operator; typsicherer als C-Style-Cast `(typ)`
- **Ternary Operator** `bedingung ? wert_true : wert_false`; Kurzform für einfache if/else-Zuweisungen
- **mqtt_client_t** lwIP MQTT Client-Objekt; erstellt mit `mqtt_client_new()`
- **mqtt_connect_client_info_t** Struct mit Client-ID, Username, Passwort für MQTT-Verbindung
- **MQTT Verbindungsflow** `mqtt_client_connect()` kehrt sofort zurück; Ergebnis kommt asynchron im Callback
### CMake-Konzepte
- **add_subdirectory** bindet einen Unterordner mit eigenem CMakeLists.txt ein; zweiter Parameter ist der Build-Unterordner
- **target_include_directories** sagt dem Compiler wo er Header-Dateien suchen soll; `PRIVATE` = nur für dieses Target
- **target_link_libraries** verknüpft Bibliotheken mit dem Executable
- **add_custom_command OUTPUT** definiert wie eine generierte Datei erzeugt wird; CMake führt den Befehl aus wenn die Datei fehlt oder Abhängigkeiten neuer sind
- **DEPENDS in add_custom_command** wenn diese Dateien sich ändern, wird der Befehl beim nächsten Build neu ausgeführt
### Pico W Besonderheiten
- Interne LED hängt am WLAN-Chip (CYW43), nicht an GPIO 25
- Braucht `pico_cyw43_arch_none` (ohne Netzwerk) oder `pico_cyw43_arch_lwip_threadsafe_background` (mit lwIP)
- `PICO_BOARD=pico_w` muss **vor** `pico_sdk_import.cmake` gesetzt werden
- `MEM_LIBC_MALLOC` ist inkompatibel mit `threadsafe_background`
- `lwipopts.h` muss selbst erstellt werden lwIP hat keine Defaults
- lwIP httpd CGI erwartet C-Funktionen in `.cpp`-Dateien mit `extern "C"` wrappen
### Bekannte Fallstricke
- `makefsdata` generiert `fsdata.c` ohne `#include "lwip/apps/fs.h"` → Compiler kennt `struct fsdata_file` nicht → `prepend_include.cmake` als Workaround
- Shell-Quoting in CMake `COMMAND` ist fehleranfällig bei komplexen Perl-Ausdrücken → CMake-Scripts (`-P`) verwenden statt Shell-Befehle
- `add_custom_command OUTPUT` mit relativem Pfad legt die Datei im Build-Verzeichnis ab, nicht im Source-Verzeichnis → immer absoluten Pfad mit `${CMAKE_CURRENT_SOURCE_DIR}` angeben
- `pico_lwip_http` ist ein INTERFACE-Target `target_include_directories` mit `PRIVATE` schlägt fehl; stattdessen `HTTPD_FSDATA_FILE` in `lwipopts.h` überschreiben
- lwIP `fs.c` inkludiert `fsdata.c` direkt via `#include HTTPD_FSDATA_FILE` ohne Override wird die Default-Seite aus dem pico-sdk verwendet statt der eigenen
- `strcmp` gibt `0` zurück wenn Strings gleich sind, nicht `true` `== true` prüft auf Verschiedenheit
- lwIP CGI-Handler parst nur GET-Parameter aus der URL, nicht POST-Body → für POST-Formulare `LWIP_HTTPD_SUPPORT_POST` + die drei Callbacks (`httpd_post_begin`, `httpd_post_receive_data`, `httpd_post_finished`) verwenden
- `dhcp_server_t` in `ap_init()` muss `static` sein sonst wird der Stack-Speicher nach Funktionsrückkehr freigegeben, der DHCP-Server läuft aber weiter und greift auf ungültigen Speicher zu
- `strncpy` fügt **kein** Null-Byte hinzu wenn exakte Länge übergeben wird → immer `dest[len] = '\0'` danach setzen