prettyprint

2023年2月14日 星期二

[Raspberry Pi Pico W (c-sdk)] lwIP: Ep 5. HTTP Server & WiFiManager

本文章介紹Raspberry Pi Pico W使用lwIP HTTP application建立WiFi Manager,當Pico W沒有儲存WiFi連線的SSID與Password時,啟用AP模式,並啟用HTTP與DHCP server,透過網頁設定要連線的WiFi SSID與Password,並將設定檔存在flash memory中。如下流程圖。
一、HTTP server:
lwIP的http server(HTTPD)支援簡易SSI(server-side-include)與CGI功能,另外有支援POST功能。
  • SSI ; 相對應的API:
    http_set_ssi_handler():Set the SSI handler function.
    tSSIHandler: 處理SSI tag的callback function,可使用multi part處理較長的內容。
    const char ** tags: SSI tags。
  • CGI: 相對應的API:
    http_set_cgi_handlers():Set an array of CGI filenames/handler functions。
    tCGI StructURL):指定URL與相對應的function。
  • POST: 相對應的API:
    httpd_post_begin():
    httpd_post_receive_data():每收到一個pbuf就呼叫一次。
    httpd_post_data_recved():
    httpd_post_finished():資料收完或connection close。
  • 啟用HTTPD:
    httpd_init()
  • 在lwipots.h加入下列:
    #define LWIP_HTTPD 1
    #define LWIP_HTTPD_SSI 1
    #define LWIP_HTTPD_CGI 1
    #define LWIP_HTTPD_SSI_MULTIPART 1
    #define LWIP_HTTPD_SUPPORT_POST 1
    #define LWIP_HTTPD_SSI_INCLUDE_TAG 0
    #define HTTPD_FSDATA_FILE "_fsdata.c"  //網頁檔案存放的flash memory image檔。
  • 在pico-sdk/lib/lwip/src/apps/http/makefsdata的perl檔案makefsdata用來產生fsdata.c
  • 修改makefsdata perl檔案,約在24行處,將
    if($file =~ /\.html) {
    改成
    if($file =~ /\.html$/ or $file =~ /\.shtml$/ or $file =~ /\.htm$/ or $file =~ /\.shtm$/) {
    讓.shtml副檔名的檔案,加入Content-type: text/html\r\n檔頭,成為網頁檔案。
  • 在CMakeLists.txt加入下列執行產生fsdata.c
二、WiFi scan:
須先啟用為STA或AP mode才可執行wifi scan功能。
相對應API:
cyw43_arch_enable_sta_mode() or cyw43_arch_enable_ap_mode()
  • cyw43_wifi_scan(&cyw43_state, &scan_options, aps, scan_result):啟用wifi scan並且呼叫scan_result callback function取得scan到的資料。
  • cyw43_wifi_scan_active(&cyw43_state):查看wifi scan是否以完成。
其他進一步解說,請觀看「成果影片」連結。

三、成果影片


四、程式碼:
  • cSJON: https://github.com/DaveGamble/cJSON
  • dhcpserver: pico_examples/pico_w/wifi/access_point/dhcp_server
  • makefsdata: pico-sdk/lib/lwip/src/apps/http/makefsdata
  • ap_http_server.c
 #include <stdio.h>
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include "lwip/apps/httpd.h"
#include "hardware/flash.h"
#include "hardware/watchdog.h"

#include "ap_http_server.h"
#include "wifi_scan.h"
#include "dhcpserver.h"

SCAN_APS_T *aps;

dhcp_server_t dhcp_server;
#define WIFI_AP_SSID "PicoW"
#define WIFI_AP_PASSWORD "picowpwd#"

/* ==== cgi begin ======*/
const char *
cgi_handler_wifi_refresh(int iIndex, int iNumParams, char *pcParam[], char *pcValue[]) {
    
    scan_aps(aps, 20000);
    return "/index.shtml";

}

static const tCGI cgi_handlers[] = {
    {
        //* Html request for "/wifi_refresh.cgi" will start cgi_handler_wifi_refresh 
        "/wifi_refresh.cgi", cgi_handler_wifi_refresh
    },
};
/* ==== cgi end ======*/

/*===== post begin =====*/
typedef struct HTTP_POST_SERVER_T_ {
    void *current_connection;
    //void *valid_connection;
    char ssid[WIFI_PASS_BUFSIZE];
    char pass[WIFI_PASS_BUFSIZE];
    bool post_recv;
 } HTTP_POST_SERVER_T;

HTTP_POST_SERVER_T *server=NULL;

char* urldecode(char* str) {
  char tmpstr[WIFI_PASS_BUFSIZE];
  int j=0;
  int i=0;
  char tmpval[5];
  while(i < strlen(str)) {
    if (str[i] == '%') {
        sprintf(tmpval, "%s%c%c", "0x",str[i+1], str[i+2]);
        tmpstr[j] = strtol(tmpval, NULL, 16);
        i+=3;
    } else {
      tmpstr[j]=str[i];
      i++;
    }
    j++;
  }
  tmpstr[j]='\0';
  strcpy(str, tmpstr);
  return str;
}

void save_to_flash() {
  char flash_buff[256];
  flash_range_erase(FLASH_OFFSET, 4096); // one sector

  memset(flash_buff, 0, 256);
  sprintf(flash_buff, "{\"ssid\":\"%s\",\"pass\":\"%s\"}", urldecode(server->ssid), urldecode(server->pass));
  flash_range_program(FLASH_OFFSET, flash_buff, 256);   // one page

  watchdog_enable(5000, false); // after save data to flash, reboot in 5 seconds
}

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)  {
  LWIP_UNUSED_ARG(connection);
  LWIP_UNUSED_ARG(http_request);
  LWIP_UNUSED_ARG(http_request_len);
  LWIP_UNUSED_ARG(content_len);
  LWIP_UNUSED_ARG(post_auto_wnd);
  server->post_recv=false;
  if (!memcmp(uri, "/wifi_conn.shtml", 17)) {
    if (server->current_connection != connection) {
      server->current_connection = connection;
      //server->valid_connection = NULL;
      snprintf(response_uri, response_uri_len, "/index.shtml"); // default : return main page
      /* e.g. for large uploads to slow flash over a fast connection, you should
         manually update the rx window. That way, a sender can only send a full
         tcp window at a time. If this is required, set 'post_aut_wnd' to 0.
         We do not need to throttle upload speed here, so: */
      *post_auto_wnd = 1;

      return ERR_OK;
    }
  }
  return ERR_VAL;
}

err_t httpd_post_receive_data(void *connection, struct pbuf *p) {
  err_t ret;

  LWIP_ASSERT("NULL pbuf", p != NULL);

  if (server->current_connection == connection) {
    u16_t token_ssid = pbuf_memfind(p, "ssid=", 5, 0);
    u16_t token_pass = pbuf_memfind(p, "pass=", 5, 0);
 
    if ((token_ssid != 0xFFFF) && (token_pass != 0xFFFF)) {
      u16_t value_ssid = token_ssid + 5;
      u16_t value_pass = token_pass + 5;
    
      u16_t len_ssid = 0;
      u16_t len_pass = 0;
      
      u16_t tmp;
      
      /* find ssid len */
      tmp = pbuf_memfind(p, "&", 1, value_ssid);
      if (tmp != 0xFFFF) {
        len_ssid = tmp - value_ssid;
      } else {
        len_ssid = p->tot_len - value_ssid;
      }
      /* find pass len */
      tmp = pbuf_memfind(p, "&", 1, value_pass);
      if (tmp != 0xFFFF) {
        len_pass = tmp - value_pass;
      } else {
        len_pass = p->tot_len - value_pass;
      }
      
      if ((len_ssid > 0) && (len_ssid < WIFI_PASS_BUFSIZE) &&
          (len_pass > 0) && (len_pass < WIFI_PASS_BUFSIZE) ) {
        
        char* tmpstr= (char*)pbuf_get_contiguous(p, &server->ssid, sizeof(server->ssid), len_ssid, value_ssid);
        tmpstr[len_ssid]=0;
        strcpy(server->ssid, tmpstr);
        tmpstr = (char*)pbuf_get_contiguous(p, &server->pass, sizeof(server->pass), len_pass, value_pass);
        tmpstr[len_pass]=0;
        strcpy(server->pass, tmpstr);
        server->post_recv=true;
      }
    }
    //server->valid_connection = connection;
   
    ret = ERR_OK;
  } else {
    ret = ERR_VAL;
  }

  /* this function must ALWAYS free the pbuf it is passed or it will leak memory */
  pbuf_free(p);

  return ret;
}

void httpd_post_finished(void *connection, char *response_uri, u16_t response_uri_len) {
  if (server->current_connection == connection) {
    //if (server->valid_connection == connection) {
        if (server->post_recv) {
           //save ssid & pass to fresh memory
            save_to_flash();
            snprintf(response_uri, response_uri_len, "/wifi_conn.shtml");
        } 
    //}
    server->current_connection = NULL;
    //server->valid_connection = NULL;
  }
}
/*===== post end =====*/

/*  === ssi begin =====*/
const char* __not_in_flash("httpd") ssi_tags[] = {
    "scanwifi",
    "ssid",
};

/* for scan wifi, multipart: every part for one scaned AP*/
u16_t __time_critical_func(ssi_handler)(int iIndex, char *pcInsert, int iInsertLen, u16_t current_tag_part, u16_t *next_tag_part)
{
    size_t printed;
    static char buff[500];
    char keyimg[]="<img src='img/key.png'>";
    char checked[8]="checked";
    int rssi=1;
    switch (iIndex) { 
        case 0: // for scanwifi in index.shtml
        if (aps) {
            if (current_tag_part < aps->len) {
                if (((aps->AP)+current_tag_part)->rssi > -90) rssi=2;
                if (((aps->AP)+current_tag_part)->rssi > -80) rssi=3;
                if (((aps->AP)+current_tag_part)->rssi > -70) rssi=4;
                if (((aps->AP)+current_tag_part)->auth_mode == CYW43_AUTH_OPEN) strcpy(keyimg,"");
                if (current_tag_part == 0) strcpy(checked, "checked"); else strcpy(checked, "");
                sprintf(buff, "<tr>"
                "<td>"
                "<input  type='radio' name='ssid' %s value='%s'>%s"
                "</td>"
                "<td>"
                "<img src='img/wifi%d.png'>"
                "</td>"
                "<td>%s</td></tr>", 
                checked, ((aps->AP)+current_tag_part)->ssid,((aps->AP)+current_tag_part)->ssid, rssi, keyimg);
                *next_tag_part=current_tag_part+1;
                printed = snprintf(pcInsert, iInsertLen, buff);

            } else {
                printed = snprintf(pcInsert, iInsertLen, "");
            }
        }
        break;
        case 1: // for ssid in wifi_conn.shtml
            printed = snprintf(pcInsert, iInsertLen,server->ssid);
            break;
        default: 
          printed = snprintf(pcInsert, iInsertLen, "");
    }
    return printed;
}
/*  === ssi end =====*/

void ap_http_server_start() {
     aps = (SCAN_APS_T*)calloc(1, sizeof(SCAN_APS_T));
     if (!aps) {
        printf("cannot alloc scan ap memory\n");
        return;
    }
    
    server = (HTTP_POST_SERVER_T*) calloc(1, sizeof(HTTP_POST_SERVER_T));
    if (!server) {
        printf("cannot alloc server object\n");
        return;
    }
    if (cyw43_arch_init()) {
      printf("http server cyw43_arch init error\n");
      return;
    }
    printf("Starting AP Mode: local IP:192.168.4.1\n");
    cyw43_arch_enable_ap_mode(WIFI_AP_SSID, WIFI_AP_PASSWORD, CYW43_AUTH_WPA2_AES_PSK);
    /* start dhcp server*/
    ip_addr_t gw, mask;
    IP4_ADDR(ip_2_ip4(&gw), 192, 168, 4, 1);
    IP4_ADDR(ip_2_ip4(&mask), 255, 255, 255, 0); 
    dhcp_server_init(&dhcp_server, &gw, &mask);

    scan_aps(aps, 2000);
  
    //check tag length
    size_t i;
    for (i = 0; i < LWIP_ARRAYSIZE(ssi_tags); i++) {
        LWIP_ASSERT("tag too long for LWIP_HTTPD_MAX_TAG_NAME_LEN",
                    strlen(ssi_tags[i]) <= LWIP_HTTPD_MAX_TAG_NAME_LEN);
    }
    
    http_set_ssi_handler(ssi_handler, ssi_tags, LWIP_ARRAYSIZE(ssi_tags));
    http_set_cgi_handlers(cgi_handlers, LWIP_ARRAYSIZE(cgi_handlers));

    httpd_init();
 
}

void ap_http_server_stop() {
    dhcp_server_deinit(&dhcp_server);
    free(aps);
    cyw43_arch_deinit();
}

  • ap_http_server.h
 #ifndef __HTTP_SERVER_H_
#define __HTTP_SERVER_H_

#define FLASH_OFFSET  0x180000    //1.5M
#define WIFI_PASS_BUFSIZE 91
void ap_http_server_start();
void ap_http_server_stop();
char* urldecode(char* str);

#endif

  • wifi_scan.c
#include <stdio.h>

#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include "string.h"
#include "wifi_scan.h"

static int scan_result(void *env, const cyw43_ev_scan_result_t *result) {
    SCAN_APS_T *res_APs = (SCAN_APS_T*) env;
    if (result) {
        for (int i =0; i < res_APs->len; i++) {
            if (strcmp(result->ssid, ((res_APs->AP)+i)->ssid)==0) {
                if (result->rssi > ((res_APs->AP)+i)->rssi) 
                    *((res_APs->AP)+i) = *result; 
                return 0;
            }
        }
        cyw43_ev_scan_result_t *tmp = (cyw43_ev_scan_result_t*) realloc(res_APs->AP, sizeof(cyw43_ev_scan_result_t)*(res_APs->len+1));
        if (tmp) {
            res_APs->len += 1;
            res_APs->AP = tmp;
            *(res_APs->AP+res_APs->len-1) = *result;
        }
    } 
   
    return res_APs->len;
}

bool scan_aps(SCAN_APS_T* aps, uint32_t timeout) { // timeout: ms
    //cyw43_arch_enable_sta_mode() or cyw43_arch_enable_ap_mode() must be called before this function
    bool ret = false;
    aps = realloc(aps,0);
    cyw43_wifi_scan_options_t scan_options = {0};
    int err = cyw43_wifi_scan(&cyw43_state, &scan_options, aps, scan_result);
    if (err == 0) {
        printf("\nPerforming wifi scan\n");
        absolute_time_t scan_timeout = make_timeout_time_ms(timeout); // timeout
        while(absolute_time_diff_us(get_absolute_time(), scan_timeout) > 0) {
            if (!cyw43_wifi_scan_active(&cyw43_state)) {
                    //print out all scaned APs for debug
                for (int i =0; i < aps->len; i++) {
                    printf("%d.. ssid: %-32s rssi: %4d chan: %3d mac: %02x:%02x:%02x:%02x:%02x:%02x sec: %u\n",
                        i, (aps->AP+i)->ssid, (aps->AP+i)->rssi, (aps->AP+i)->channel,
                        (aps->AP+i)->bssid[0], (aps->AP+i)->bssid[1], (aps->AP+i)->bssid[2], (aps->AP+i)->bssid[3], (aps->AP+i)->bssid[4], (aps->AP+i)->bssid[5],
                        (aps->AP+i)->auth_mode);
                }
                ret = true;
                break;
            }
            cyw43_arch_poll();
            sleep_ms(1);
        }
        ret=false;
    } else {
        printf("Failed to start scan: %d\n", err);
        ret = false;
    }
    
    return ret;

}


  • wifi_scan.h
#ifndef __WIFI_SCAN_H_
#define _WIFI_SCAN_H_

#include "pico/stdlib.h"
typedef struct SCAN_APS_T_ {
    uint16_t len;
    cyw43_ev_scan_result_t *AP;
} SCAN_APS_T;

bool scan_aps(SCAN_APS_T* aps, uint32_t timeout);
#endif 

CMakeLists.txt
 # perl makefsdata 
find_package(Perl)
if(NOT PERL_FOUND)
    message(FATAL_ERROR "Perl is needed for generating the fsdata.c file")
endif()

set(MAKE_FS_DATA_SCRIPT ${CMAKE_CURRENT_LIST_DIR}/mkfsdata/makefsdata)

if (EXISTS ${MAKE_FS_DATA_SCRIPT})
    message("Find makefsdata script")
    message("Running makefsdata script")
      execute_process(COMMAND
          perl ${MAKE_FS_DATA_SCRIPT}
          WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}
          ECHO_OUTPUT_VARIABLE
          ECHO_ERROR_VARIABLE
        )
    file(RENAME fsdata.c _fsdata.c)
