Budeme měřit teplotu a chceme, aby byly výsledky měření přístupné na síti a mohli bychom si je prohlédnout v libovolném webovém prohlížeči. Popíši naprogramování jednoduchého web serveru, který poběží na Picu W s wifi kartou (na obyčejném Raspberry Pi Pico to nebude fungovat, protože se s ním neumíme připojit do sítě). Používat budeme nešifrovaný protokol HTTP na IPv4 a nebudeme vystavovat naše zařízení do Internetu, protože je těžce nezabezpečené a libovolný darebák ho může shodit.

Jaké problémy musíme vyřešit

  • připojit Pico W k nějakému WiFi AP

    • nastavit radiové parametry připojení

    • nastavit IP adresu zařízení Pico W a pokud si necháme přidělit IP pomocí DHCP, tak nějakým způsobem ji zjistit

  • naprogramovat a rozchodit webserver

    • udělat jednoduchou html stránku, zobrazující výsledky měření teploty — použijeme technologie SSI (Server Side Includes)

    • udělat ovládání zapínaní a vypínání vestavěné LED — použijeme technologii CGI (Common Gateway Interface)

Http server je naprogramován v lwIP (lightweight IP) a je součástí Pico SDK, takže se tím nemusíme zabývat, protože to je dost těžké. Ovladač wifi karty cyw43 je součástí Pico SDK, protokoly ethernet, IP, ARP, DHCP, DNS jsou součástí lwIP a nemusíme se tím zabývat, jenom je musíme umět použít, což tak těžké nebude.

Nastavení radiových parametrů

Nastavit ESSID a heslo k Wifi je nejlepší udělat přímo ve zdrojovém kódu.

// WIFI udaje pro pripojeni
const char WIFI_SSID[] = "chorche3";            (1)
const char WIFI_PASSWORD[] = "xxxxxxxxx"; (2)

