# 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 `` - **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(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