ESP-NOWを使った温度測定用スケッチとスクリプト

スケッチやサーバー側スクリプト。データベース定義については以前載せたものと同様なのでこちらを参照のこと。このページの本文はこちら

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 内にある。

#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() による接続が失敗していた。

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(); を入れることにした。

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”については、こちらのページを参照のこと。

#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[] から値を取得する部分はムダだが問題はない。

<?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社を表す)の種類も増えてきた。