int main() {

       cyw43_arch_init();
    // wifina bude jako klient
    cyw43_arch_enable_sta_mode();   (3)
    // nastavím hostname
    printf("Nastavuji hostname na %s\n", MY_HOSTNAME);
    struct netif *n = &cyw43_state.netif[CYW43_ITF_STA];
    cyw43_arch_lwip_begin();
    netif_set_hostname(n,MY_HOSTNAME);
    netif_set_up(n);
    cyw43_arch_lwip_end();

    // Zkusim se ve smycce pripojit k wifine
    while(cyw43_arch_wifi_connect_timeout_ms(WIFI_SSID, WIFI_PASSWORD, CYW43_AUTH_WPA2_AES_PSK, 30000) != 0){ (4)
        printf("Pokousim se pripojit...\n");
    }
1 ESSID
2 Heslo
3 Wifi na Picu přepneme do módu station.
4 Zkoušíme se připojit k AP.

Problém zjištění IP adresy Pica

Pokud si necháme pro Pico W přidělit IP adresu od DHCP serveru, tak ji musíme nějakým způsobem zjistit. Lze to udělat několika způsoby:

Výpis IP adresy do debugovacího výstupu

   // Zkusim se ve smycce pripojit k wifine
    while(cyw43_arch_wifi_connect_timeout_ms(WIFI_SSID, WIFI_PASSWORD, CYW43_AUTH_WPA2_AES_PSK, 30000) != 0){
        printf("Pokousim se pripojit...\n");
    }
    // Jsem pripojen
    printf("Pripojen k %s: %s  IP adresa: %s\n", WIFI_SSID, MY_HOSTNAME, ip4addr_ntoa(netif_ip4_addr(netif_list)));

Na debuggovacím port se potom objeví výpis IP adresy

Connected. Use C-a C-q to exit.
Pripojen k chorche4: jirkovopico1  IP adresa: 10.0.0.38
Http server nahozen.
SSI Handler nahozen.
CGI Handler nahozen.

Výpis IP adresy na pomocný displej

Tohle je mnohem lepší, protože nemusíme mít debugovací konzoli, IP adresu máme na displeji. Použil jsem kód z předchozího projektu s OLED displejem Displej je malinko jiný, ale funguje úplně stejně jako ten v předchozím projektu. Drobná změna je v tom, že má 64 řádků a proto je potřeba upravit #define DISPLAY_HEIGHT 64.

#define DISPLAY_WIDTH 128
#define DISPLAY_HEIGHT 64
#define I2C_ADDRESS 0x3C
#define I2C_FREQ 400000
#define SLEEPTIME 55


void setup_gpios(void) { (1)
    i2c_init(i2c_default, I2C_FREQ);
    gpio_set_function(PICO_DEFAULT_I2C_SDA_PIN, GPIO_FUNC_I2C);
    gpio_set_function(PICO_DEFAULT_I2C_SCL_PIN, GPIO_FUNC_I2C);
    gpio_pull_up(PICO_DEFAULT_I2C_SDA_PIN);
    gpio_pull_up(PICO_DEFAULT_I2C_SCL_PIN);
}


int main() {
char buf[128];
ssd1306_t disp;

    stdio_init_all();

    setup_gpios();      (2)
    disp.external_vcc = false;
    ssd1306_init(&disp, DISPLAY_WIDTH, DISPLAY_HEIGHT, I2C_ADDRESS, i2c_default);
    ssd1306_clear(&disp);

    cyw43_arch_init();
    // wifina bude jako klient
    cyw43_arch_enable_sta_mode();
    // nastavím hostname
    printf("Nastavuji hostname na %s\n", MY_HOSTNAME);
    struct netif *n = &cyw43_state.netif[CYW43_ITF_STA];
    cyw43_arch_lwip_begin();
    netif_set_hostname(n,MY_HOSTNAME);
    netif_set_up(n);
    cyw43_arch_lwip_end();
    snprintf(buf,128,"Hostname:%s", MY_HOSTNAME);
    ssd1306_draw_string_with_font(&disp, 0, 30, 1, font_spleen_8x5, buf);
    ssd1306_show(&disp);

    // Zkusim se ve smycce pripojit k wifine
    while(cyw43_arch_wifi_connect_timeout_ms(WIFI_SSID, WIFI_PASSWORD, CYW43_AUTH_WPA2_AES_PSK, 30000) != 0){
        printf("Pokousim se pripojit...\n");
    }
    // Jsem pripojen
    snprintf(buf,20,ip4addr_ntoa(netif_ip4_addr(netif_list)));  (3)
    printf("Pripojen k %s: %s  IP adresa: %s\n", WIFI_SSID, MY_HOSTNAME, buf );
    ssd1306_draw_string_with_font(&disp, 0,  0,  1, font_spleen_16x8a, buf); (4)
    ssd1306_draw_string_with_font(&disp, 0,  16, 1, font_spleen_8x5, WIFI_SSID ); (5)
    ssd1306_show(&disp); (6)
    // ....
1 Funkce pro nastavení GPIO pinů k displeji
2 Nastavení displeje
3 Uložím si IP adresu do bufferu.
4 Pošlu na displej IP adresu (horní část displeje je žlutá).
5 Pošlu na displej ESSID APčka menším fontem (spodní část displeje je modrá).
6 Aktivuji na displeji výpisy.
Pico W webserver s OLED diplejem

IMG 20240210 081125

Zjištění IP adresy z DHCP serveru

Při tomto způsobu musíme mít přístup k DHCP serveru, někdy ho máme a někdy (třeba ve škole) ne.

Výpis DHCP zápůjček na mém testovacím routeru Comtrend

zjisteni dhcp

Pico W se ve výpisu nejmenuje jirkovopico1, ale PicoW, což je defaultní jméno Pica, pokud ho nenastavíme. Buďto mám chybu v kódu, nebo si DHCP pamatuje předchozí pokusy, kdy se testovací Pico jmenovalo skutečně PicoW. Pro DHCP server je důležitá hlavně MAC adresa 28:cd:c1:0c:33:1e, kterou ovšem taky předem nevíme. Někdy může být hledání správné IP adresy Pica docela zábava.

Webserver

obrazovka webu

Web server (httpd) funguje tak, že přijímá požadavky HTTP protokolu a na základě požadavku URI vrací příslušný soubor, který se nachází ve struktuře webu. Například požadavek je /index.html, server se podívá do svého souborového systému (/ znamená kořen) a vyhledá soubor se jménem index.html a ten odešle klientovi. Pokud soubor nenalezne, tak pošle chybu, obvykle 404.

Náš server je velmi jednoduchý, nemáme k dispozici filesystém a proto bude nás httpd server odpovídat jenom na požadavky /index.shtml (zobraz úvodní stránku), /led.cgi?led=0 (vypni LED) a /led.cgi?led=1 (zapni LED). To že nemáme na Picu k dispozici filesytém obejdeme tak, že stránka index.html bude přímo v céčkovém kódu serveru httpd. Toto zakódování provádí Pythoní skript makefsdata.py, který převede html stránku /html_files/index.shtml do céčkového kódu.

Naše stránka index.shtml
<!DOCTYPE html> (1)
<html>
    <head> (2)
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> (3)
        <meta name="author" content="Mgr. Ing. Jiří Chráska">
        <title>Měření teploty a ovládání LED</title> (4)
	    <style> (5)
	      *    {margin 0; padding: 0; font-size: 1em; font-family: 'Segoe UI', Tahoma, sans-serif;}
	      body {background-color: #dcdcdc;}
	      h1   {color: #00008b ; font-family: verdana; font-size: 120%;}
	      h2   {color: #00008b; font-family: verdana; font-size: 100%;}
	      button {margin: 5px; padding: 10px;}
	    </style>
    </head>
    <body> <h1>Jednoduchý webserver Pico W</h1> (6)
        <br>
        <h2>Technologie SSI:</h2>
        <p>Napětí: <!--#volt--> V</p>
        <p>Teplota: <!--#temp--> °C</p>
        <p>LED je: <!--#led--></p>
        <br>
        <h2>Technologie CGI:</h2>
        <div><a href="/led.cgi?led=1"><button>Zapnout LED</button></a></div>
        <div><a href="/led.cgi?led=0"><button>Vypnout LED</button></a></div>
        <br>
        <br>
        <div><a href="/index.shtml"><button>Aktualizovat měření</button</a></div>
   </body>
</html>
1 Typ dokumentu
2 Hlavička dokumentu (je ukončena </head>).
3 Kódování dokumentu UTF-8. To aby internetový prohlížeč věděl, jak má zobrazovat písmena.
4 Titulek dokumentu, prohlížeč ho zobrazuje v horní liště.
5 Kaskádový styl, informace pro prohlížeč, jak má dokument zobrazit.
6 Vlastní obsah dokumentu (je ukončeno </body>)

To by bylo ale málo, takto lze distribuovat pouze statické html stránky. Do stránky index.html potřebujeme nějak propašovat naměřené hodnoty teploty a napětí a k tomu použijeme technologii Server Side Includes.

Jak funguje technologie Server Side Include

Server Side Includes (zkláceně SSI — vkládání na straně serveru), funguje tak, že httpd server se dívá do html stránky před jejím odesláním klientovi a hledá vzory typu <!--#proměnná-->. Pokud nalezne, tak místo tohoto vzoru <!--#proměnná--> do stránky vloží hodnotu, která se v této proměnné nachází. Pokud nastane nějaká chyba, tak se nic neděje, protože internetový prohlížeč interpretuje html značku <!--blablabla--> jako komentář a nezobrazuje ji.

        <h2>Technologie SSI:</h2>
        <p>Napětí: <!--#volt--> V</p>  (1)
        <p>Teplota: <!--#temp--> °C</p> (2)
        <p>LED je: <!--#led--></p> (3)
1 Na tomto řádku najde server proměnnou se jménem volt a místo toho vloží třeba 0.72461.
2 Na tomto řádku najde server proměnnou se jménem temp a místo toho vloží třeba 22.865.
3 Na tomto řádku najde server proměnnou se jménem led a místo toho vloží zapnuto nebo vypnuto.
Výsledek odeslaný klientovi
        <h2>Technologie SSI:</h2>
        <p>Napětí: 0.72461 V</p>
        <p>Teplota: 22.865 °C</p>
        <p>LED je: zapnuto</p>

Vložení hodnoty do proměnné zajistí server tak, že zavolá ovladač (handler), předá mu jméno proměnné a práce handleru je odpovědět serveru hodnotou.

Nastavení handleru a předání jeho funkce httpd serveru
// SSI tags - položky, které umí handler zpracovat
const char * ssi_tags[] = {"volt","temp","led"};

// Inicializace SSI handleru
void ssi_init() {
  // Inicializace analogově digitálního převodníku.  Budeme ho potřebovat na zjištění naměřené teploty.
  adc_init();
  adc_set_temp_sensor_enabled(true);
  adc_select_input(4);
  // web serveru předáme adresu funkce handleru ssi_handler,
  // seznam položek v poli, které handler umí -- ssi_tags
  // a v třetím parametru velikost pole ssi_tags
  http_set_ssi_handler(ssi_handler, ssi_tags, LWIP_ARRAYSIZE(ssi_tags));
}
Implementace handleru
u16_t ssi_handler(int iIndex, char *pcInsert, int iInsertLen) { (1)
  size_t printed;
  switch (iIndex) {
  case 0: // volty  (2)
    {
      const float voltage = adc_read() * REF_VOLT / (1 << 12);
      printed = snprintf(pcInsert, iInsertLen, "%f", voltage);
    }
    break;
  case 1: // teplota (3)
    {
    const float voltage = adc_read() * REF_VOLT / (1 << 12);
    const float tempC = 27.0f - (voltage - 0.706f) / 0.001721f;
    printed = snprintf(pcInsert, iInsertLen, "%f", tempC); (5)
    }
    break;
  case 2: // led (4)
    {
      bool led_status = cyw43_arch_gpio_get(CYW43_WL_GPIO_LED_PIN);
      if(led_status == true){
        printed = snprintf(pcInsert, iInsertLen, "zapnuto");
      }
      else{
        printed = snprintf(pcInsert, iInsertLen, "vypnuto");
      }
    }
    break;
  default:
    printed = 0;
    break;
  }

  return (u16_t)printed; (6)
}
1 Httpd server předává v parametru iIndex index položky z pole ssi_tags[], podle toho co našel. *pcInsert je parametr předaný odkazem, ssi_handler do něho vloží odpověď, která ovšem nesmí být větší než iIntsertLen bajtů.
2 Tady zjisťujeme hodnotu napětí ve voltech.
3 Tady měříme teplotu.
4 Tady zjišťujeme, v jakém stavu se nachází LED.
5 Funkce snprintf() nebude kopírovat více bajtů než je hodnota iInsertLen, aby nám to nepřeteklo.
6 Návratová hodnota je skutečný počet předaných bajtů httpd serveru.

Jak funguje CGI

Common Gateway Interface (zkráceně CGI) je protokol pro propojení externích aplikací s webovým serverem. To serveru umožňuje delegovat požadavek od klienta na externí aplikaci, která dle požadavku vrátí výstup. Taková aplikace typicky zpracuje nějaký skript ve webové stránce a webovému serveru navrátí statickou stránku, která je následně poslána klientovi jako výstup jeho požadavku.

Rozhraní Common Gateway Interface bylo v prostředí internetu přítomno již od počátku 90. let a ve své době představovalo jediný způsob dynamického zpracování obsahu. Postupně vznikla efektivnější řešení (FastCGI, integrace skriptovacích jazyků jako modulu WWW serveru) a CGI bylo vytlačeno do ústraní.

CGI common gateway interface

V našrm případě bude CGI velmi jednoduché, budeme mít jednu stránku /led.cgi, která bude přijímat dva parametry len=0 a led=1. Otazník ? v URL slouží k oddělení jména stránky od parametrů. Proto pokud obdrží httpd server URL http://adresa_serveru/led.cgi?led=1 bude vědět, že má zpracovat stránku led.cgi s parametrem led=1 a tudíž rozsvítil LED, led je jméno parametru, = je oddělovač a hodnota parametru je 1.

Implementace bude opět velmi jednoduchá

#include "lwip/apps/httpd.h"
#include "pico/cyw43_arch.h"

// CGI handler which is run when a request for /led.cgi is detected
const char * cgi_led_handler(int iIndex, int iNumParams, char *pcParam[], char *pcValue[]) (3)
{
    // Check if an request for LED has been made (/led.cgi?led=x)
    if (strcmp(pcParam[0] , "led") == 0){
        // Look at the argument to check if LED is to be turned on (x=1) or off (x=0)
        if(strcmp(pcValue[0], "0") == 0)
            cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 0); (4)
        else if(strcmp(pcValue[0], "1") == 0)
            cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 1); (5)
    }

    // Send the index page back to the user
    return "/index.shtml";
}

// tCGI Struct
// Fill this with all of the CGI requests and their respective handlers
static const tCGI cgi_handlers[] = {  (1)
    {
        // Html request for "/led.cgi" triggers cgi_handler
        "/led.cgi", cgi_led_handler
    },
};

void cgi_init(void) (2)
{
    http_set_cgi_handlers(cgi_handlers, 1);
}
1 Inicializace cgi handleru, httpd serveru se předává pole se všemi cgi handlery a počet položek pole (v našem případě 1).
2 Pole pro obsluhu jednotlivých stránek, máme jenom jednu /led.cgi a obsluhuje ji cgi_led_handler.
3 Implementace cgi_led_handleru, iIndex je index do pole cgi_handlers[] (nepoužíváme ho), v iNumParams je počet parametrů, pcParam je pole se všemu parametry a v pcValue se předávají hodnoty parametrů. Rozlišení, co je parametr a co je hodnota parametru provádí httpd server.
4 Tady je malinko jiná funkce na rozsvícení a zhasnutí LED, pracujeme s Pico W
5 To samé jako bod 4.

Vlastní httpd server je naprogramován v Pico-SDK v rámci lwIP a nemusíme se tím naštěstí zabývat, protože to je hodně těžké.

A to je všechno. Mělo by to fungovat.

Sestavení

Zde jse seznam souborů nutných k sestavení projektu. CMakeLists.txt je sestavovací předpis pro Cmake, lwipopts.h je konfigurace TCP/IP stacku lwIP. Do toho raději nešahejte.

CMakeLists.txt
cmake_minimum_required(VERSION 3.13)

set(PROGRAM_NAME pico_w_webserver)
set(PICO_BOARD pico_w)

if(DEFINED HOSTNAME)
    add_compile_definitions(CYW43_HOST_NAME=\"${HOSTNAME}\")
endif()

include(pico_sdk_import.cmake)

project(pico_w_webserver)

pico_sdk_init()

message("Running makefsdata python script")
execute_process(COMMAND
    python3 makefsdata.py
    WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}
)

add_executable(${PROGRAM_NAME}
    main.c
)

target_include_directories(${PROGRAM_NAME} PRIVATE
    ${CMAKE_CURRENT_LIST_DIR}
)

target_link_libraries(${PROGRAM_NAME}
    pico_cyw43_arch_lwip_threadsafe_background
    pico_lwip_http
    pico_stdlib
    hardware_adc
)

pico_enable_stdio_usb(${PROGRAM_NAME} TRUE)
pico_enable_stdio_uart(${PROGRAM_NAME} FALSE)

pico_add_extra_outputs(${PROGRAM_NAME})
html_files/index.shtml
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <meta name="author" content="Mgr. Ing. Jiří Chráska">
        <title>Měření teploty a ovládání LED</title>
	    <style>
	      *    {margin 0; padding: 0; font-size: 1em; font-family: 'Segoe UI', Tahoma, sans-serif;}
	      body {background-color: #dcdcdc;}
	      h1   {color: #00008b ; font-family: verdana; font-size: 120%;}
	      h2   {color: #00008b; font-family: verdana; font-size: 100%;}
	      button {margin: 5px; padding: 10px;}
	    </style>
    </head>
    <body> <h1>Jednoduchý webserver Pico W</h1>
        <br>
        <h2>Technologie SSI:</h2>
        <p>Napětí: <!--#volt--> V</p>
        <p>Teplota: <!--#temp--> °C</p>
        <p>LED je: <!--#led--></p>
        <br>
        <h2>Technologie CGI:</h2>
        <div><a href="/led.cgi?led=1"><button>Zapnout LED</button></a></div>
        <div><a href="/led.cgi?led=0"><button>Vypnout LED</button></a></div>
        <br>
        <br>
        <div><a href="/index.shtml"><button>Aktualizovat měření</button></a></div>
   </body>
</html>
main.c
/* Web server na Pico W
 * (c) Jirka Chráska 2024, <jirka@lixis.cz>
 * BSD-licence
 */
#include "lwip/apps/httpd.h"
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include "lwip/netif.h"
#include "lwipopts.h"
#include "ssi.h"
#include "cgi.h"

// hostname
const char MY_HOSTNAME[] = "jirkovopico1";

// WIFI udaje pro pripojeni
const char WIFI_SSID[] = "essidwifi";
const char WIFI_PASSWORD[] = "heslowifi";

int main() {

    stdio_init_all();
    // pauza na nahození seriove linky
    sleep_ms(20000);

    cyw43_arch_init();
    // wifina bude jako klient
    cyw43_arch_enable_sta_mode();
    // nastavím hostname
    printf("Nastavuji hostname na %s\n", MY_HOSTNAME);
    struct netif *n = &cyw43_state.netif[CYW43_ITF_STA];
    cyw43_arch_lwip_begin();
    netif_set_hostname(n,MY_HOSTNAME);
    netif_set_up(n);
    cyw43_arch_lwip_end();

    // Zkusim se ve smycce pripojit k wifine
    while(cyw43_arch_wifi_connect_timeout_ms(WIFI_SSID, WIFI_PASSWORD, CYW43_AUTH_WPA2_AES_PSK, 30000) != 0){
        printf("Pokousim se pripojit...\n");
    }
    // Jsem pripojen
    printf("Pripojen k %s: %s  IP adresa: %s\n", WIFI_SSID, MY_HOSTNAME, ip4addr_ntoa(netif_ip4_addr(netif_list)));

    // Initializace web serveru
    httpd_init();
    printf("Http server nahozen.\n");

    // Konfigurace SSI a CGI handleru
    ssi_init();
    printf("SSI Handler nahozen.\n");
    cgi_init();
    printf("CGI Handler nahozen.\n");

    // Nekonecna smycka
    while(1);
}
ssi.h
#include "lwip/apps/httpd.h"
#include "pico/cyw43_arch.h"
#include "hardware/adc.h"

#define REF_VOLT 3.3f

// SSI tags - tag length limited to 8 bytes by default
const char * ssi_tags[] = {"volt","temp","led"};

u16_t ssi_handler(int iIndex, char *pcInsert, int iInsertLen) {
  size_t printed;
  switch (iIndex) {
  case 0: // volty
    {
      const float voltage = adc_read() * REF_VOLT / (1 << 12);
      printed = snprintf(pcInsert, iInsertLen, "%f", voltage);
    }
    break;
  case 1: // teplota
    {
    const float voltage = adc_read() * REF_VOLT / (1 << 12);
    const float tempC = 27.0f - (voltage - 0.706f) / 0.001721f;
    printed = snprintf(pcInsert, iInsertLen, "%f", tempC);
    }
    break;
  case 2: // led
    {
      bool led_status = cyw43_arch_gpio_get(CYW43_WL_GPIO_LED_PIN);
      if(led_status == true){
        printed = snprintf(pcInsert, iInsertLen, "zapnuto");
      }
      else{
        printed = snprintf(pcInsert, iInsertLen, "vypnuto");
      }
    }
    break;
  default:
    printed = 0;
    break;
  }

  return (u16_t)printed;
}

// Initialise the SSI handler
void ssi_init() {
  // Initialise ADC (internal pin)
  adc_init();
  adc_set_temp_sensor_enabled(true);
  adc_select_input(4);

  http_set_ssi_handler(ssi_handler, ssi_tags, LWIP_ARRAYSIZE(ssi_tags));
}
cgi.h
#include "lwip/apps/httpd.h"
#include "pico/cyw43_arch.h"

// CGI handler which is run when a request for /led.cgi is detected
const char * cgi_led_handler(int iIndex, int iNumParams, char *pcParam[], char *pcValue[])
{
    // Check if an request for LED has been made (/led.cgi?led=x)
    if (strcmp(pcParam[0] , "led") == 0){
        // Look at the argument to check if LED is to be turned on (x=1) or off (x=0)
        if(strcmp(pcValue[0], "0") == 0)
            cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 0);
        else if(strcmp(pcValue[0], "1") == 0)
            cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 1);
    }

    // Send the index page back to the user
    return "/index.shtml";
}

// tCGI Struct
// Fill this with all of the CGI requests and their respective handlers
static const tCGI cgi_handlers[] = {
    {
        // Html request for "/led.cgi" triggers cgi_handler
        "/led.cgi", cgi_led_handler
    },
};

void cgi_init(void)
{
    http_set_cgi_handlers(cgi_handlers, 1);
}

Pythoní skript, který předělá html soubory v adresáři html_files do céčkového kódu. (Na Picu nemáme filesystém).

makefsdata.py
#!/usr/bin/python3

# This script is by @rspeir on GitHub:
# https://github.com/krzmaz/pico-w-webserver-example/pull/1/files/4b3e78351dd236f213da9bebbb20df690d470476#diff-e675c4a367e382db6f9ba61833a58c62029d8c71c3156a9f238b612b69de279d
# Renamed output to avoid linking incorrect file

import os
import binascii

#Create file to write output into
output = open('htmldata.c', 'w')

#Traverse directory, generate list of files
files = list()
os.chdir('./html_files')
for(dirpath, dirnames, filenames) in os.walk('.'):
    files += [os.path.join(dirpath, file) for file in filenames]

filenames = list()
varnames  = list()

#Generate appropriate HTTP headers
for file in files:

    if '404' in file:
        header = "HTTP/1.0 404 File not found\r\n"
    else:
        header = "HTTP/1.0 200 OK\r\n"

    header += "Server: lwIP/pre-0.6 (http://www.sics.se/~adam/lwip/)\r\n"

    if '.html' in file:
        header += "Content-type: text/html\r\n"
    elif '.shtml' in file:
        header += "Content-type: text/html\r\n"
    elif '.jpg' in file:
        header += "Content-type: image/jpeg\r\n"
    elif '.gif' in file:
        header += "Content-type: image/gif\r\n"
    elif '.png' in file:
        header += "Content-type: image/png\r\n"
    elif '.class' in file:
       header += "Content-type: application/octet-stream\r\n"
    elif '.js' in file:
       header += "Content-type: text/javascript\r\n"
    elif '.css' in file:
       header += "Content-type: text/css\r\n"
    elif '.svg' in file:
       header += "Content-type: image/svg+xml\r\n"
    else:
        header += "Content-type: text/plain\r\n"

    header += "\r\n"

    fvar = file[1:]                 #remove leading dot in filename
    fvar = fvar.replace('/', '_')   #replace *nix path separator with underscore
    fvar = fvar.replace('\\', '_')  #replace DOS path separator with underscore
    fvar = fvar.replace('.', '_')   #replace file extension dot with underscore

    output.write("static const unsigned char data{}[] = {{\n".format(fvar))
    output.write("\t/* {} */\n\t".format(file))

    #first set of hex data encodes the filename
    b = bytes(file[1:].replace('\\', '/'), 'utf-8')     #change DOS path separator to forward slash
    for byte in binascii.hexlify(b, b' ', 1).split():
        output.write("0x{}, ".format(byte.decode()))
    output.write("0,\n\t")

    #second set of hex data is the HTTP header/mime type we generated above
    b = bytes(header, 'utf-8')
    count = 0
    for byte in binascii.hexlify(b, b' ', 1).split():
        output.write("0x{}, ".format(byte.decode()))
        count = count + 1
        if(count == 10):
            output.write("\n\t")
            count = 0
    output.write("\n\t")

    #finally, dump raw hex data from files
    with open(file, 'rb') as f:
        count = 0
        while(byte := f.read(1)):
            byte = binascii.hexlify(byte)
            output.write("0x{}, ".format(byte.decode()))
            count = count + 1
            if(count == 10):
                output.write("\n\t")
                count = 0
        output.write("};\n\n")

    filenames.append(file[1:])
    varnames.append(fvar)

for i in range(len(filenames)):
    prevfile = "NULL"
    if(i > 0):
        prevfile = "file" + varnames[i-1]

    output.write("const struct fsdata_file file{0}[] = {{{{ {1}, data{2}, ".format(varnames[i], prevfile, varnames[i]))
    output.write("data{} + {}, ".format(varnames[i], len(filenames[i]) + 1))
    output.write("sizeof(data{}) - {}, ".format(varnames[i], len(filenames[i]) + 1))
    output.write("FS_FILE_FLAGS_HEADER_INCLUDED | FS_FILE_FLAGS_HEADER_PERSISTENT}};\n")

output.write("\n#define FS_ROOT file{}\n".format(varnames[-1]))
output.write("#define FS_NUMFILES {}\n".format(len(filenames)))
lwipopts.h
// Common settings used in most of the pico_w examples
// (see https://www.nongnu.org/lwip/2_1_x/group__lwip__opts.html for details)]

// allow override in some examples
#ifndef NO_SYS
#define NO_SYS                      1
#endif
// allow override in some examples
#ifndef LWIP_SOCKET
#define LWIP_SOCKET                 0
#endif
#if PICO_CYW43_ARCH_POLL
#define MEM_LIBC_MALLOC             1
#else
// MEM_LIBC_MALLOC is incompatible with non polling versions
#define MEM_LIBC_MALLOC             0
#endif
#define MEM_ALIGNMENT               4
#define MEM_SIZE                    4000
#define MEMP_NUM_TCP_SEG            32
#define MEMP_NUM_ARP_QUEUE          10
#define PBUF_POOL_SIZE              24
#define LWIP_ARP                    1
#define LWIP_ETHERNET               1
#define LWIP_ICMP                   1
#define LWIP_RAW                    1
#define TCP_WND                     (8 * TCP_MSS)
#define TCP_MSS                     1460
#define TCP_SND_BUF                 (8 * TCP_MSS)
#define TCP_SND_QUEUELEN            ((4 * (TCP_SND_BUF) + (TCP_MSS - 1)) / (TCP_MSS))
#define LWIP_NETIF_STATUS_CALLBACK  1
#define LWIP_NETIF_LINK_CALLBACK    1
#define LWIP_NETIF_HOSTNAME         1
#define LWIP_NETCONN                0
#define MEM_STATS                   0
#define SYS_STATS                   0
#define MEMP_STATS                  0
#define LINK_STATS                  0
// #define ETH_PAD_SIZE                2
#define LWIP_CHKSUM_ALGORITHM       3
#define LWIP_DHCP                   1
#define LWIP_IPV4                   1
#define LWIP_TCP                    1
#define LWIP_UDP                    1
#define LWIP_DNS                    1
#define LWIP_TCP_KEEPALIVE          1
#define LWIP_NETIF_TX_SINGLE_PBUF   1
#define DHCP_DOES_ARP_CHECK         0
#define LWIP_DHCP_DOES_ACD_CHECK    0

#ifndef NDEBUG
#define LWIP_DEBUG                  1
#define LWIP_STATS                  1
#define LWIP_STATS_DISPLAY          1
#endif

#define ETHARP_DEBUG                LWIP_DBG_OFF
#define NETIF_DEBUG                 LWIP_DBG_OFF
#define PBUF_DEBUG                  LWIP_DBG_OFF
#define API_LIB_DEBUG               LWIP_DBG_OFF
#define API_MSG_DEBUG               LWIP_DBG_OFF
#define SOCKETS_DEBUG               LWIP_DBG_OFF
#define ICMP_DEBUG                  LWIP_DBG_OFF
#define INET_DEBUG                  LWIP_DBG_OFF
#define IP_DEBUG                    LWIP_DBG_OFF
#define IP_REASS_DEBUG              LWIP_DBG_OFF
#define RAW_DEBUG                   LWIP_DBG_OFF
#define MEM_DEBUG                   LWIP_DBG_OFF
#define MEMP_DEBUG                  LWIP_DBG_OFF
#define SYS_DEBUG                   LWIP_DBG_OFF
#define TCP_DEBUG                   LWIP_DBG_OFF
#define TCP_INPUT_DEBUG             LWIP_DBG_OFF
#define TCP_OUTPUT_DEBUG            LWIP_DBG_OFF
#define TCP_RTO_DEBUG               LWIP_DBG_OFF
#define TCP_CWND_DEBUG              LWIP_DBG_OFF
#define TCP_WND_DEBUG               LWIP_DBG_OFF
#define TCP_FR_DEBUG                LWIP_DBG_OFF
#define TCP_QLEN_DEBUG              LWIP_DBG_OFF
#define TCP_RST_DEBUG               LWIP_DBG_OFF
#define UDP_DEBUG                   LWIP_DBG_OFF
#define TCPIP_DEBUG                 LWIP_DBG_OFF
#define PPP_DEBUG                   LWIP_DBG_OFF
#define SLIP_DEBUG                  LWIP_DBG_OFF
#define DHCP_DEBUG                  LWIP_DBG_OFF

// This section enables HTTPD server with SSI, SGI
// and tells server which converted HTML files to use
#define LWIP_HTTPD 1
#define LWIP_HTTPD_SSI 1
#define LWIP_HTTPD_CGI 1
#define LWIP_HTTPD_SSI_INCLUDE_TAG 0
#define HTTPD_FSDATA_FILE "htmldata.c"
pico_sdk_import.cmake
# This is a copy of <PICO_SDK_PATH>/external/pico_sdk_import.cmake

# This can be dropped into an external project to help locate this SDK
# It should be include()ed prior to project()

if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH))
    set(PICO_SDK_PATH $ENV{PICO_SDK_PATH})
    message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')")
endif ()

if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT))
    set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT})
    message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')")
