ESPNow のブロードキャスト通信機能を利用したバッテリー駆動対応のチャイムをつくる

# 概要
ホームセンターなどで販売されている電池駆動式の呼び鈴（受信側はAC駆動）。
テレメトリー機能がないため電池切れに気づかないことが多々あった。

そこで、ESP32 の WiFi P2P 通信の ESPNow と ディープスリープモード を使って、
省電力かつ電池残量が把握可能な チャイム を作ってみた。

また、手軽に受信機を増やせるように、MACアドレスによる送信元の識別機能＆ペアリング機能も搭載させてみた。

実地テストはまだ行っていないので、今後、本文を更新する予定。

# ハードウエア（送信部）

Seeed Studio XIAO ESP32S3 を使用。
ミニマム構成で使いやすい。
余計なものがついていないので省電力性能も問題なく、バッテリーの充電もできる。

https://www.switch-science.com/products/8968

・防水スイッチ
・カーボン抵抗（2MΩ x2）（バッテリー電圧計測用）
・ブザー（送信時に鳴らす）
・リチウムイオン充電池
・電池ケース
・基板
・その他諸々...

![スクリーンショット 2023-10-10 16.28.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/190537/38ab8457-63be-6e3a-b9b1-11ff76dcbf0f.png)

# ハードウエア（受信部）

受信部は複数設置することができるようにした。

・一般的な ESP32開発ボードに スピーカー をつけたものを複数用意。

・バッテリー残量の確認用に、M5StickCとスピーカーを一つ用意。

![スクリーンショット 2023-10-10 16.34.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/190537/8bb029d4-1882-af79-6485-598d93891d9f.png)

# プログラムソース(送信部)

単純な作り。
ディープスリープモードは、loop関数には到達せずに都度Setupから実行される。

バッテリー残量計算は大雑把にやっているので、厳密な値にはならない。
残量が少ないかどうかわかれば良い程度の精度。

```C
#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)

他の基板用のソースコードは省略（音を鳴らすだけなので基本構造は同じ）。

初回起動時 および、本体のボタンを長押しすると、ペアリングモードに入る。
送信側からのパケットを受信するとペアリングされる。

```C
//#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;
        }
    }
}

```

# その他

現在は、実際に設置テストをすべく送信機の筐体を作成中。
進展があれば、本文を更新予定。


# 以上