(8)新しい機能追加。(HTTP Serverその2)

今回から、前回立ち上げたHTTP_Server HP用のプログラムを書いて行きます。

先ずは、ホーム画面

サーバーのホーム画面として下記の様なものを考えています。

  • サーバー(my-esp32.local)にアクセスした時のホームページの画面。
  • タイトル、”Radio Editor”に下線を付け、色をフォントの大きさを指定。
  • ボタンは2つ。各ボタンで以下の作業を行う
    • Set WiFi: WiFiのSSIDとPasswordを設定
    • Radio Info: ラジオ局データの入力

以下はこのホームページのHTML。

index.html

<!doctype html>
<html>
	<head>
		<meta charset='utf-8'>
		<meta name='viewport' content='width=device-width,initial-scale=1'>
		<link rel='stylesheet' type='text/css' href='./http_server.css' >
		<title>Radio Editor</title>
	</head>
	<body>
		<center>
			<div class='b_frame'>
				<div class='t_font'><u>Radio Editor</u></div><br>
				<form method='get'>
					<button class='m_button' type='submit' name='1' value='0' > Set WiFi </button>
					<button class='m_button' type='submit' name='2' value='0' > Radio Info </button>
				</form>
			</div>
		</center>
	</body>
</html>

このホームページを実行するにはサーバーに以下の機能が必要です。

  • ホームページのHTMLファイル index.html を送信する
  • CSSファイル等、クライアントから要求さえたファイルを送信する。
  • クライアントからの操作(今回はボタン操作)を判断し処理する。

これらの機能をサーバーに実装して行きます。

サーバーへの実装

実装はmainフォルダーの下のmain.cに行いました。

main.c

#include <esp_wifi.h>
#include <esp_event.h>
#include <esp_log.h>
#include "nvs_flash.h"
#include <esp_http_server.h>
#include "mdns.h"
#include "esp_spiffs.h"
#include "freertos/event_groups.h"

static const char *TAG = "Http_server01";

#define SCRATCH_BUFSIZE  2000
esp_err_t send_file(httpd_req_t *req, char* f_name)
{
    FILE *fp = NULL;
    char send_buf[SCRATCH_BUFSIZE];
    size_t buf_size;

	fp = fopen(f_name, "r");
    do {
        buf_size = fread(send_buf, 1, SCRATCH_BUFSIZE, fp);
        if (buf_size > 0) 
        {
            if (httpd_resp_send_chunk(req, send_buf, buf_size) != ESP_OK) 
            {
                fclose(fp);
                ESP_LOGE(TAG, "File sending failed!");
                httpd_resp_sendstr_chunk(req, NULL);
                httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to send file");
               	return ESP_FAIL;
           	}
        }
    } while (buf_size != 0);
    fclose(fp);
    httpd_resp_send_chunk(req, NULL, 0);
               
    return ESP_OK;
}

//---------------	Station Edit    --------------------------------------------------------------------------
void station_edit(httpd_req_t *req)
{
}

//---------------	WiFi Edit HP    --------------------------------------------------------------------------
void wifi_edit(httpd_req_t *req)
{
}

//---------------	Radio Editor HP   --------------------------------------------------------------------------
void radio_main(httpd_req_t *req)
{
    httpd_resp_set_type(req, "text/html");
    send_file(req, "/spiffs/index.html");
}

#define IS_FILE_EXT(filename, ext) \
    (strcasecmp(&filename[strlen(filename) - sizeof(ext) + 1], ext) == 0)
void down_load_file(httpd_req_t *req)
{
    char name_buf[100];

	if (IS_FILE_EXT(req->uri, ".html")) httpd_resp_set_type(req, "text/html");
	else if (IS_FILE_EXT(req->uri, ".css")) httpd_resp_set_type(req, "text/css");
	else if (IS_FILE_EXT(req->uri, ".js")) httpd_resp_set_type(req, "application/javascript");
	else if (IS_FILE_EXT(req->uri, ".jpeg")) httpd_resp_set_type(req, "image/jpeg");
	else if (IS_FILE_EXT(req->uri, ".txt")) httpd_resp_set_type(req, "text/plain");
	strcpy(name_buf,"/spiffs");
	strcat(name_buf,req->uri);
	send_file(req, name_buf);
}

