ESP32 で Web サーバを立てて,Angular アプリケーションを動かす方法を紹介します.
はじめに
ESP32 に Web API を実装する場合,簡易的な Web UI もつけたくなったりしますが,まとまった資料がなかったので紹介します.
Angular 側で準備を行った後,ESP32 へ組み込んでいきます.
Angular 側の準備
Angular 側でやることは,大きく次の2つがあります.順に説明していきます.
- ファイル名のランダム化の停止
- ファイルの圧縮と配置パスの指定
ファイル名のランダム化の停止
ng build --prod
で普通に Angular アプリをビルドすると,下記のようにファイル名がランダム化されています.ファイル名が一意に決まらないと ESP32 への組み込みに不便なので,この機能を停止させます.
1 2 3 4 5 |
chunk {0} runtime.a5dd35324ddfd942bef1.js (runtime) 1.41 kB [entry] [rendered] chunk {1} main.a7e2bc867568886f94ea.js (main) 339 kB [initial] [rendered] chunk {2} polyfills.46532d96d3286697c138.js (polyfills) 41 kB [initial] [rendered] chunk {3} styles.9979e02a4f596e73f5b9.css (styles) 144 kB [initial] [rendered] chunk {scripts} scripts.201cd8c2534507b4525e.js (scripts) 140 kB [entry] [rendered] |
angular.json の中の "outputHashing": "none",
を "outputHashing": "none",
に変更すれば OK です.
ファイルの圧縮と配置パスの指定
Angular アプリはシンプルなものでもサイズがそれなりに大きくなってしまいます.小さなアプリであればそのままでも収まるかもしれませんが,ESP32 の Flash サイズは潤沢ではないので,gzip で圧縮して配信しておくのがおすすめです.
そのため,下記のような Makefile を使って圧縮されたファイルを生成するようにします.「esp32-wifi-io」は,お使いの Angular アプリケーション名で置き換えてください.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
DIST_PATH = ./dist/esp32-wifi-io DIST_FILES = runtime.js main.js polyfills.js scripts.js styles.css all: build $(addsuffix .gz,$(addprefix $(DIST_PATH)/,$(DIST_FILES))) @echo "*BEFORE" @du -shc $(addprefix $(DIST_PATH)/,$(DIST_FILES)) @echo "*AFTER" @du -shc $(DIST_PATH)/*.gz build: ng build --prod --base-href=/app/ %.gz : % gzip -c --best $< > $@ .SUFFIXES: .gz .PHONY: all build |
ng build
を実行する際に,アプリを配置するパスを指定しています.Angular アプリを ESP32 で配信する場合,おそらく API サーバも ESP32 で動かすことになると思います.ルートではなく /app にアプリを配置し,API のエンドポイントを /api に配置すると,配信の際の扱いがしやすくなります.
実行すると下記のようになり,676KB あったものが約 1/4 の 176KB になっていることが分かります.これなら,ROM サイズ(4MB)に対してかなり小さいので,サイズを気にせずにアプリ開発が行えます.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
ng build --prod --base-href=/app/ Date: 2019-02-24T13:08:07.699Z Hash: 2278494bf89b363ec473 Time: 16931ms chunk {0} runtime.js (runtime) 1.41 kB [entry] [rendered] chunk {1} main.js (main) 339 kB [initial] [rendered] chunk {2} polyfills.js (polyfills) 41 kB [initial] [rendered] chunk {3} styles.css (styles) 144 kB [initial] [rendered] chunk {scripts} scripts.js (scripts) 140 kB [entry] [rendered] gzip -c --best dist/esp32-wifi-io/runtime.js > dist/esp32-wifi-io/runtime.js.gz gzip -c --best dist/esp32-wifi-io/main.js > dist/esp32-wifi-io/main.js.gz gzip -c --best dist/esp32-wifi-io/polyfills.js > dist/esp32-wifi-io/polyfills.js.gz gzip -c --best dist/esp32-wifi-io/scripts.js > dist/esp32-wifi-io/scripts.js.gz gzip -c --best dist/esp32-wifi-io/styles.css > dist/esp32-wifi-io/styles.css.gz *BEFORE 4.0K ./dist/esp32-wifi-io/runtime.js 340K ./dist/esp32-wifi-io/main.js 44K ./dist/esp32-wifi-io/polyfills.js 140K ./dist/esp32-wifi-io/scripts.js 148K ./dist/esp32-wifi-io/styles.css 676K total *AFTER 88K ./dist/esp32-wifi-io/main.js.gz 16K ./dist/esp32-wifi-io/polyfills.js.gz 4.0K ./dist/esp32-wifi-io/runtime.js.gz 44K ./dist/esp32-wifi-io/scripts.js.gz 24K ./dist/esp32-wifi-io/styles.css.gz 176K total |
ESP32 への組み込み
以上で準備したファイルを ESP32 から配信できるようにします.ESP-IDF の使用を前提に,順に説明していきます.
- ファームへのファイルの組み込み
- サーバによるファイルの配信
ファームへのファイルの組み込み
main/component.mk に次のように記載します.これにより,生成されるファイルに Angular アプリケーションが組み込まれるようになります.
「../angular/dist/esp32-wifi-io/」は,生成した Angular アプリケーションの相対パスで置き換えてください.
1 2 3 4 5 6 |
COMPONENT_EMBED_FILES += ../angular/dist/esp32-wifi-io/index.html COMPONENT_EMBED_FILES += ../angular/dist/esp32-wifi-io/runtime.js.gz COMPONENT_EMBED_FILES += ../angular/dist/esp32-wifi-io/main.js.gz COMPONENT_EMBED_FILES += ../angular/dist/esp32-wifi-io/polyfills.js.gz COMPONENT_EMBED_FILES += ../angular/dist/esp32-wifi-io/scripts.js.gz COMPONENT_EMBED_FILES += ../angular/dist/esp32-wifi-io/styles.css.gz |
サーバによるファイルの配信
まずは全体像を示します.下記のコードで Angular アプリを配信できるようになります.
APP_PATH
は,Angular アプリをビルドしたときの --base-href=
の指定と合わせておきます.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
#include <string.h> #include "app_log.h" #include "http_task.h" #define ARRAY_SIZE_OF(a) (sizeof(a) / sizeof(a[0])) #define APP_PATH "/app" extern const unsigned char index_html_start[] asm("_binary_index_html_start"); extern const unsigned char index_html_end[] asm("_binary_index_html_end"); extern const unsigned char runtime_js_start[] asm("_binary_runtime_js_gz_start"); extern const unsigned char runtime_js_end[] asm("_binary_runtime_js_gz_end"); extern const unsigned char main_js_start[] asm("_binary_main_js_gz_start"); extern const unsigned char main_js_end[] asm("_binary_main_js_gz_end"); extern const unsigned char polyfills_js_start[] asm("_binary_polyfills_js_gz_start"); extern const unsigned char polyfills_js_end[] asm("_binary_polyfills_js_gz_end"); extern const unsigned char scripts_js_start[] asm("_binary_scripts_js_gz_start"); extern const unsigned char scripts_js_end[] asm("_binary_scripts_js_gz_end"); extern const unsigned char styles_css_start[] asm("_binary_styles_css_gz_start"); extern const unsigned char styles_css_end[] asm("_binary_styles_css_gz_end"); typedef struct static_content { const char *path; const unsigned char *data_start; const unsigned char *data_end; const char *content_type; bool is_gzip; } static_content_t; static_content_t content_list[] = { { "index.htm", index_html_start, index_html_end, "text/html", false, }, { "runtime.js", runtime_js_start, runtime_js_end, "text/javascript", true, }, { "main.js", main_js_start, main_js_end, "text/javascript", true, }, { "polyfills.js", polyfills_js_start, polyfills_js_end, "text/javascript", true, }, { "scripts.js", scripts_js_start, scripts_js_end, "text/javascript", true, }, { "styles.css", styles_css_start, styles_css_end, "text/css", true, }, }; static esp_err_t http_handle_app(httpd_req_t *req) { static_content_t *content = NULL; for (uint32_t i = 0; i < ARRAY_SIZE_OF(content_list); i++) { if (strstr(req->uri, content_list[i].path) != NULL) { content = &(content_list[i]); } } if (content == NULL) { content = &(content_list[0]); } ESP_ERROR_CHECK(httpd_resp_set_type(req, content->content_type)); if (content->is_gzip) { ESP_ERROR_CHECK(httpd_resp_set_hdr(req, "Content-Encoding", "gzip")); } ESP_ERROR_CHECK(httpd_resp_send(req, (const char *)content->data_start, content->data_end - content->data_start)); return ESP_OK; } static esp_err_t http_handle_app_redirect(httpd_req_t *req) { ESP_ERROR_CHECK(httpd_resp_set_status(req, "301 Moved Permanently")); ESP_ERROR_CHECK(httpd_resp_set_hdr(req, "Location", APP_PATH "/")); ESP_ERROR_CHECK(httpd_resp_sendstr(req, NULL)); return ESP_OK; } httpd_uri_t http_uri_app = { .uri = APP_PATH "*", .method = HTTP_GET, .handler = http_handle_app, .user_ctx = NULL }; httpd_uri_t http_uri_app_redirect = { .uri = "/", .method = HTTP_GET, .handler = http_handle_app_redirect, .user_ctx = NULL }; httpd_handle_t http_task_start(void) { httpd_handle_t server = NULL; httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.uri_match_fn = httpd_uri_match_wildcard; ESP_ERROR_CHECK(httpd_start(&server, &config)); ESP_ERROR_CHECK(httpd_register_uri_handler(server, &http_uri_app)); ESP_ERROR_CHECK(httpd_register_uri_handler(server, &http_uri_app_redirect)); return server; } void http_task_stop(httpd_handle_t server) { ESP_ERROR_CHECK(httpd_stop(server)); } |
コードのポイントを順に説明します.
ファイルデータの取得
COMPONENT_EMBED_FILES
で指定して組み込んだファイルデータの開始・終了アドレスは,次のようにして取得できます.
1 2 |
extern const unsigned char index_html_start[] asm("_binary_index_html_start"); extern const unsigned char index_html_end[] asm("_binary_index_html_end"); |
アドレスさえ取得できれば,あとは httpd_resp_send_chunk
にて送信データとして指定してやれば配信できます.
gzip ファイルの配信
上のほうで HTML 以外は gzip で圧縮しました.そのため,次のようにして,圧縮されていることをブラウザーに通知します.
1 |
httpd_resp_set_hdr(req, "Content-Encoding", "gzip") |
パスのワイルドカード指定
ESP32 の HTTP Server ライブラリでは,パスに対応するハンドラ関数を指定してやる必要がありますが,Angular のように複数のファイルがある場合,ファイル毎に関数を用意するのは手間です.
そこで,ワイルドカードによる指定を行えるようにするため,下記のようにします.
1 |
config.uri_match_fn = httpd_uri_match_wildcard; |
その上で,次のようにすると,/app 以下の全てのアクセスに対して http_handle_app
が呼ばれるようになります.(下記では APP_PATH
マクロを展開しています)
1 2 3 4 5 6 |
httpd_uri_t http_uri_app = { .uri = "/app*", .method = HTTP_GET, .handler = http_handle_app, .user_ctx = NULL }; |
あとは,http_handle_app
の中で,req->uri を基にどのファイルへのアクセスか判定して,適切なファイルを配信してやれば OK.
動作確認
Wifi 接続が完了したタイミングで次の関数を呼んでおけば,Angular アプリを実行できるようになっています.
1 |
http_task_start(); |
コメント