endif()

# Generated Cmake Pico project file
cmake_minimum_required(VERSION 3.13)

set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

# Initialise pico_sdk from installed location
# (note this can come from environment, CMake cache etc)
set(PICO_SDK_PATH "/home/duser/pico/pico-sdk")

set(PICO_BOARD pico_w CACHE STRING "Board type")

# Pull in Raspberry Pi Pico SDK (must be before project)
include(pico_sdk_import.cmake)

if (PICO_SDK_VERSION_STRING VERSION_LESS "1.5.0")
  message(FATAL_ERROR "Raspberry Pi Pico SDK version 1.5.0 (or later) required. Your version is ${PICO_SDK_VERSION_STRING}")
endif()

project(picow_wifimanager C CXX ASM)

# Initialise the Raspberry Pi Pico SDK
pico_sdk_init()

# Add executable. Default name is the project name, version 0.1

add_executable(picow_wifimanager 
  picow_wifimanager.c 
    wifi_scan/wifi_scan.c 
    ap_http_server/ap_http_server.c 
    cJSON/cJSON.c
    dhcpserver/dhcpserver.c
    )

pico_set_program_name(picow_wifimanager "picow_wifimanager")
pico_set_program_version(picow_wifimanager "0.1")

pico_enable_stdio_uart(picow_wifimanager 1)
pico_enable_stdio_usb(picow_wifimanager 0)