int flg_state = 1;
esp_err_t get_handler(httpd_req_t *req)
{
    char* buf;
    char * cmd_buf;
    size_t buf_len;
	int flg_index;
	int a;

    buf_len = strlen(req->uri);
    flg_index = 1;
    flg_state = 1;
    if (buf_len > 1)
    {
   		if(strchr(req->uri, '?') != NULL)
   		{
        	buf = malloc(buf_len);
        	if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK)
        	{
       			cmd_buf = strchr(buf, '='); 
       			*cmd_buf  = 0;
				a = atoi(buf);
       			*cmd_buf  = '=';
				
            	switch(a)
            	{
            		case 1:		// Edit Wifi
		            			flg_state = 2;
            					break;
            					
            		case 2:		// Enter Radio Info
		            			flg_state = 3;
            					break;

          			default	:	// Main Menu
		            			flg_state = 1;
            	}
        	}
        	free(buf);
   		}
   		else
   		{
   			down_load_file(req);
   			flg_index = 0;
   		}
    }

	if(flg_index)
	{
		switch(flg_state)
		{
			case 1:	radio_main(req);
					break;
			case 2:	wifi_edit(req);
					break;
			case 3:	station_edit(req);
					break;
		}
	}

    return ESP_OK;
}

struct st_info_data {         //Station info
    char _deco[5];
    char _st_ID[20];
    char _st_URL[200];
};

struct st_info_data info_data[24];

void read_station(void)
{
	char buf[330];
    FILE *fp = NULL;
    int a,b;

	fp = fopen("/spiffs/station.txt", "r");
	for(a = 0; a < 24; a ++)
	{
		fgets(buf, 300, fp);

		*(strchr(buf, ',')) = 0;
		strcpy(info_data[a]._deco, buf);
		b = strlen(buf) + 1;

		*(strchr(&buf[b], ',')) = 0;
		strcpy(info_data[a]._st_ID, &buf[b]);
		b += (strlen(&buf[b]) + 1);
		
		strcpy(info_data[a]._st_URL, &buf[b]);
	}	
	fclose(fp);
}

void init_spiffs(void)
{
    esp_vfs_spiffs_conf_t conf = {
      .base_path = "/spiffs",
      .partition_label = NULL,
      .max_files = 5,
      .format_if_mount_failed = false
    };

    // Use settings defined above to initialize and mount SPIFFS filesystem.
    // Note: esp_vfs_spiffs_register is an all-in-one convenience function.
    esp_vfs_spiffs_register(&conf);

}

httpd_handle_t start_webserver(void)
{

    httpd_handle_t server = NULL;
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.lru_purge_enable = true;
    config.uri_match_fn = httpd_uri_match_wildcard;

    // Start the httpd server
    ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port);
    if (httpd_start(&server, &config) != ESP_OK) return NULL;

    // Set URI handlers
	httpd_uri_t hello = {
        .uri       = "/*",  // Match all URIs of type /path/to/file
    	.method    = HTTP_GET,
    	.handler   = get_handler,
    	.user_ctx  = "Hello World!"
	};
    ESP_LOGI(TAG, "Registering URI handlers");
    httpd_register_uri_handler(server, &hello);

   return server;
}

void stop_webserver(httpd_handle_t server)
{
    // Stop the httpd server
    httpd_stop(server);
}

void disconnect_handler(void* arg, esp_event_base_t event_base,
                               int32_t event_id, void* event_data)
{
    httpd_handle_t* server = (httpd_handle_t*) arg;
    if (*server) {
        ESP_LOGI(TAG, "Stopping webserver");
        stop_webserver(*server);
        *server = NULL;
    }
}

// FreeRTOS event group to signal when we are connected
static EventGroupHandle_t s_wifi_event_group;

// The event group allows multiple bits for each event, but we only care about two events:
// - we are connected to the AP with an IP
// - we failed to connect after the maximum amount of retries 
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT      BIT1
#define EXAMPLE_ESP_MAXIMUM_RETRY  10

static int s_retry_num = 0;

static void event_handler(void* arg, esp_event_base_t event_base,
                                int32_t event_id, void* event_data)
{
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) 
    {
        esp_wifi_connect();
    } 
    else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) 
    {
        if (s_retry_num < EXAMPLE_ESP_MAXIMUM_RETRY) 
        {
            esp_wifi_connect();
            s_retry_num++;
            ESP_LOGI(TAG, "retry to connect to the AP");
        } 
        else 
        {
            xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
        }
        ESP_LOGI(TAG,"connect to the AP fail");
    } 
    else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) 
    {
        ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
        ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
        s_retry_num = 0;
        xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
    }
}

