概要
ホームセンターなどで販売されている電池駆動式の呼び鈴(受信側はAC駆動)。
テレメトリー機能がないため電池切れに気づかないことが多々あった。
そこで、ESP32 の WiFi P2P 通信の ESPNow と ディープスリープモード を使って、
省電力かつ電池残量が把握可能な チャイム を作ってみた。
また、手軽に受信機を増やせるように、MACアドレスによる送信元の識別機能&ペアリング機能も搭載させてみた。
実地テストはまだ行っていないので、今後、本文を更新する予定。
ハードウエア(送信部)
Seeed Studio XIAO ESP32S3 を使用。
ミニマム構成で使いやすい。
余計なものがついていないので省電力性能も問題なく、バッテリーの充電もできる。
・防水スイッチ
・カーボン抵抗(2MΩ x2)(バッテリー電圧計測用)
・ブザー(送信時に鳴らす)
・リチウムイオン充電池
・電池ケース
・基板
・その他諸々...
ハードウエア(受信部)
受信部は複数設置することができるようにした。
・一般的な ESP32開発ボードに スピーカー をつけたものを複数用意。
・バッテリー残量の確認用に、M5StickCとスピーカーを一つ用意。
プログラムソース(送信部)
単純な作り。
ディープスリープモードは、loop関数には到達せずに都度Setupから実行される。
バッテリー残量計算は大雑把にやっているので、厳密な値にはならない。
残量が少ないかどうかわかれば良い程度の精度。
#include <Arduino.h>
#include <esp_now.h>
#include <esp_wifi.h>
#include <WiFi.h>
static esp_now_peer_info_t recvr;
#define BUTTON_PIN D1
#define VBAT_PIN A0
#define BUZZER_PIN D3
void buzzer(){
pinMode(BUZZER_PIN,OUTPUT);
digitalWrite(BUZZER_PIN,0);
digitalWrite(BUZZER_PIN,1);
delay(200);
digitalWrite(BUZZER_PIN,0);
}
void espnow_init(void)
{
esp_netif_init();
esp_event_loop_create_default();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&cfg);
esp_wifi_set_storage(WIFI_STORAGE_RAM);
esp_wifi_set_mode(WIFI_MODE_STA);
esp_wifi_start();
wifi_country_t wc = {.cc="JP",.schan=1,.nchan=14,.max_tx_power=80,.policy=WIFI_COUNTRY_POLICY_AUTO};
esp_wifi_set_country(&wc);
WiFi.disconnect();
if (esp_now_init() == ESP_OK) {
;;
} else {
ESP.restart();
}
memset(&recvr, 0, sizeof(recvr));
recvr.peer_addr[0] = 0xFF;
recvr.peer_addr[1] = 0xFF;
recvr.peer_addr[2] = 0xFF;
recvr.peer_addr[3] = 0xFF;
recvr.peer_addr[4] = 0xFF;
recvr.peer_addr[5] = 0xFF;
esp_now_add_peer(&recvr);
}
uint16_t read_vbat(void)
{
uint32_t vbat32 = 0;
for(int i = 0; i < 16; i++) {
delay(10);
vbat32 = vbat32 + analogReadMilliVolts(VBAT_PIN);
}
float vbatf = 1.075f * 2.0f * (float)vbat32 / 16.0f;
uint16_t vbat = vbatf;
return vbat;
}
void setup()
{
pinMode(VBAT_PIN, INPUT);
bool is_switch_on = false;
bool is_wifi_on = false;
pinMode(BUTTON_PIN,INPUT);
unsigned long ts = millis();
if (digitalRead(BUTTON_PIN) == 1)
{
while (digitalRead(D1) == 1)
{
if ((millis() - ts) > 250)
{
is_switch_on = true;
break;
}
delay(10);
}
}
else
{
is_wifi_on = true;
espnow_init();
uint16_t vbat = read_vbat();
uint8_t packet[] = {0xBC, 0xBC, 0xFF, 0xFF, 0xBC, 0xBC}; // broadcast magic packet(vbat telemetry)
packet[2] = (uint8_t)(((vbat & 0xFF00) >> 8) & 0x00FF);
packet[3] = (uint8_t)(vbat & 0xFF00 & 0x00FF);
for (int i = 0; i < 3; i++)
{
if (ESP_OK == esp_now_send(recvr.peer_addr, packet, sizeof(packet)))
{
break;
}
delay(10);
}
}
if (is_switch_on && !is_wifi_on) // Chime
{
is_wifi_on = true;
espnow_init();
uint16_t vbat = read_vbat();
uint8_t packet[] = {0xBC, 0xB0, 0xFF, 0xFF, 0xB0, 0xBC}; // broadcast magic packet(Chime)
packet[2] = (uint8_t)(((vbat & 0xFF00) >> 8) & 0x00FF);
packet[3] = (uint8_t)(vbat & 0xFF00 & 0x00FF);
for (int i = 0; i < 5; i++)
{
if (ESP_OK == esp_now_send(recvr.peer_addr, packet, sizeof(packet)))
{
break;
}
delay(10);
}
}
if (is_wifi_on)
{
esp_now_deinit();
esp_wifi_stop();
WiFi.disconnect(true,true);
}
if (is_switch_on) buzzer();
esp_sleep_enable_ext0_wakeup(GPIO_NUM_2,1);
esp_sleep_enable_timer_wakeup(5ULL * 60ULL * 1000ULL * 1000ULL);
esp_deep_sleep_start();
}
void loop()
{
}
プログラムソース(受信部 M5StickC)
他の基板用のソースコードは省略(音を鳴らすだけなので基本構造は同じ)。
初回起動時 および、本体のボタンを長押しすると、ペアリングモードに入る。
送信側からのパケットを受信するとペアリングされる。
//#include <Arduino.h>
#include <esp_now.h>
#include <esp_wifi.h>
#include <WiFi.h>
#include "nvs_flash.h"
#include "nvs.h"
#include <M5GFX.h>
M5GFX display;
#define SPK_PIN (26)
#define BTN_PIN (37)
static volatile bool on_pairing_mode = false;
static volatile bool done_pairing_mode = false;
static volatile bool on_update_vbat = false;
static volatile bool on_chime_request = false;
static volatile uint16_t vbat = 0xFFFFU;
static constexpr float deg_to_rad = 0.017453292519943295769236907684886;
static constexpr int TFT_GREY = 0x5AEB;
static constexpr int LOOP_PERIOD = 35; // Display updates every 35 ms
static int liner_height;
static int meter_height;
static int needle_x;
static int needle_y;
static int needle_r;
static int osx, osy; // Saved x & y coords
uint8_t chime_mac[] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF};
// Function to print MAC address on Serial Monitor
void printMAC(const uint8_t * mac_addr){
char macStr[18];
snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X",
mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
Serial.println(macStr);
}
// Callback function executed when data is received
void OnDataRecv(const uint8_t * mac_addr, const uint8_t *data, int len) {
Serial.print("Packet received from: "); printMAC(mac_addr);
if (len != 6) return;
if (on_pairing_mode)
{
Serial.print("pairing mode\r\n");
if (done_pairing_mode)
{
Serial.print("pairing mode done...r\n");
;;
}
else if ( (data[0] == 0xBC)
&& (data[1] == 0xB0)
&& (data[4] == 0xB0)
&& (data[5] == 0xBC) )
{
memcpy(chime_mac,mac_addr,6);
vbat = (data[2] << 8) + (data[3]);
on_update_vbat = true;
done_pairing_mode = true;
}
}
else if (0 == memcmp(chime_mac,mac_addr,6))
{
// Battery Telemetry
if ( (data[0] == 0xBC)
&& (data[1] == 0xBC)
&& (data[4] == 0xBC)
&& (data[5] == 0xBC) )
{
vbat = (data[2] << 8) + (data[3]);
on_update_vbat = true;
Serial.printf("Telematry VBAT: %d\r\n",vbat);
}
// Chime Request
else if ( (data[0] == 0xBC)
&& (data[1] == 0xB0)
&& (data[4] == 0xB0)
&& (data[5] == 0xBC) )
{
vbat = (data[2] << 8) + (data[3]);
on_update_vbat = true;
on_chime_request = true;
Serial.printf("Chime REQ VBAT: %d\r\n",vbat);
}
}
}
void plotNeedle(int in_value)
{
int value = in_value;
if (value < 3000) value = 3000;
else if (value > 4000) value = 4000;
value = (value - 3000);
float sdeg = map(value, 0, 1000, -135, -45); // Map value to angle
// Calcualte tip of needle coords
float sx = cosf(sdeg * deg_to_rad);
float sy = sinf(sdeg * deg_to_rad);
display.setClipRect(0, 0, display.width(), meter_height - 5);
// Erase old needle image
display.drawLine(needle_x - 1, needle_y, osx - 1, osy, TFT_BLACK);
display.drawLine(needle_x + 1, needle_y, osx + 1, osy, TFT_BLACK);
display.drawLine(needle_x , needle_y, osx , osy, TFT_BLACK);
// Store new needle end coords for next erase
osx = roundf(sx * needle_r) + needle_x;
osy = roundf(sy * needle_r) + needle_y;
// Draw the needle in the new postion, magenta makes needle a bit bolder
// draws 3 lines to thicken needle
display.drawLine(needle_x - 1, needle_y, osx - 1, osy, TFT_RED);
display.drawLine(needle_x + 1, needle_y, osx + 1, osy, TFT_RED);
display.drawLine(needle_x , needle_y, osx , osy, TFT_MAGENTA);
// Re-plot text under needle
display.setTextColor(TFT_WHITE,TFT_BLACK);
if (display.width() > 100)
{
display.setFont(&fonts::Font4);
display.setTextDatum(textdatum_t::middle_center);
char s[5];
snprintf(s,sizeof(s),"%04d",in_value);
s[4] = '\0';
display.drawString(s, needle_x, display.height() - 15);
}
display.clearClipRect();
}
void analogMeter()
{
display.fillRect(0, 0, display.width() , meter_height , TFT_BLACK);
display.drawRect(1, 1, display.width()-2 , meter_height-2, TFT_DARKGRAY);
int r3 = needle_y * 13 / 15;
int r2 = needle_y * 12 / 15;
int r1 = needle_y * 11 / 15;
needle_r = r1 - 3;
display.fillArc(needle_x, needle_y, r1, r3, 226, 248, TFT_ORANGE);
display.fillArc(needle_x, needle_y, r1, r3, 248, 270, TFT_GREEN);
display.fillArc(needle_x, needle_y, r1, r3, 270, 292, TFT_GREEN);
display.fillArc(needle_x, needle_y, r1, r3, 292, 315, TFT_CYAN);
display.fillArc(needle_x, needle_y, r1, r1, 225, 315, TFT_BLACK);
display.setTextColor(TFT_WHITE);
display.setFont(&fonts::Font2);
display.setTextDatum(textdatum_t::bottom_center);
for (int i = 0; i <= 20; i++)
{
display.fillArc(needle_x, needle_y, r1, (i % 5) ? r2 : r3, 225 + i * 4.5f, 225 + i * 4.5f, TFT_BLACK);
}
}
void setup(void)
{
display.begin();
display.setEpdMode(epd_mode_t::epd_quality);
display.setRotation(1);
display.startWrite();
display.fillScreen(TFT_BLACK);
liner_height = display.height() * 0 / 5;
meter_height = display.height() * 5 / 5;
needle_x = display.width() / 2;
needle_y = (meter_height*2 + display.width()) / 3;
osx = needle_x;
osy = needle_y;
analogMeter(); // Draw analogue meter
display.endWrite();
Serial.begin(115200);
Serial.println("");
Serial.println("");
Serial.printf("Internal Total heap %d, internal Free Heap %d\r\n", ESP.getHeapSize(), ESP.getFreeHeap());
Serial.printf("SPIRam Total heap %d, SPIRam Free Heap %d\r\n", ESP.getPsramSize(), ESP.getFreePsram());
Serial.printf("Flash Size %d, Flash Speed %d\r\n", ESP.getFlashChipSize(), ESP.getFlashChipSpeed());
Serial.printf("ChipRevision %d, Cpu Freq %d, SDK Version %s\r\n", ESP.getChipRevision(), ESP.getCpuFreqMHz(), ESP.getSdkVersion());
ledcSetup(1,12000, 8);
ledcAttachPin(SPK_PIN,1);
pinMode(BTN_PIN,INPUT_PULLUP);
// READ pairing-data from NVM
{
nvs_handle_t nvsHandle;
uint32_t rsz = 0;
esp_err_t err = nvs_open("config",NVS_READWRITE,&nvsHandle);
if( err != ESP_OK )
{
Serial.printf("Failed to open NVS partition (%s)", esp_err_to_name(err));
nvs_close(nvsHandle);
ESP.restart();
}
// Read
Serial.println("Reading data from NVS ... ");
// chime_mac
{
rsz = sizeof(chime_mac);
err = nvs_get_blob(nvsHandle, "chime_mac", chime_mac, &rsz);
switch (err)
{
case ESP_OK:
Serial.printf("chime_mac = %02X:%02X:%02X:%02X:%02X:%02X\r\n"
,chime_mac[0],chime_mac[1],chime_mac[2]
,chime_mac[3],chime_mac[4],chime_mac[5]);
break;
case ESP_ERR_NVS_NOT_FOUND:
Serial.println("chime_mac is not initialized yet!");
memset(chime_mac,0,sizeof(chime_mac));
done_pairing_mode = false;
on_pairing_mode = true;
for (int i = 0; i < 2; i++)
{
ledcWriteTone(1,659);
delay(500);
ledcWriteTone(1,0);
delay(200);
}
break;
default :
Serial.printf("chime_mac: read-error (%s)\r\n", esp_err_to_name(err));
nvs_close(nvsHandle);
ESP.restart();
break;
}
}
nvs_close(nvsHandle);
}
WiFi.disconnect();
WiFi.mode(WIFI_STA);
delay(500);
Serial.println(WiFi.macAddress());
// Init ESP-NOW
if (esp_now_init() != ESP_OK) {
Serial.println("There was an error initializing ESP-NOW");
return;
}
esp_now_peer_info_t slave;
memset(&slave, 0, sizeof(slave));
slave.peer_addr[0] = 0xFF; //0x48;
slave.peer_addr[1] = 0xFF; //0x27;
slave.peer_addr[2] = 0xFF; //0xe2;
slave.peer_addr[3] = 0xFF; //0xe6;
slave.peer_addr[4] = 0xFF; //0xda;
slave.peer_addr[5] = 0xFF; //0x2C;
esp_now_add_peer(&slave);
// Once ESPNow is successfully Init, we will register for recv CB to
// get recv packer info
esp_now_register_recv_cb(OnDataRecv);
}
void loop(void)
{
if (on_update_vbat)
{
if (!display.displayBusy())
{
on_update_vbat = false;
display.startWrite();
plotNeedle(vbat);
display.endWrite();
on_update_vbat = false;
}
}
if (on_pairing_mode)
{
if (done_pairing_mode)
{
done_pairing_mode = false;
on_pairing_mode = false;
// update NVS
{
nvs_handle_t nvsHandle;
uint32_t rsz = 0;
esp_err_t err = nvs_open("config",NVS_READWRITE,&nvsHandle);
if( err != ESP_OK )
{
Serial.printf("Failed to open NVS partition (%s)", esp_err_to_name(err));
nvs_close(nvsHandle);
ESP.restart();
}
{
rsz = sizeof(chime_mac);
err = nvs_set_blob(nvsHandle, "chime_mac", chime_mac, rsz);
switch (err)
{
case ESP_OK:
Serial.printf("new chime_mac = %02X:%02X:%02X:%02X:%02X:%02X\r\n"
,chime_mac[0],chime_mac[1],chime_mac[2]
,chime_mac[3],chime_mac[4],chime_mac[5]);
Serial.println("");
break;
default :
Serial.printf("chime_mac: read-error (%s)\r\n", esp_err_to_name(err));
nvs_close(nvsHandle);
ESP.restart();
}
nvs_commit(nvsHandle);
nvs_close(nvsHandle);
}
}
for (int i = 0; i < 3; i++)
{
ledcWriteTone(1,659);
delay(500);
ledcWriteTone(1,0);
delay(200);
}
}
}
else
{
unsigned long ts = millis();
while (digitalRead(BTN_PIN) == 0)
{
if (millis() - ts > 3000)
{
done_pairing_mode = false;
on_pairing_mode = true;
for (int i = 0; i < 2; i++)
{
ledcWriteTone(1,659);
delay(500);
ledcWriteTone(1,0);
delay(200);
}
}
}
if (on_chime_request)
{
on_chime_request = false;
for (int i = 0; i < 3; i++)
{
ledcWriteTone(1,659);
delay(800);
ledcWriteTone(1,0);
delay(200);
ledcWriteTone(1,523);
delay(2000);
ledcWriteTone(1,0);
delay(1000);
}
on_chime_request = false;
}
}
}
その他
現在は、実際に設置テストをすべく送信機の筐体を作成中。
進展があれば、本文を更新予定。