# Add the standard library to the build
target_link_libraries(picow_wifimanager
        pico_stdlib)

# Add the standard include files to the build
target_include_directories(picow_wifimanager PRIVATE
  ${CMAKE_CURRENT_LIST_DIR}
  ${CMAKE_CURRENT_LIST_DIR}/.. # for our common lwipopts or any other standard includes, if required
  ${CMAKE_CURRENT_LIST_DIR}/dhcpserver
  ${CMAKE_CURRENT_LIST_DIR}/cJSON 
  ${CMAKE_CURRENT_LIST_DIR}/ap_http_server
  ${CMAKE_CURRENT_LIST_DIR}/wifi_scan

)

# Add any user requested libraries
target_link_libraries(picow_wifimanager
        pico_cyw43_arch_lwip_poll
        pico_lwip_http
        hardware_flash
        hardware_watchdog
        )

pico_add_extra_outputs(picow_wifimanager)
  • picow_wifimanager.c
 #include <stdio.h>
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include "lwip/apps/httpd.h"
#include "ap_http_server.h"
#include "cJSON.h"


bool connect_to_wifi_ssid() {

    char flash_buff[256];
    memset(flash_buff,0,256);
    snprintf(flash_buff, 256, "%s",(uint8_t*)(XIP_BASE+FLASH_OFFSET));
    if (!flash_buff) return false;
    cJSON *ssid_pass = cJSON_CreateObject();
    char *ssid;
    char *pass;
    ssid_pass = cJSON_Parse(flash_buff);
    if (ssid_pass) {
        ssid = cJSON_GetStringValue(cJSON_GetObjectItem(ssid_pass, "ssid"));
        pass = cJSON_GetStringValue(cJSON_GetObjectItem(ssid_pass, "pass"));
        
        if (cyw43_arch_init()) {
            printf("cyw43_arch init error\n");
            return false;
        }
        cyw43_arch_enable_sta_mode();
        printf("\n\n==========================\n"
            "Connecting to WiFi:%s\n"
                "==============================\n", ssid);
        if (cyw43_arch_wifi_connect_timeout_ms(ssid, pass, CYW43_AUTH_WPA2_AES_PSK, 10000)) { 
            printf("wifi sta connect error ssid:%s\n", ssid);
            cyw43_arch_deinit();
            return false;
        }
    } else {
        return false;
    }
    cJSON_Delete(ssid_pass);
    
    ip_addr_t addr = cyw43_state.netif->ip_addr;
    printf("connect successfully. get IP: %s\n", ipaddr_ntoa(&addr));
    return true;
}

