主題の目的に向かって調べ物をしていた時、Ethernetライブラリのサンプルの中に、「WebServer」というプログラムを見つけました。Arduino IDEを起動して「ファイルメニュー ⇒ スケッチ例 ⇒ Ethernet ⇒ WebServer」で、開くことができます。
100行余りのプログラムで、さすがに短すぎる……。たったの100行なので、ざっと流し読んだだけで、大方の動作は理解できました。「クライアントからリクエストがあったら、メソッドもURIも無視して、兎に角、作り込まれたHTMLデータを送信して終了」、と言う物。
Arduinoアナログ端子の状態を読み取って、それをHTMLデータに埋め込んで返すみたいなので、何かしらアナログセンサの情報を、HPデータとして送信することができるみたいです。
でも、「これぢゃない」……(1^^;)。
ちゃんと、階層化されたHPデータを持っておいて、クライアント(Webブラウザソフトウェア)からの要求に応じて、データを送信するHTTPサーバを立てたいの♪
HPデータの保持には、「#6-5 microSDカードI/Fを自作する」で自作した、SDカードI/Fを使いたいと考えています。SDカードI/Fについては、市販のSDカードI/Fモジュールでも代用できます。
実は、使用するモジュールの選択をミスして、1からやり直しました。こんな失敗談をして、何の得になるのか?ってことですが、皆さんも遭遇しかねない問題なので、簡単に説明させてください。
本家Arduinoに「Ethernetシールド」っていうものがあって、これを使うと、有線Ethernet接続とSDカードが使えるシールドとなっています。手元にはEthernetシールドは無いのですが、以下表の様に、同等の環境を構築できていました。
Ethernetシールド | 手元の環境 | 備考 | |
Ethernet制御チップ | W5100 | ENC28J60 | ENC28J60にはMACアドレスがついていない。 |
Ethernet制御ライブラリ | Ethernet.h | UIPEthernet.h | 本家Ethernet.hと同等のAPIが揃っている。 |
SDカードI/F | シールド上に実装 | 自作(※) | ※「#6-5 microSDカードI/Fを自作する」参照 |
SDカードライブラリ | SD.h | ← |
何が良いと言って、本家の標準ライブラリ(Ethernet.h)と、ほぼ同じAPIを持っていることで、本家Ethernetライブラリのサンプルコードを参考にできるし、ここで作成したコード(UIPEthernetライブラリ使用)を、逆にEthernetシールド(Ethernetライブラリ)へ逆輸入することもできるので、応用が広がるのではないか?と、思ってしまったのです。
そうなんです……。何をとち狂ったか、ENC28J60を使おうとしてしまった。「#4-7 ENC28J60を使う」の時に、UIPEthernetライブラリの「メモリ喰らい」という問題点を自分で指摘していたのに。
今回は、ここに上乗せしてSDライブラリがまたメモリを喰らう。「SD.h」と「UIPEthernet.h」を#includeした時点で、すでにUNOのRAM(2Kバイトしかない)は、カツカツの状態でした。「まずいな……」と、思いながらも、HTTPサーバのコーディングを進めたところ、途中まで実装した段階で、RAMが底をついてしまいました。
あきらめ気味に、ターゲットをMicro(ATmega32U4なので、RAMは2.5Kバイト持っていて、UNOより512バイトほど多い)へ切り替えてコンパイルしたところ、期待通り、RAMには余裕ができたのですが、今度はFlashが溢れました。……USB周りのファームウェアが思いの他、大きかった様です……。
これで万事休す。上表の構成で、ATmega328やATmega32U4を使って、HTTPサーバを実装するのは断念しました。Ethernetライブラリのサンプル(WebServer)程度の実装が関の山みたいです。
今回は、UNO+ENC28J60+UIPEthernetライブラリ+SDライブラリでメモリ不足になってしまったのですが、恐らく、本家のUNO+Etherシールド+Ethernetライブラリ+SDライブラリでも、同様な状況なのではないか?と予想します。
ATmega系で実装するなら、ATmega2560(Flash 256Kバイト、RAM 8Kバイト)を搭載した、Arduino MEGA等の機体が必要でしょう。手元に無いので未確認ですが……。
手元の機体だと、STM32ボードが良い感じではありますね。STM32F103は、Flash 64Kバイト、RAM 20Kバイトと、少し余裕があります。個人的にはThumb ARMが好みではないので、あまり使う気はしませんし、SDライブラリがSTM32で使用できるかどうかも未確認です。UIPEthernetライブラリは、STM32対応みたいです。
※そもそも、STM32は、Logiscope専用機として稼働しているので、あまりアプリケーション用に使いたくないというのが本音(デバッグに支障をきたす!)。
冷静に考えてみると、何も、MACアドレスすらついていない様なモジュール(ENC28J60)を無理して使って、HTTPサーバを稼働させなくても良かったのですよ。筆者には、本命のESP32がありました(「#4-10 ArduinoとESP32の非同期シリアル通信」を参照)。
ESP32なら、MACアドレスもついているし、何より、UIPEthernetライブラリに相当する部分のコードとバッファメモリがESP32側に押し出されているので、Arduino UNO側はずいぶんと軽く済むはずです。
しかも、無線LAN。
ESP32とは、非同期シリアル(UART)接続になるので、「#4-10 ArduinoとESP32の非同期シリアル通信」の時には、ハードウェアシリアルが使用可能な、MicroまたはPro Miniを使いました。その後、「やっぱりUNOでもESP32を使いたい」と思い、自分でソフトウェアシリアル(HsswSerial)ライブラリを作成してしまいました。
「#5-1 高速ソフトウェアシリアル(前編)」
「#5-2 高速ソフトウェアシリアル(後編)」
「#5-3 高速ソフトウェアシリアル(続編)」
たくさん出回っている、他のソフトウェアシリアルでも良かったのだけれど、ESP32とは、115,200baudの高速通信を実現したかったので、自分で起こしました。上のリンクから、今回関係しそうなところを要約すると、以下の通りです。
今回は、SDライブラリを使うことにより、SPIユニットを使用することになります。もし、SPIの制御に割り込みを使っていると、HsswSerialの割り込みと競合して、HsswSerialの受信を失敗するケースが出てくるかもしれません。
SDカードアクセス中にSPIユニットのレジスタ(SPCRのビット7、SPIE)を確認してみたところ、SDライブラリでは、SPIの割り込みは使用していない様です。これで、割り込み競合に掛かる心配はクリアになりました。
と、そもそも、HsswSerialが通信に使用する端子は、元々、SPIの通信用端子と重複しており、同時使用不可でした(以下表)。
Arduino UNO | HsswSerial | 競合 | SPI | 備考 |
D8 | RXD | − |
TC1インプットキャプチャを使用するため、 変更不可。ただし、SPIとの競合無し。 |
|
D9 | TXD | − | ||
D10 | RTS | × | SS | SPIと競合、同時使用不可。 |
D11 | CTS | × | MOSI | SPIと競合、同時使用不可。 |
D12 | − | MISO | ||
D13 | − | SCK |
競合していたのは、D10とD11の2本でした。そこで、先日、HsswSerialをアップデートして、競合を回避できるように改造(改定)しておきました。現在公開しているバージョンでは、RTSとCTSを任意のデジタル端子から選択可能になっています。詳しくはこちらから。
HsswSerialのアップデートにより、SPIとの端子の競合も無くなりました。これで、HsswSerialクラスライブラリとSD(+SPI)ライブラリが共存できます。
ESP32は、起動すると自動的にAPに接続してIPアドレスを取得する様に設定しておいてください。関係しそうなATコマンドは以下の通りです。
その他の細かな設定値は、「工場出荷時のデフォルト」を期待しています。「#4-5 WiFiインターネット通信」等も、ご覧ください。
そうでない場合は、APへの接続や、IPアドレスの獲得などもArduino側のプログラム(setup()関数内のESP32初期化部分)に記述する必要があります。
ENC28J60を使ってコーディングし始めた時は、UIPEthernetライブラリを使っていました。ESP32では、非同期シリアル通信でATコマンドを叩きながら処理を投げることになるので、UIPEthernetライブラリとは異なるコードとなります。途中まで組み始めたHTTPサーバのコードは1から作り直しとなりました。
まあ、コーディングより、RFCを読み解いてアルゴリズムに落とし込むところが一番大変な作業になるので、その部分をそのまま持ち越せるだけでだいぶ楽でしたけど……。最初にもう少し良く検討を積んでおくべきでしたね。最初からESP32を使っていたら、要らない工数でした。
周辺のSDカードI/Fと、ESP32の接続回路は、以前に示した回路図で足りるはずです。再掲載しておきます。
まずは、ESP32の接続回路図です(下図)。UNOとの接続は、スラッシュ(/)記号の右側の端子を使います。
次に、SDカードI/Fの接続回路図です(下図)。
HTTPサーバを立てようとするので、HTTPの仕様について知っておく必要があります。
以前、Arduino+ESP32でHTTPクライアント側になって、インターネットからHTMLデータをダウンロードしたことがありました(「#4-5 WiFiインターネット通信」)。この時に登場した、IETF(Internet Engineering Task Force)という組織の、RFC(Request For Comments)というドキュメントが今回も大活躍です。
RFCは随時アップデートされていくので、最新のドキュメントを参照してください。検索して、IETFのRFCに辿り着いたら、ヘッダ部分を確認してください。ヘッダに「Obsoleted by: xxxx」がついていると、「このドキュメントはRFC-xxxxで上書きされた」と、いう事らしいです。新しいRFC-xxxxの方を参照する必要があります。
本ページ執筆時点では、RFC-7230〜7235辺りがHTTP関係の様です。
HTTPのRFCドキュメントを検索する(別ウインドウが開きます)
今回の簡易サーバでは、色々と機能を端折っているので、筆者が参照したRFCは、以下の通りです。
上の3つだけですが、斜め読みして分かったことは、RFCの要求事項が兎に角多くて、しかも細かい。まじめに対応しようとすると、とてつもない労力がかかりそう、と言うこと。労力だけの問題では済まされず、Arduinoという、メモリもMCUパワーも限られた環境で実現するには、相当割り切って、端折らないと実現できないと考えました。
と、腹を括れたところで、目標仕様をまとめておきましょう。
以下を目標仕様とします。
特に、SDカード上のファイルについては、細心の注意を払ってください。今回のHTTPサーバを立てると、HTTPを通して、SDカード上の全てのファイルにアクセス可能です。「全て」です。HTTP_ROOTにSDカードのルートディレクトリ以外を指定した場合でも、URIに"../"等の相対パスを指定することで、SDカード上のすべてのファイルにアクセス可能となります。
アクセスされては困るデータは、絶対にSDカード上に置かないようにしてください。アタックされて、データが流出する恐れがあります。
プログラムにセキュリティコードを追加するなり、SDカード上に「参照されてはまずいファイル」を置かないようにするなり、対策は使用者各位の責任において実施してください。
まだ、ここからプログラムのチューニングの話を進めるのですが、皆様へは、完成版プログラムをご覧に入れておきます。
完成版プログラム:http_server_esp.ino
以下、実験中の様子と、サンプルデータへのリンクです。
サンプルデータ:HTTP_TOP
HTTP_TOP/
AR62OAMP.JPG 11289
BACK1YEN.PNG 886
FAVICON.ICO 3638
INDEX.HTM 623
ARDUINO/
INDEX.HTM 427
TITLE.PNG 32160
SUBDIR/
INDEX.HTM 460
サンプルデータは、実験に使うSDカードにコピーしたのと同じディレクトリ構造を、このページ内に再現しました。上のリンクから、お手本(期待値)をご覧いただけます。
プログラムをコンパイル&Arduino UNOへ書き込んで、ESP32のIPアドレスを調べてください。USE_SERIALを#defineしておいた場合は、シリアルモニタにIPアドレスが表示されるはずです。
ESP32: AT+CIPSTA?
ESP32: +CIPSTA:ip:"192.168.0.102"
ESP32: +CIPSTA:gateway:"192.168.0.1"
ESP32: +CIPSTA:netmask:"255.255.255.0"
ESP32: OK
以下、ESP32のIPアドレスが、「192.168.0.102」だったと仮定して、サンプルを示しますので、皆さんの環境で表示されたIPアドレスに置き換えて進めてください。
次は、PC上で、適切なブラウザを起動して、アドレスバーに、以下の様に打ち込みます。
http://192.168.0.102
「"http://"<IPアドレス>」と打ち込んでENTERです。
SDカードにコピーしておいた、「HTTP_TOP/」ディレクトリ以下に配置した、index.htmがブラウザに表示されるはずです。模範解答はこちら(HTTP_TOP)。
そうか、皆様には、既に完成版プログラムをご覧に入れてあるのでしたね。表示には時間がかかりますが、最後には、模範解答が表示されたのではないでしょうか?……時々失敗するので、その時は「再読み込み」してみてください。
最初に実験した時には、背景のイメージ(HTTP_TOP/back1yen.png)は表示されたのですが、ページ中に挿入したイメージ(HTTP_TOP/ar62oamp.jpg)は、×マークが出て、表示されませんでした(上図)。
実は、最初は、複数接続を受け付けようにも、リクエストを取り込むバッファメモリが足りなかったため、接続数を1に限定(esp_max_conn=1)して試行していました。DBG_SERIALも#defineしておいて、シリアルモニタに送られてくるデバッグ情報をモニタした結果、どうやら、2つ目の接続要求を拒否しているため、HTMLから参照している一部のイメージデータが表示されていないことが分かりました。
まともにHTTPサーバとして機能させるためには、複数接続に対応できる必要がある様です。しかし、困ったことに、全くメモリ(RAM)が足りません。複数接続を容認するにしても、クライアントからのリクエストを保持(受信)しきれない様では、どうにもなりません。
「仮想メモリみたいなの、使えないのかな?」……と、悩んでいると、有るじゃないですか!2Gバイトも。クライアント(Webブラウザ)から受け取ったリクエストデータを、一旦SDカード上の一時ファイルに受信してしまえば、ArduinoのRAMを消費せずに処理が進められます。
ESP32は、最大で5つの接続を確立できる仕様なので、一時ファイルに<ESP_IPD_TFILE><link ID>".tmp"と言う名前を使うことに決めました。<ESP_IPD_TFILE>は、#defineでコンパイル時に指定できるようにしておきます。<link ID>は、ESP32の接続毎に、0〜4の数字が割り当てられて、ESP32から通知されます。合わせて8文字以内になるように、<ESP_IPD_TFILE>を設定する必要があります。
HTTPサーバ処理を開始する条件は、上の一時ファイルが存在するかどうか?とします。上の一時ファイルは、最大で5つ見つかるので、見つかった順に処理していくことにしました。処理が終わった一時ファイルは、削除することに決めておくと、一度処理したファイルを複数回処理してしまう恐れはありません。
準備とコーディングが整ったので、esp_max_conn=5に設定して、コンパイル&書き込みます。
もう一度、Webブラウザから「"http://"<IPアドレス>」と打ち込んでみます。さっき、×印が出て、表示されなかったイメージ(HTTP_TOP/ar62oamp.jpg)も、表示されました。
良い感じです。しかし、シリアルモニタに送られてきた、デバッグ情報を良く眺めてみると、結局、Webブラウザからは、最大2つの接続しかして来ていない様子。たぶん、Webブラウザによって差が出るところだろうけど……。
サーバ側としては、パワー不足気味のArduinoによるHTTPサーバに、ガンガン接続して来られても対処しきれないので、返って都合は良いです。
そもそも、世の中のサーバは、世界中から、非同期かつ同時にリクエストを受けるのだから、高々1つのクライアントがそんなにいくつも接続してきては、たまらないですよね?→Webブラウザ側も、そんなにたくさん同時並行でサーバに繋ぐようなことはしていないのかもしれないですね(と、予想)。
1つ目の接続がクローズされるタイミングと、続けて届く別なリクエストのタイミングが微妙に重なった時だけ、同時に2つの接続を受けてしまうみたいです。
ということで、少しイジメてみましょう。2つのWebブラウザ(筆者の場合はIEとEdge)を起動して、ほぼ同時にリクエスト「http://192.168.0.102」を投げてみます。
結果、……、どうもうまくないですね。一部が表示できなかったり、Webブラウザが受信待ち状態で停止してしまったり。
シリアルモニタのデバッグログを眺めてみても、そもそもESP32が、Webブラウザから飛んでくるリクエストを受信できていないように見えます。……ESP32が受信してくれないようだと、こちら(Arduino)側では、どうしようもありません。
うまくESP32との連携が取れていないせいで、ESP32が受け取ったリクエストを、Arduinoが吸い出せていないのか?はたまた、ESP32が取りこぼしているのか?ちょっと、追いきれませんでした。Webブラウザから再読み込みをかけると、今度はリクエストを受信できて、ちゃんと表示されたりするので、基本的なところは動作している様なのですが……、安定しません。
「全く動かない」って言うわけでは無いので、微妙〜、ですf(1^−^;)。
更にその上、遅すぎます。例えば、サンプルページ内から参照しているイメージ(HTTP_TOP/ar62oamp.jpg)ですが、以下の様な小さなJPEGイメージです。
たったの、160×98[ドット]、11,289バイトなのですが、Arduino⇒ESP32の非同期シリアル通信は、「#5-3 高速ソフトウェアシリアル(続編)」の計測結果によると、115,200baudで4.7Kバイト/sですので、11,289バイトのJPEGデータを転送するのに約2.3秒程度かかってしまいます。Webブラウザ側では、データを受信し終わるまで通信待ち状態が続きますので、モッサリとした遅さを感じてしまう訳です。
※USE_SERIAL/DBG_SERIALの#define状況によっては、シリアルモニタへのデバッグ情報出力も、そこそこ時間がかかっているし、その他処理のオーバヘッドもあるので、実際には上の計算(〜2.3s)より、多く時間がかかります。
このスピードと安定度では、HPを世界に公開するのはちょっと無理ですね。
インターネットには繋がるので、世界中のどこからでも、ネットワーク環境さえあれば、このHTTPサーバに接続できるのは強みなのですが……(距離の制約から逃れることができる)。
自分だけがアクセスできるHTTPサーバだったら、使い道あるかな?リモートでArduinoを設置した場所のセンサ測定値などを確認するだけのサーバとか。
その場合は、他の一般の人間からは分からない(アクセスできない)様に、HTTPの標準である、ポート名"80"以外でサーバを立てておくのが良いかもしれません。#define HTTP_PORTの設定で変更可能です。
例えば、HTTP_PORT="8080"なら、アクセスするときは、「http://<IPアドレス>:8080」とすればアクセス可能です。そうだと知らない人は、わざわざ「:8080」をつけてアクセスしようとはしないだろうから、自分だけがアクセス可能なHPにすることができます。
※ただし、攻撃者(アタッカ)は除きます。ポートスキャンなどでアタックされると、アクセスできてしまうので、アタックに対するセキュリティ対策は各自で実施してください。
そう考えて行くと、インターネットにつなぐのは大げさかもしれませんね。家庭内LANならどうでしょうか?
家庭内LANにぶら下がると、家庭内のどこからでも、Arduinoに接続できることが利点になるはずです。リビングに居ながらにして、寝室の室温をモニタするとか。何なら、IRリモコンの送信機能も持たせておいて、リビングから寝室のエアコンを操作するとか。
※IRリモコンの送信については「#4-3 赤外線リモコン(送信)」を参照してください。
USE_SERIAL/DBG_SERIALでシリアルモニタにデバッグ情報を出力するのも、そこそこ時間がかかっていますので、デバッグと、動作の確認が済んだら、両方とも#defineをコメントしてコンパイルすることを推奨します。
兎に角、メモリ(RAM)がカツカツの状態です。メモリ(RAM)節約のため、バッファメモリを2つ(lbufとfbuf)、staticに確保しておいて、これを、ありとあらゆる場所で共有して使い回してしまいました。
メモリ削減のためには仕方なかったのですが、お陰で、パズルと言うか、暗号と言うか、中々神経質なコードになってしまいました。実際、コーディング中にバッファメモリを競合させてしまって、あらぬ動きをさせてしまったことは一度や二度では済みません。
下手に改変すると、バッファメモリが競合して、妙な動きを始めるかもしれません。たぶん、数か月後には、筆者自身にも、改変不可能になっていることでしょう(1^^;)。
本当に、メモリが足りなくて、苦肉の策です。パズル化/暗号化する意図はございませんのであしからず。
メモリさえ、潤沢に確保できるなら、用途毎に異なるバッファを定義して使った方が良い事は、言うまでもありません。
それにしても、Arduinoのメモリの少なさと、実行速度では、まともなHTTPサーバは無理かもしれませんね。「作っては見たものの」、この遅さでは、応用が利きません。