endif ()

if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH))
    set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH})
    message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')")
endif ()

set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK")
set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable")
set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK")

if (NOT PICO_SDK_PATH)
    if (PICO_SDK_FETCH_FROM_GIT)
        include(FetchContent)
        set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR})
        if (PICO_SDK_FETCH_FROM_GIT_PATH)
            get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}")
        endif ()
        # GIT_SUBMODULES_RECURSE was added in 3.17
        if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0")
            FetchContent_Declare(
                    pico_sdk
                    GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
                    GIT_TAG master
                    GIT_SUBMODULES_RECURSE FALSE
            )
        else ()
            FetchContent_Declare(
                    pico_sdk
                    GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
                    GIT_TAG master
            )
        endif ()

        if (NOT pico_sdk)
            message("Downloading Raspberry Pi Pico SDK")
            FetchContent_Populate(pico_sdk)
            set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR})
        endif ()
        set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE})
    else ()
        message(FATAL_ERROR
                "SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git."
                )
    endif ()
endif ()

get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}")
if (NOT EXISTS ${PICO_SDK_PATH})
    message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found")
endif ()

set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake)
if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE})
    message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK")
endif ()

set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE)

include(${PICO_SDK_INIT_CMAKE_FILE})

Všechno zabalené ke stažení projekt-web-server.tar.gz

Doplnění o displej

Pro snadné zjištění IP adresy jsem k zařízení dopnil OLED displej pro zobrazovaní parametrů spojení a lehký debugging.

Pico W webserver s OLED diplejem

IMG 20240210 122238

Na OpenBSD DHCP serveru se hostname zobrazuje správně.

dizzy# cat /var/db/dhcpd.leases

lease 192.168.120.249 {
	starts 6 2024/02/10 11:18:56 UTC;
	ends 6 2024/02/10 12:18:56 UTC;
	hardware ethernet 28:cd:c1:0c:33:1e;
	client-hostname "jirkovopico1";
}

Projekt s OLED displejem ke stažení projekt-web-server2.tar.gz

Zdroje a odkazy

Poznámky

IPv6

/home-jirka/pico/pico-sdk/lib/btstack/3rd-party/lwip/core/src/include/lwip/ip6_addr.h /home-jirka/pico/pico-sdk/lib/btstack/3rd-party/lwip/core/src/include/lwip/netif.h netif_ip6_addr()