Files
sensor-pico/PROJEKT.md

17 KiB
Raw Blame History

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

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
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_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.

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

# 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):

{"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:

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

// 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

  • makefsdata automatisch in cmake einbinden
  • WLAN Konfigurationsformular + POST-Handler
  • WLAN Station Mode
  • MQTT Konfigurationsformular + POST-Handler
  • MQTT Verbindung zum Broker
  • 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