esp_err_t wifi_init_sta(void)
{
    esp_event_handler_instance_t instance_any_id;
    esp_event_handler_instance_t instance_got_ip;
    
    s_wifi_event_group = xEventGroupCreate();
    esp_netif_create_default_wifi_sta();
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
                                                        ESP_EVENT_ANY_ID,
                                                        &event_handler,
                                                        NULL,
                                                        &instance_any_id));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
                                                        IP_EVENT_STA_GOT_IP,
                                                        &event_handler,
                                                        NULL,
                                                        &instance_got_ip));
	
    wifi_config_t wifi_config = {
        .sta = {
            .ssid = CONFIG_ESP_WIFI_SSID,
            .password = CONFIG_ESP_WIFI_PASSWORD,
	     	.threshold.authmode = WIFI_AUTH_WPA2_PSK,
        },
    };
    
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) );
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config) );
    ESP_ERROR_CHECK(esp_wifi_start() );

    ESP_LOGI(TAG, "wifi_init_sta finished.");

//  Waiting until either the connection is established (WIFI_CONNECTED_BIT) or connection failed for the maximum
//  number of re-tries (WIFI_FAIL_BIT). The bits are set by event_handler() (see above)
    EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
            WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
            pdFALSE,
            pdFALSE,
            portMAX_DELAY);

//  xEventGroupWaitBits() returns the bits before the call returned, hence we can test which event actually	happened. 
    ESP_ERROR_CHECK(esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, instance_got_ip));
    ESP_ERROR_CHECK(esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, instance_any_id));
    vEventGroupDelete(s_wifi_event_group);
    if (bits & WIFI_CONNECTED_BIT) 
    {
        ESP_LOGI(TAG, "connected to ap SSID:%s password:%s",CONFIG_ESP_WIFI_SSID, CONFIG_ESP_WIFI_PASSWORD);
        return ESP_OK;         
    } 
    else if (bits & WIFI_FAIL_BIT) 
    {
        ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s",CONFIG_ESP_WIFI_SSID, CONFIG_ESP_WIFI_PASSWORD);
    } 
    else 
    {
        ESP_LOGE(TAG, "UNEXPECTED EVENT");
    }
    return ESP_FAIL;
}
//========================================== Main ============================================================
void app_main(void)
{
    esp_err_t err;
    
    ESP_ERROR_CHECK(nvs_flash_init());
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    err = wifi_init_sta();
    if(err) 
    {
        ESP_LOGE(TAG, "WiFi Init failed: %d\n", err);
        return;
    }

   	start_webserver();
    
    //initialize mDNS service
    err = mdns_init();
    if (err) 
    {
        ESP_LOGE(TAG, "MDNS Init failed: %d\n", err);
        return;
    }

    //set hostname
    mdns_hostname_set("my-esp32");
    //set default instance
    mdns_instance_name_set("Jhon's ESP32 Thing");
    
	init_spiffs();
    read_station();
}

クライアントにindex.htmlを送る

全てのGETリクエストは75行のesp_err_t get_handler(httpd_req_t *req)で処理する設定になっています。(詳細は前回を参照)ここから説明を初めます。

  • クライアントが、ブラウザに”my-esp32.local”と入力してサーバーにリクエストすると、リクエストURIが空のアクセスとなります。この場合、サーバーはHPを表示する様設定しています。
  • リクエストURIがからなので86行のif文は実行されず、121行に飛びます。
  • flg_index、flg_state が1にセットされているので、下のswitch文 radio_main(req);が実行されます。この関数がHPを表示する関数です。
  • radio_main(req)の本体は、52行に有ります。この関数はFlashに予め保存されたHTMLファイル ”index.html”をクライアントに送信する関数です。
    • 54行:httpd_resp_set_type(req, “text/html”);
      • これから送るファイルがHTML形式で有ることをセット
    • 55行:send_file(req, “/spiffs/index.html”);この関数が実際にファイルをクライアントに送っています。14行に本体が有ります。
      • 20行:指定されたファイルを開いて
      • 22行:指定文字数を読み込む。
      • 23行:読み込みが有れば
      • 25行:それをクライアントに送信。この様に部分的にクライアントに送信する時は、httpd_resp_send_chunk(req, send_buf, buf_size)関数を使用します。引数は
        • 第一:httpd_req_t型のポインター
        • 第二:送信するバッファーのポインター
        • 第三:送信文字数。
      • 35行:送信するデータがなくなればファイルを閉じて
      • 36行:送信終了をクライアントに知らせる為に、httpd_resp_send_chunk(req, NULL, 0); (NULLを送る)を実行します。
  • これで、クライアントに ”index.html”が送信されました。