int main()
{
    stdio_init_all();
    bool ap_mode=false;
    if (!connect_to_wifi_ssid()) {
        ap_mode=true;
        ap_http_server_start();
    }

    while(1) {
        static absolute_time_t led_time;
        static int led_on = true;
        if (absolute_time_diff_us(get_absolute_time(), led_time) < 0) {
            if (ap_mode) { 
                led_on = !led_on;
            }
            cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, led_on);
            led_time = make_timeout_time_ms(1000);
        }
        cyw43_arch_poll();
        sleep_ms(1);
    }
   
    ap_http_server_stop();
    return 0;
}
  • index.shtml
<!DOCTYPE html>
<html>
    <head> <title>Pico W WiFi Manager</title> 
    <style>
        h1 { font-size: 70px;}
        table {width:600px; margin:auto}
        img {height:35px;}
        td {padding:8px;font-size:40px;}
        p {font-size: 50px;}
        tr:hover {background-color: coral;}
        input[type="radio"] {
            height:40px;
            width:40px;
        }
                
    </style>
    </head>
    <body> <h1>Pico W WiFi Manager</h1>
        <p>Please select a SSID:</p>
        <form method="post" action="/wifi_conn.shtml">

        <table>
            <!--#scanwifi-->
        </table>  
        <p align="center">        
            Password:<input style="height:40px;width:300px;font-size: 35px;" type="password" name="pass" required maxlength="30"> </p>
        <p align="center"> 
            <input style="height:70px;width:250px;font-size: 40px;" type="submit" name="button" value="Save">&nbsp&nbsp&nbsp&nbsp
            <input style="height:70px;width:250px;font-size: 40px;" type="button" name="refresh" value="Refresh" onclick="window.location.href='wifi_refresh.cgi'">
        </p>
        
        </form>       
   </body>
</html>
  • wifi_conn.shtml
<!DOCTYPE html>
<html>
    <head>
        <title>
            Pico W WiFi Manager
        </title>
    </head>
    <body>
        <p style="font-size: 45px;">Connecting to ssid: <!--#ssid--> </p>
        <p style="font-size: 35px;color:red;">The device will reboot in 5 seconds ...  </p>
    </body>
</html>
  • 404.shmtl
<html>
<head><title>lwIP - A Lightweight TCP/IP Stack</title></head>
<body bgcolor="white" text="black">

    <table width="100%">
      <tr valign="top"><td width="80">	  
	  
	</td><td width="500">	  
	  <h1>lwIP - A Lightweight TCP/IP Stack</h1>
	  <h2>404 - Page not found</h2>
	  <p>
	    Sorry, the page you are requesting was not found on this
	    server. 
	  </p>
	</td><td>
	  &nbsp;
	</td></tr>
      </table>
</body>
</html>

沒有留言:

張貼留言