ESP8266(ESP-WROOM-02)を最近よく使っています。
ESP8266を使ったセンサを、Wi-Fiアクセスポイントから遠い、直接電波の届きにくい場所に置きたかったため、ESP8266をもう一つ使ってWi-Fiの到達範囲を拡張する中継機にした(といってもブリッジではなくNAPTルータ)という話です。
とその前に軽くESP8266について紹介しておくと…(もう知ってる方は読み飛ばしてください)
ESP8266は、自身がWi-FiのアクセスポイントになるWIFI_APモード、既設のWi-Fiアクセスポイントに接続するWIFI_STAモードの両方をサポートしています。
しかしこれ以外にWIFI_AP_STAという、他のアクセスポイントに接続しつつ、同時に自分もアクセスポイントとして他のクライアントからのWi-Fi接続を受け付けるということができます。(ちなみにArduino化した直後のデフォルトではこのモードになっているため、明示的にモード指定していないと ESP_xxxxxx というSSIDが発信されています。APのON/OFFやSSID等の設定はフラッシュに不揮発に保存され、前回の値が電源ONに設定されます。)
モードの切り替えは以下のようにWiFi.mode()を使用します。
const char *ssid = "ParentAP";
const char *password = "********";
const char *ap_ssid = "ESPap";
const char *ap_password = "ESPap_password";
...
WiFi.mode(WIFI_AP_STA);
WiFi.softAP(ap_ssid, ap_password);
IPAddress myIP = WiFi.softAPIP();
Serial.print("AP IP address: ");
Serial.println(myIP);
WiFi.begin(ssid, password);
子端末になるESP8266では、上記のap_ssidに対してWiFi.begin()で接続することになります。こうすると以下のような接続状態になります。
親AP(192.168.10.1) <=> (192.168.10.x) ESP8266 (AP: 192.168.4.1) <=> (192.168.4.y)子端末
なお、親と子で別のサブネットのアドレスになっていないと(少なくとも親ネットワーク側からは)通信不能となるのでご注意を。
IPアドレスはDHCPで自動的に設定されます(静的に指定もできます)。
また、ESP8266WiFiMultiを使って複数の接続先SSIDを指定しておけば、接続できる方に自動接続するということも可能です。詳しくはWiFiMultiというサンプルスケッチを見てください。
親APとクライアントに同時接続できるということは、Wi-Fi中継機として使えるのでは?と思うわけですが、そのままでは中継機能(IP_FORWARD)は無効化されており、SDKに含まれるIPスタック(lwip)の設定を変更する必要があります。
参考: ESP8266 lwip IP_FORWARD/routing - ESP8266 Developer Zone
これにより、親APのネットワークとESP8266 APのネットワークの間でパケットが転送されるようになります。しかし、これだけでは親APやそのネットワーク内の各端末に「192.168.4.0/24 にアクセスするには 192.168.10.x をルータとして使う」というルーティング情報を設定しないと、通信が行えません(子端末からのパケットはESP8266がデフォルトゲートウェイになるので設定しなくとも親APのネットワークに出て行けますが、その応答パケットが戻っていくためのルーティング情報を設定しないと応答が得られません)。
この設定を不要にする方法として、普通のルータで使われている方式としてNAT(正確にはNAPT)があります。子端末から親ネットワークに出ていく際にソースアドレスをESP8266のもの(192.168.10.x)に書き換えておき、逆にこれに対する応答パケットが来た際には適切に子端末宛てに書き換えるというものです。(戻り先を特定するためにはセッション管理が必要になります。)
なお他にネットワークブリッジとして機能させることも考えられますが、こちらは色々ルーティングに改造を加えないと難しい気がして手を出していません。
NATを実装する
esp8266のSDKのlwipをざっと眺めたところ、NATの機能はないようでしたので、自分で実装してみました。
変更の内容はGitHubに置いてあります。
https://github.com/NeoCat/esp8266-Arduino/commit/4108c8dbced7769c75bcbb9ed880f1d3f178bcbe
上記のバグ修正コミット
https://github.com/NeoCat/esp8266-Arduino/commit/634dfb5f60e902c681c95712bc7455e24773667d
ボードマネージャ等でESP8266のライブラリを導入している場合は、以下のパッチを当てることで対応できます。
https://gist.github.com/NeoCat/da3c141813980edaa256ad351cab3a2c (バグ修正コミットを含む差分)
上記からRaw(またはDownload ZIP)でパッチを適当な場所に保存し、Macであれば端末から
$ cd ~/Library/Arduino15/packages/esp8266
$ patch -p1 < (保存した場所)/0001-Adds-NAPT-and-port-mapping-functionality-to-esp8266-.patch
とすればパッチを適用できます。適用後、lwipを再コンパイルする必要があります。
$ cd hardware/esp8266/2.3.0/tools
$ ln -s ../../../../tools/xtensa-lx106-elf-gcc/1.20.0-* xtensa-lx106-elf
$ cd sdk/lwip/src
$ make && make release
パッチを適用したら、Arduinoのスケッチ内で以下のようにすることでNATを有効化できます。
#define IP_PROTO_TCP 6
#define IP_PROTO_UDP 17
extern "C" {
void ip_napt_enable(unsigned long addr, int enable);
void ip_portmap_add(byte proto, unsigned long maddr, unsigned short mport,
unsigned long daddr, unsigned short dport);
bool ip_portmap_remove(byte proto, unsigned short mport);
}
...
ip_napt_enable(WiFi.softAPIP(), 1);
これ以降、子端末からのTCP/UDP/ICMP(pingのみ)のパケットは親ネットワークにアドレス変更の上で転送され、応答も対応付けてアドレス変換されます。
なおTCP, UDPの送信元ポート番号(1024以上)も重複を避けるために変換対象になります。
親ネットワークから子端末のTCP/UDPサーバにアクセスするためには、ESP8266の特定ポートを子端末にマッピングする必要があります。このためにはスケッチから、
ip_portmap_add(IP_PROTO_TCP, WiFi.localIP(), 8080,
IPAddress(192,168,4,3), 80);
のように、対象プロトコル(TCPまたはUDP)、ESP8266上のマッピングするポート、転送先となる子端末のIPアドレス、ポートを指定します。
この例では、親ネットワークから 192.168.10.x:8080 にアクセスすると、子端末192.168.4.3の80番ポート(Webサーバ)に接続できます。
ポートマッピングを解除するには、以下のようにします。
ip_portmap_remove(IP_PROTO_TCP, 8080);
なお実装上、1つの宛先(アドレス+ポート)に対しては1つのポートしかマッピングできず、同一の宛先を複数ポートにマップすると動作がおかしくなるのでご注意ください。
制限事項
- NATテーブルがメモリ(グローバル変数領域)を消費する。エントリ数は512となっていますが、これだと12KB消費します。子端末がせいぜい1,2セッションくらいしか張らないならこんなに要らないので、パッチ中の IP_NAPT_MAX を32等に小さくすれば、メモリを節約できます。(エントリはセッション切断後もタイムアウトするまでしばらく残るので、多めにしないと新たな接続ができなくなります。)逆にPC等をクライアントにしてしまうと、512でギリギリ足りるかどうかといったところです。
- そんなに速度は出ません。単一TCPセッションのみで測ってみると1MB/s出たら良い方という感じです。センサーデータを送るくらいなら十分すぎますが、PCとかスマートフォンを接続するのには向かないでしょう。
- TCPセッション管理がやや適当。TCPシーケンス番号を管理していない
- ICMPパケットのping以外のメッセージに対応していない
- 1つの宛先(アドレス+ポート)あたり1つのポートマッピングしかできない(NAPTエントリの数を節約するため)
- AS-ISなので、どこか何かバグってるかも?(言い訳)