LoginSignup
1
1

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

Last updated at Posted at 2023-10-10

概要

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

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

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

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

ハードウエア(送信部)

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

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

スクリーンショット 2023-10-10 16.28.03.png

ハードウエア(受信部)

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

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

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

スクリーンショット 2023-10-10 16.34.05.png

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

単純な作り。
ディープスリープモードは、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;
        }
    }
}

その他

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

以上

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1