クライアントにCSSファイルを送る

  • サーバーから”index.html”を受け取ったクライアントは、”index.html”の中 に href=’./http_server.css’ >(index.html6行目)とCSSファイルの指定が有る為、これをサーバーに要求します。
  • クライアントから要求は全て、esp_err_t get_handler(httpd_req_t *req) 関数が処理するのでこの要求もここで処理されます。
  • ファイルを要求された場合のURIは、要求されたファイルのFull Path(path + Name)となります。
  • よって、86行のif文を満たし、88行のif文でURIに”?”が無いので114行のelse以下を実行します。
  • 116行:down_load_file(req);が要求されたファイルをクライアントに送る関数です。
  • 60行:関数本体です。
    • 64から68行で送信するファイルの種類をチェックしています。
      • クライアントにファイルを送る前に送信するファイルの種類を設定する必要が有ります。
      • URIの拡張子の部分をチェックして “httpd_resp_set_type()” 関数で指定しています。
      • 今回は、”.css”, “.html”, “.js”, “.jpeg”, “.txt” ファイルに対応しています。
    • 69,70行:指定されたファイルのflashでのPathを作成して
    • 71行:send_file(req, name_buf);を使って実際にクライアントに送信します。
  • 送信が済むとプログラムは本体117行に戻ります。
  • クライアントからの要求はファイルのみでHPを送信する必要が無いので、flg_index 変数を0にセットします。
  • クライアントは、HTMLとCSSファイルを元にホームページを表示します。

ボタンの処理

  • 例えばこのホームページで”Set WiFi”ボタンをクリックすると、サーバーに、GETで”?1=0”が送られます(index.htnlの14行参照)。
  • これを受けて76行:esp_err_t get_handler(httpd_req_t *req)が起動。
    • 87行:ここでURI(?1=0)が有るので、89行を実行
    • 89行:URIに、”?”が有るのでIF文以下を実行
    • 92行:queryを一度コピーし
    • 94から97行:値を取り出す。今回は ”1=0” の ”1”。
    • 99行:その値を元に処理を割り振る。
    • 102行:今回はここで処理される。変数 flg_state に2が代入される。
    • 122行を通過し、124行のSwitch文でCase 2:が実行される。
    • 128行:実行される関数は、”wifi_edit(req)”。これはWiFi関係のパラメータを編集する関数
    • 47行:void wifi_edit(httpd_req_t *req) これが本体。今回はまだコーディングしていません。
  • ホームページで”Radio Info”ボタンをクリックすると、サーバーに、GETで”?2=0”が送られます(index.htnlの15行参照)。その後は同様にサーバーで処理され、42行の、”void station_edit(httpd_req_t *req)”が実行されます。

プロジェクトの構造

変更したファイル:main/main.c

追加したファイル:components/spiffs_image/index.html、 http_server.css


- http_sample/
      - CMakeLists.txt
      - partitions_example.csv
      - sdkconfig.defaults
      - main/
                   - CMakeLists.txt
                   - main.c
                   - Kconfig.projbuild
      - components/
                   - spiffs_image/
                              - station.txt
                              - index.html
                              - http_server.css

今回使用した CSSファイル

http_server.css

@charset "UTF-8";

.t_font {
        font-size: 40px;
        font-weight:bold;
        font-style: italic;
        color: #0ff;
}

.b_frame {
        width: 400px;
        background: #363636;
        padding: 15px;
        border-radius: 10px;
        margin-top: -30px;
        margin-right: 10px;
}

.m_button {
        display: block;
        margin: 10px;
        line-height: 50px;
        cursor: pointer;
        color: #fff;
        background: #228b22;
        border-radius: 10px;
        font-size: 24px;
	    width:200px;
}

.m01_button {
        display: inline;
        margin: 10px;
        line-height: 28px;
        cursor: pointer;
        color: #fff;
        background: #228b22;
        border-radius: 10px;
        font-size: 18px;
	    width:80px;
}

.m02_button {
        display: inline;
        margin: 10px;
        line-height: 24px;
        cursor: pointer;
        color: #fff;
        background: #228b22;
        border-radius: 10px;
        font-size: 16px;
	    width:70px;
}
.m02_button:disabled,
.m02_button[disabled]{
  border: 1px solid #999999;
  background-color: #cccccc;
  color: #666666;
}

コンパイル、実行

今回も前回同様、Kconfig.projbuildにWiFISSID,Passwordの情報を予め記入しています。コンパイルはCPUをESP32としているのでモニタから、”idf.py -p [usb port] flash monitor” でOKです。今回のプロジェクトをここに保存します。ただし、void wifi_edit(httpd_req_t *req)、void station_edit(httpd_req_t *req)のコーディングがされていないので、101,105行のflg_stateに1を代入してそれらの関数が実行されない様に変更しています。

確認

実行後ブラウザを上げて、”my-esp32.local”と入力して下さい。この様な画面が表示されます。

ここで、”Set WiFi”ボタンをクリックする、ブラウザのURL欄に、/?1=0 が表示されます

今回はここまでです。

次回は

void wifi_edit(httpd_req_t *req)、void station_edit(httpd_req_t *req)の2つの関数のコーディングを行います。