ESP-NOWを使った温度測定用スケッチとスクリプト
スケッチやサーバー側スクリプト。データベース定義については以前載せたものと同様なのでこちらを参照のこと。このページの本文はこちら。
- ESP32用スレーブ側スケッチ (ESP32_espnow_slave3.ino)
- ESP8266用コントローラ側スケッチ (ESP_espnow_ctrl5.ino)
- データベース格納用スクリプト (store_data.php)
ESP32_espnow_slave3.ino
Arduino core for ESP32でビルドした。バージョン番号がよく分からないのだが、2018年4月15日にgithubからそっくりzipで落として、…\[aruino-src]\hardware\espressif\esp32 に展開して使っている。esp_now.hは、…\tools\sdk\include\esp32 内にある。
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 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 |
#include <esp_now.h> #include <WiFi.h> /** * ESP32で動かすスレーブ側 */ #define SERIAL_MONITOR 1 #if SERIAL_MONITOR #define START_MSG "\n" + String(__FILE__) + " start.\n" #define SERIAL_OUT(a) Serial.print(a) #define SERIAL_F(...) Serial.printf(__VA_ARGS__) #define SERIAL_INIT(a) Serial.begin(a) #else #define SERIAL_INIT(a) #define SERIAL_OUT(a) #define SERIAL_F(...) #endif #define LED 13 #define WIFI_CHANNEL 0 const char* slave_ssid = "ESP_SLAVE_SSID"; const char* slave_password = "SLAVE_PASSWORD"; // wifi const char* ssid = "ssid"; const char* password = "password"; const char* remote_host = "host_ipaddress"; // static ip info const IPAddress myip(192,168,1,123); const IPAddress gwip(192,168,1,1); const IPAddress mask(255,255,255,0); #pragma pack(push, 1) typedef struct RX_LOG { char mac[32]; unsigned long rx_millis; long last_seq_no; } rx_log_t; typedef struct { uint16_t seq_no; uint16_t elapsed; uint16_t extra; // uint8_t retry; float T; float H; float P; float V; uint8_t checksum; } sensor_data_t; #pragma pack(pop) bool data_recieved = false; String sender_mac; sensor_data_t rx_data; #define MAX_CLIENT 20 rx_log_t rx_log[MAX_CLIENT]; void init_rx_log() { for(int i = 0; i < MAX_CLIENT; i++) memset((void*)&rx_log[i], 0, sizeof(rx_log_t)); } rx_log_t* findlog() { int i; for(i = 0; i < MAX_CLIENT; i++) { rx_log_t* p = &rx_log[i]; if (!*p->mac || !strcmp(p->mac, sender_mac.c_str())) return p; } return NULL; } static uint8_t calc_check_sum(uint8_t* p) { uint8_t sum = 0; for(int i = 0; i < sizeof(sensor_data_t) - 1; i++) sum += p[i]; return sum; } void die(const char* cp) { if (cp) SERIAL_OUT(String(cp) + "\n"); digitalWrite(LED, 1); delay(500); digitalWrite(LED, 0); delay(500); } #define WAIT_MS 50 #define WAIT_LIMIT (int)(15000 / WAIT_MS) bool ap_connect() { int n; int count = 0; int retry = 0; SERIAL_OUT("\nstart ap_connect()\n"); WiFi.config(myip, gwip, mask, gwip); WiFi.mode(WIFI_STA); WiFi.disconnect(); delay(10); WiFi.begin(ssid, password); while ((n = WiFi.status()) != WL_CONNECTED) { SERIAL_OUT(n); delay(WAIT_MS); if (n == WL_NO_SSID_AVAIL || n == WL_CONNECT_FAILED) { WiFi.reconnect(); SERIAL_OUT("+"); count = 0; retry++; } if (count++ > WAIT_LIMIT || retry > 3) { SERIAL_OUT("\nap_connect() failed\n"); return false; } } SERIAL_OUT("\nap_connect() done.\n"); return true; } bool send_to_server() { int count = 0; SERIAL_OUT("start send_to_server()"); WiFiClient client; if (!client.connect(remote_host, 80)) return false; String request = "/store_data.php?point_id=" + sender_mac + "&T=" + String(rx_data.T) + "&H=" + String(rx_data.H) + "&P=" + String(rx_data.P) + "&V=" + String(rx_data.V) + "&X1=" + String(rx_data.elapsed) + "&X2=" + rx_data.retry; String req_line = "GET " + request + " HTTP/1.1\r\nHost: " + String(remote_host) + "\r\nConnection: close\r\n\r\n"; client.print(req_line); count = 0; while(!client.available()) { // waif for response. delay(1); if (count++ > 5000) return false; } while(client.available()) { // read response. String line = client.readStringUntil('\n'); SERIAL_F("%s\n", line.c_str()); delay(1); } return true; } void onReceive(const uint8_t *mac, const uint8_t *data, int data_len) { char tmp[20]; sprintf(tmp, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); sender_mac = String(tmp); esp_now_unregister_recv_cb(); if (data_len == sizeof(sensor_data_t)) { memcpy((uint8_t*)&rx_data, data, data_len); if (calc_check_sum((uint8_t*)&rx_data) == rx_data.checksum) data_recieved = true; else SERIAL_OUT("onReceive() : checksum not match.\n"); } else SERIAL_OUT("onReceive() : data length dose not match\n"); } void start_espnow() { SERIAL_F("\nstart_espnow()\n"); WiFi.mode(WIFI_AP); if (WiFi.softAP(slave_ssid, slave_password, WIFI_CHANNEL, 1)) SERIAL_F("SoftAP Start. my MAC : %s\n" , WiFi.softAPmacAddress().c_str()); else die("softAP start failed"); if (esp_now_init() != ESP_OK) die("ESPNow Init Failed"); esp_now_register_recv_cb(onReceive); } void setup() { delay(1); SERIAL_INIT(115200); SERIAL_OUT("\nESP32_espnow_slave2 start\n"); pinMode(LED, OUTPUT); digitalWrite(LED, 0); init_rx_log(); start_espnow(); } void loop() { delay(1); if (data_recieved) { esp_now_deinit(); data_recieved = false; rx_log_t* p = findlog(); unsigned long tm = millis(); bool skip = false; if (p != NULL) { if (*p->mac) { SERIAL_F("find %s in rx_log\n", p->mac); SERIAL_F("rx interval = %d last_seq = %d\n", tm - p->rx_millis, p->last_seq_no); if (p->last_seq_no == rx_data.seq_no) // 前回と同じシーケンス番号。マスタは失敗したと思って再送しているが受信済。 skip = true; } else { strncpy(p->mac, sender_mac.c_str(), sizeof(p->mac)); SERIAL_F("new client %s\n", p->mac); } p->last_seq_no = rx_data.seq_no; p->rx_millis = tm; if (!skip) { digitalWrite(LED, 1); if (ap_connect()) { send_to_server(); WiFi.disconnect(); } digitalWrite(LED, 0); SERIAL_F("send_data takes %d msec.\n", (millis() - tm)); } start_espnow(); } else { init_rx_log(); ESP.restart(); } } } |
追記
WiFiアクセスポイントに接続できなくなった
最初のスケッチで動かし始めてから4.5日間ほど経過した時点で、スレーブ側からWiFiルーターへの接続が失敗するようになった。コントローラからのESP-NOWによる接続は正常に行われているようで、スレーブからWiFiルーターへの接続中(実際は試行中)を示すLEDも定期的に点灯しておりESP-NOWの受信動作自体は持続していた模様。
接続不能になるまでのデータはサーバーのデータベーステーブルに格納されており、約27600行が格納されていたから、同じだけの回数WiFi.begin() による接続が成功していたことになる。
スレーブ側をリセットしたり電源をオフにしても治らないので、別のESP32モジュールに切り替え、MACアドレスを変更する都合でコントローラのスケッチも入れ替えてESP-NOWの実験は継続することにした。
問題のESP32モジュールにWiFi接続のための単純なスケッチを入れて試してみてもルーターに接続できなかった。調べてみるとWiFi.begin() による接続が失敗していた。
1 2 3 |
WiFi.begin(ssid, password); while ((n = WiFi.status()) != WL_CONNECTED) { ... |
WiFi.status() の戻り値がしばらくの間は WL_DISCONNECTED ( == 6) を返すが、その後 WL_NO_SSID_AVAIL ( == 1) となって回復せず、スケッチが接続をあきらめていることが分かった。発生した状況は違うが、以前ESP-WROOM-02相互間での接続がうまくいかなかったときに似ている。
対策とし、現在のスケッチのように、WiFi.begin(); の前に、WiFi.disconnect(); を入れることにした。
1 2 3 4 |
WiFi.mode(WIFI_STA); WiFi.disconnect(); delay(10); WiFi.begin(ssid, password); |
そして、アクセスポイントへの接続が成功した場合は常にWiFi.disconnect(); してからESP-NOWの初期化を行うようにした。接続不能になった理由は不明だが、WiFi接続に関する内部状態が妙な具合になってしまうことがあるんだろう。モジュールの変更とスケッチの書き直し後、約8時間ほどで動作を再開した。
ESP_espnow_ctrl5.ino
ESP8266 Core for Arduino 2.4.1でビルド。I2Cを使った温度センサーインタフェース用の“hdc1000_bme280.h”については、こちらのページを参照のこと。
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 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
#include <espnow.h> #include <ESP8266WiFi.h> extern "C" { #include "user_interface.h" } #include "hdc1000_bme280.h" #define LED 15 #define MEASURE_INTERVAL_MS 60000 // 60秒 #define RESEND_INTERVAL_MS 10000 // 10秒 #define WIFI_CHANNEL 0 #ifndef ESP_OK #define ESP_OK 0 #endif #define SERIAL_MONITOR 0 #if SERIAL_MONITOR #define START_MSG "\n" + String(__FILE__) + " start.\n" #define SERIAL_INIT(a) Serial.begin(a) #define SERIAL_OUT(a) Serial.print(a) #define SERIAL_F(...) Serial.printf(__VA_ARGS__) #else #define START_MSG #define SERIAL_INIT(a) #define SERIAL_OUT(a) #define SERIAL_F(...) #endif #pragma pack(push, 1) typedef struct { uint16_t seq_no; uint16_t elapsed; uint16_t extra; uint8_t retry; float T; float H; float P; float V; uint8_t checksum; } sensor_data_t; #pragma pack(pop) uint8_t slave_mac[] = {0x24, 0x0a, 0xc4, 0xXX, 0xXX, 0xXX}; // 送信先 (ESP-32) bme280 bme280(0x76); unsigned long start_time = millis(); void die(const char* cp) { if (cp) SERIAL_F("\n%s\n", cp); for(;;) { digitalWrite(LED, 1); delay(500); digitalWrite(LED, 0); delay(500); } } #if SERIAL_MONITOR static void DUMP_RTC(sensor_data_t* p) { String s; char tmp[120]; s = "RTC: "; for(int i = 0; i < sizeof(sensor_data_t); i++) { SERIAL_F("%02X ", ((uint8_t*)p)[i]); s += String(tmp) + String(" "); } SERIAL_F("\nDUMP RTC\nseq_no=%d, elapsed=%d, extra=%d, retry=%d, T=%.2f, checksum = %02x\n", p->seq_no, p->elapsed, p->extra, p->retry, p->T, p->checksum); } #else #define DUMP_RTC(a) #endif uint8_t calc_check_sum(uint8_t* p) { uint8_t sum = 0; for(int i = 0; i < sizeof(sensor_data_t) - 1; i++) sum += p[i]; return sum; } enum { S_WAIT = 0, S_SUCCESS , S_RETRY, S_TIMEOUT, S_FAILED } state = S_WAIT; void onSent(u8* mac, u8 result) { state = result == ESP_OK ? S_SUCCESS : S_RETRY; } int send_data(sensor_data_t* p) { float vbat = 570.0 * system_adc_read() / 1024.0; // 100倍した電圧。470K + 100Kでの分圧時 570 / 100 = 5.7 bme280.measure(); p->T = bme280.temp; p->H = bme280.humi; p->P = bme280.press; p->V = vbat; p->checksum = calc_check_sum((uint8_t*)p); esp_now_register_send_cb(onSent); if (esp_now_send(slave_mac, (u8*)p, sizeof(sensor_data_t)) != ESP_OK) { esp_now_unregister_send_cb(); return S_FAILED; } unsigned long start = millis(); while(state == S_WAIT) { if (millis() - start > 1000) { // 最大1秒待つ。 state = S_TIMEOUT; break; } delay(1); } esp_now_unregister_send_cb(); return state; } void setup() { uint8_t sta_mac[6]; // 自分 sensor_data_t data; SERIAL_INIT(115200); SERIAL_OUT(START_MSG); if (!bme280.init()) die("FATAL: BME280 init failed."); pinMode(LED, OUTPUT); digitalWrite(LED, 1); rst_info *prst = ESP.getResetInfoPtr(); if (prst->reason != REASON_DEEP_SLEEP_AWAKE) { memset(&data, 0, sizeof(sensor_data_t)); delay(10); } else { ESP.rtcUserMemoryRead(0, (uint32_t*)&data, sizeof(sensor_data_t)); if (data.checksum != calc_check_sum((u8*)&data)) { SERIAL_F("\nrtc data corrupted.\n"); memset(&data, 0, sizeof(sensor_data_t)); } } DUMP_RTC(&data); WiFi.macAddress(sta_mac); unsigned long random_seed = (unsigned long)sta_mac[5] << 24 & 0xff000000 | (unsigned long)sta_mac[4] << 16 & 0xff0000 | (unsigned long)sta_mac[3] << 8 & 0xff00 | (unsigned long)sta_mac[2] & 0xff; SERIAL_F("random_seed = %d\n", random_seed); randomSeed(random_seed); WiFi.mode(WIFI_STA); SERIAL_F("STA MAC: %s\n", WiFi.macAddress().c_str()); if (esp_now_init() != ESP_OK) die("ESPNow Init Failed"); esp_now_set_self_role(ESP_NOW_ROLE_CONTROLLER); if ( esp_now_add_peer(slave_mac, ESP_NOW_ROLE_SLAVE, WIFI_CHANNEL, NULL, 0) != ESP_OK) die("esp_now_add_peer() failed."); unsigned long sleep_ms = 0; int state = send_data(&data); switch(state) { case S_FAILED: die("esp_now_send() failed."); break; case S_SUCCESS: SERIAL_OUT("Data is sent.\n"); sleep_ms = MEASURE_INTERVAL_MS; if (data.retry > 0) sleep_ms += random(2000, 4000); data.seq_no++; data.retry = 0; break; case S_RETRY: data.retry++; if (data.retry < 10) { sleep_ms = random(1000, 2000); SERIAL_F("Data send failed. retry count = %d. wait %d msec.\n", data.retry, sleep_ms); break; } ESP.restart(); delay(10); break; case S_TIMEOUT: SERIAL_OUT("send_data timeout."); data.retry = 0; data.seq_no = 0; sleep_ms = RESEND_INTERVAL_MS; break; } unsigned long tm = millis() - start_time; if (state != S_SUCCESS) data.extra += tm; else { data.elapsed = tm + data.extra; data.extra = 0; } data.checksum = calc_check_sum((u8*)&data); ESP.rtcUserMemoryWrite(0, (uint32_t*)&data, sizeof(sensor_data_t)); #if SERIAL_MONITOR delay(10); #endif ESP.deepSleep(sleep_ms * 1000, WAKE_RF_DEFAULT); } void loop() { } |
store_data.php
すべてのコントローラからのデータは単一のテーブル(envdata10) と、MACアドレスごとのテーブルに格納している。
各テーブルは、
mysql> create table envdata10 like envdata;
といった方法で作成した。このスクリプトが利用している envdata_db.php はこちらを参照。insert_array() 内で $_GET[] から値を取得する部分はムダだが問題はない。
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 |
<?PHP require_once('envdata_db.php'); header('cotent-type: text/html'); $ar = array('T' => 0.0, 'P' => 0.0, 'H' => 0.0, 'V' => 0.0, 'X1' => 0.0, 'X2' => 0.0, 'point_id' => 'unknown'); foreach($ar as $k => $v) { $s = isset($_GET[$k]) ? $_GET[$k] : ""; if (strlen($s) > 0) $ar[$k] = $s; } $db = new envdata_db(); $db->insert_array("envdata10", $ar); $table = ""; if ($ar['point_id'] == '18:FE:34:XX:XX:XX') $table = "envdata5"; else if ($ar['point_id'] == 'BC:DD:C2:XX:XX:XX') $table = "envdata"; else if ($ar['point_id'] == '5C:CF:7F:XX:XX:XX') $table = "envdata7"; else if ($ar['point_id'] == '84:F3:EB:XX:XX:XX') $table = "envdata8"; else echo "table name is empty"; if ($table != "") { if (!$db->insert_array($table, $ar)) { echo "NG SQL ERROR: " . $envdata->getLast_error() . '<br/>'; echo $envdata->getLast_sql() . '<br/>'; } else echo "OK"; } ?> |
MACアドレス先頭3バイト(Espressif社を表す)の種類も増えてきた。