経緯
メモ: ESP8266 での実測電流 | tech - 氾濫原 というのを書いたあと、WiFi のコネクションなしでもデータを適当に送信できたらいいのではないか?と考えました。
具体的には SSID などをビーコン情報としてコネクションレスでブロードキャストしているのだから、これにセンサーデータをつけてしまえばいいだろう、というアイデアです。
ということで ESP8266 の API を調べてみましたが、どうやら WiFi のフレームを生で扱うようなAPIはないようでした。じゃあSSID にデータを入れて softap としてビーコンを発射すればとも思いましたが、さすがに邪悪すぎます。
もうちょっとAPIを眺めていると ESP-NOW というのがあることに気付きました。独自拡張のような形でIEEE802.11のフレームを飛ばしてWiFiコネクションレスで送受信できるもののようです。これを使えば前述のような邪悪なことをしなくても同じようなことができそうです。
ESP-NOW の概念
常時データを受けとるスレーブと、必要なときにデータを送信するコントローラに分かれます (送受信はどちらもできる)。スレーブは常時チャンネルを受信していなければならないので、基本的に固定の電源が必要です。コントローラはデータを投げるだけなのでバッテリ駆動できます。
送受信するまえにお互いの Mac アドレスを知っている必要があります。ESP-NOW としては、お互いに一旦 softap となって BSSID (Macアドレス) を公開してペアリングすることを想定しているようです。
なお、(おそらく)フレームをそのまま送信しているだけなので、確実に到達する保証はなく、もし要求されるならACKみたいなものは自力で実装する必要もあります。
暗号化のサポートもあるようですがそこまでとりあえずは調べていません。
実装
スレーブとコントローラいずれも ESP8266 である必要があるので (ローレベルなIEEE802.11のフレームを扱えるなら互換デバイスは作れそうですが)、ESP8266 は2台必要です。
ペアリング部分はめんどうなので、肝心のところだけ試しに実装して試してみました。
スレーブ
スレーブはデータを受けとって表示するだけです。現実的なアプリケーションなら、STATION+AP モードにして、別のホストに中継するように動作させると思いますが、今回はAPモードで動作させています。
esp_now_register_send_cb してますが send してないので呼ばれません。
ピアのMacアドレスは、STATION_IF のMacアドレスです。
#include <Arduino.h>
#include <ESP8266WiFi.h>
extern "C" {
#include <espnow.h>
#include <user_interface.h>
}
#define WIFI_DEFAULT_CHANNEL 1
uint8_t mac[] = {0x5C,0xCF,0x7F,0x8,0x37,0xC7};
void printMacAddress(uint8_t* macaddr) {
Serial.print("{");
for (int i = 0; i < 6; i++) {
Serial.print("0x");
Serial.print(macaddr[i], HEX);
if (i < 5) Serial.print(',');
}
Serial.println("}");
}
void setup() {
pinMode(13, OUTPUT);
Serial.begin(74880);
Serial.println("Initializing...");
WiFi.mode(WIFI_AP);
WiFi.softAP("foobar", "12345678", 1, 0);
uint8_t macaddr[6];
wifi_get_macaddr(STATION_IF, macaddr);
Serial.print("mac address (STATION_IF): ");
printMacAddress(macaddr);
wifi_get_macaddr(SOFTAP_IF, macaddr);
Serial.print("mac address (SOFTAP_IF): ");
printMacAddress(macaddr);
if (esp_now_init() == 0) {
Serial.println("init");
} else {
Serial.println("init failed");
ESP.restart();
return;
}
esp_now_set_self_role(ESP_NOW_ROLE_SLAVE);
esp_now_register_recv_cb([](uint8_t *macaddr, uint8_t *data, uint8_t len) {
Serial.println("recv_cb");
Serial.print("mac address: ");
printMacAddress(macaddr);
Serial.print("data: ");
for (int i = 0; i < len; i++) {
Serial.print(" 0x");
Serial.print(data[i], HEX);
}
Serial.println("");
});
esp_now_register_send_cb([](uint8_t* macaddr, uint8_t status) {
Serial.println("send_cb");
Serial.print("mac address: ");
printMacAddress(macaddr);
Serial.print("status = "); Serial.println(status);
});
int res = esp_now_add_peer(mac, (uint8_t)ESP_NOW_ROLE_CONTROLLER,(uint8_t)WIFI_DEFAULT_CHANNEL, NULL, 0);
// esp_now_unregister_recv_cb();
// esp_now_deinit();
}
void loop() {
}
コントローラ
スレーブとほぼ同じですが ROLE が変わっています。適当なバイト列を esp_now_send() しています。
ピアのMacアドレスは、SOFTAP_IF のMacアドレスです。
#include <Arduino.h>
#include <ESP8266WiFi.h>
extern "C" {
#include <espnow.h>
#include <user_interface.h>
}
#define WIFI_DEFAULT_CHANNEL 1
uint8_t mac[] = {0x1A,0xFE,0x34,0xEE,0x84,0x88};
void printMacAddress(uint8_t* macaddr) {
Serial.print("{");
for (int i = 0; i < 6; i++) {
Serial.print("0x");
Serial.print(macaddr[i], HEX);
if (i < 5) Serial.print(',');
}
Serial.println("}");
}
void setup() {
pinMode(13, OUTPUT);
Serial.begin(74880);
Serial.println("Initializing...");
WiFi.mode(WIFI_STA);
uint8_t macaddr[6];
wifi_get_macaddr(STATION_IF, macaddr);
Serial.print("mac address (STATION_IF): ");
printMacAddress(macaddr);
wifi_get_macaddr(SOFTAP_IF, macaddr);
Serial.print("mac address (SOFTAP_IF): ");
printMacAddress(macaddr);
if (esp_now_init()==0) {
Serial.println("direct link init ok");
} else {
Serial.println("dl init failed");
ESP.restart();
return;
}
esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER);
esp_now_register_recv_cb([](uint8_t *macaddr, uint8_t *data, uint8_t len) {
Serial.println("recv_cb");
Serial.print("mac address: ");
printMacAddress(macaddr);
Serial.print("data: ");
for (int i = 0; i < len; i++) {
Serial.print(data[i], HEX);
}
Serial.println("");
});
esp_now_register_send_cb([](uint8_t* macaddr, uint8_t status) {
Serial.println("send_cb");
Serial.print("mac address: ");
printMacAddress(macaddr);
Serial.print("status = "); Serial.println(status);
});
int res = esp_now_add_peer(mac, (uint8_t)ESP_NOW_ROLE_SLAVE,(uint8_t)WIFI_DEFAULT_CHANNEL, NULL, 0);
uint8_t message[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x08 };
esp_now_send(mac, message, sizeof(message));
ESP.deepSleep(2.5e6, WAKE_RF_DEFAULT);
// esp_now_unregister_recv_cb();
// esp_now_deinit();
}
void loop() {
}
動作と消費電力
こんな普通に簡単な実装を書いてみたら普通に動きました。
スレーブ側のシリアル出力
コントローラの電流値の推移
この実装だと 2.5秒ごとに毎回送信しているので割と平均消費も大きいです。しかしそれでも、前回の16秒ごとのWiFi接続の30mAの半分以下です。
コントローラの別実装
ということで、2.5秒おきに deep sleep から復帰して値を RTC メモリ書きつつ、4回に1度はWiFiで送信するようなサンプルも書いてみました。これは前回のWiFiコネクション+UDPの部分をESP-NOWを使うように置き換えたものになります。
エントリ冒頭の画像はこれの横軸を広げたものです。
平均で6.8mAぐらいです。
備考
この実装ではフレームの再送などはしていないので、データが欠落する可能性があります。とはいえ UDP の実装でも同じことがいえるので、これが原因だからダメということにはならないでしょう。
しかしこの実装だとMacアドレス以外一切何も確認していないので、もし悪意のある人が近くにいれば変なデータを送りこむのは容易です。
ESP8266 を使ってできる無線通信ではこれ以上の省電力化は難しそうな気がします。あとは送信出力を下げるぐらいしかなさそうです。