XIAO ESP32C6のリポバッテリー運用について

# はじめに
[前回](https://qiita.com/nori-dev-akg/items/e9d2ff175b7d7384498f)の続き「Seeed Studio XIAO ESP32C6」の記事です。今回は、これとリチウムポリマーバッテリー運用の話になります。

# リポバッテリー
「XIAO ESP32C6」といえば、外部バッテリー運用。

## リポバッテリーの接続
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203949/ea924cfa-efe2-4ef0-97d1-bf391ce1bb98.png" width=30%>

背面に外部バッテリー用の実装があります（図中赤枠）。ここに接続すると充電も可能です。公式HPには以下のような記述があります。実際に充電させたところ、充電中（赤点滅）→完了（消灯）しました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203949/8cd102a3-4b50-4f0f-a702-f9bdb04c893b.png" width=100%>

## リポバッテリーの電圧の読み取り
公式HPには、バッテリー電圧の読み取りの方法があります。抵抗220kで分圧してA0ピンで読み取るような仕組みです。私が実装したものを以下の写真です。バッテリー用にPHコネクタを付けました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203949/d120ad06-a4dd-454c-9e0c-f1c6a8d06717.png" width=30%>

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203949/71086c23-5319-4413-950e-49441479cdff.png" width=50%>

下はLCDをI2Cで繋げ、満タンのリポを繋いで測定したものです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203949/0c37b3ab-4bab-4da1-a37a-dba5eb18061a.png" width=50%>

# DeepSleep
バッテリー運用といえば DeepSleep の話になります。XIAO ESP32C6 は、公式だと 「~ 15 μA」の記載があります。

## 計測してみよう
[公式にDeepSleepの記載](https://wiki.seeedstudio.com/xiao_esp32c6_getting_started/#demo1-deep-sleep-with-external-wake-up)がありますので、また流用です。GPIO_NUM_1にタクトスイッチを繋げてHIGHにするとWakeUpするようにします。

公式ソースとの変更点は
- 開始のdelayを 1000 → 10000に変更。次の書き込みが面倒なことになります
- GPIO_NUM_0 → GPIO_NUM_1に変更。 GPIO_NUM_0 はバッテリー計測用にしてしまったので

```cpp
/*
Hardware Connections
======================
Push Button to GPIO 0 pulled down with a 10K Ohm
resistor

NOTE:
======
Bit mask of GPIO numbers which will cause wakeup. Only GPIOs
which have RTC functionality can be used in this bit map.
For different SoCs, the related GPIOs are:
- ESP32: 0, 2, 4, 12-15, 25-27, 32-39
- ESP32-S2: 0-21
- ESP32-S3: 0-21
- ESP32-C6: 0-7
- ESP32-H2: 7-14
*/

//  GIPO 0 はバッテリー計測用にしてしまったので 1 に変更しました。
#define BUTTON_PIN_BITMASK (1ULL << GPIO_NUM_1) // GPIO 1 bitmask for ext1

RTC_DATA_ATTR int bootCount = 0;

/*
Method to print the reason by which ESP32
has been awaken from sleep
*/
void print_wakeup_reason(){
  esp_sleep_wakeup_cause_t wakeup_reason;

  wakeup_reason = esp_sleep_get_wakeup_cause();

  switch(wakeup_reason)
  {
    case ESP_SLEEP_WAKEUP_EXT0 : Serial.println("Wakeup caused by external signal using RTC_IO"); break;
    case ESP_SLEEP_WAKEUP_EXT1 : Serial.println("Wakeup caused by external signal using RTC_CNTL"); break;
    case ESP_SLEEP_WAKEUP_TIMER : Serial.println("Wakeup caused by timer"); break;
    case ESP_SLEEP_WAKEUP_TOUCHPAD : Serial.println("Wakeup caused by touchpad"); break;
    case ESP_SLEEP_WAKEUP_ULP : Serial.println("Wakeup caused by ULP program"); break;
    default : Serial.printf("Wakeup was not caused by deep sleep: %d\n",wakeup_reason); break;
  }
}

void setup(){
  Serial.begin(115200);

  if(Serial) {
    return; // USB接続はsleepさせない
  }

  //Increment boot number and print it every reboot
  ++bootCount;
  Serial.println("Boot number: " + String(bootCount));

  //Print the wakeup reason for ESP32
  print_wakeup_reason();

  /*
  First we configure the wake up source
  We set our ESP32 to wake up for an external trigger.
  There are two types for ESP32, ext0 and ext1, ext0 
  don't support ESP32C6 so we use ext1.
  */

  //If you were to use ext1, you would use it like
  esp_sleep_enable_ext1_wakeup(BUTTON_PIN_BITMASK,ESP_EXT1_WAKEUP_ANY_HIGH);

  //Go to sleep now
  Serial.println("Going to sleep now");
  delay(1000); //Take some time to open up the Serial Monitor
  esp_deep_sleep_start();
  Serial.println("This will never be printed");
}

void loop(){
  //This is not going to be called
}
```
### 結果
| active | DeepSleep |
|:-:|:-:|
| <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203949/a2e2c712-1b04-44b5-959e-4a1d2572fa41.png" width=50%>  | <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203949/6a03a5da-6771-484f-9361-cd7fced8c47b.png" width=50%>  |
| 19.3mA | 22.5μA |

22.5μA。公式とはいかず。まぁバッテリー電圧の読み取り用の抵抗が付いていますし。**active** の 19.3mAですが起動直後です。WiFi接続時は 40mAを超えることもありました。
 
# Ambient
ESP32といえば WiFi。読み取った値をクラウドに保存して可視化しましょう。そこでお手軽に使える [Ambient](https://ambidata.io/) です。


# SwichBot温湿度計
我が家には屋内と屋外に「SwichBot温湿度計」があります。このSwichBotからは、アドバタイズパケット(BLE)が常に飛んでいます。これキャッチしてAmbient に送信して10分間DeepSleepする運用をテストしたいと思います。

```mermaid
graph LR
  SwichBot温湿度計-->|BLE|A[ESP32C6]-->|WiFi|Ambient-->DeepSleep-->|10分|A
```
# ソースコード
2台のSwitchBot温湿度計があるので、２台のBLEの取得を待ってます。

あと、WiFiとBLEを使うので、デフォルトのプログラム領域サイズだと書き込みません。OTAを使わないので、NoOTAに設定を変えました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203949/a0b032c2-8938-4f76-be6b-550ce166acbe.png" width=70%>

```cpp
#include <WiFi.h>
#include <BLEDevice.h>
#include "Ambient.h"

// WiFi
const char *ssid = "xxxxxxxxxxxxxxx";
const char *password = "yyyyyyyyyyyyy";
IPAddress ip(192, 168, 0, 231); // 固定IP yourself
IPAddress gateway(192, 168, 0, 1); // yourself
IPAddress subnet(255, 255, 255, 0); // yourself
IPAddress DNS(192, 168, 0, 1); // yourself
WiFiClient client;

// Ambient
unsigned int channelId = 99999;
const char *writeKey = "zzzzzzzzzzzzzzzzzz";
Ambient ambient;

// SwitchBot
int scanTime = 3; //In seconds
BLEScan* pBLEScan;
BLEAddress addr01       = BLEAddress("nn:nn:nn:nn:nn:nn"); // 内
BLEAddress addr02       = BLEAddress("vv:vv:vv:vv:vv:vv"); // 外
BLEUUID serviceUUID     = BLEUUID("cba20d00-224d-11e6-9fb8-0002a5d5c51b");
BLEUUID serviceDataUUID = BLEUUID("00000d00-0000-1000-8000-00805f9b34fb");
float temp[] = {0.0, 0.0};
int humi[] = {0, 0};

// DeepSleep
#define TIME_TO_SLEEP  (10*60) // go to sleep (in seconds)


class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
    void onResult(BLEAdvertisedDevice advertisedDevice) {
      if(!advertisedDevice.getAddress().equals(addr01) && 
          !advertisedDevice.getAddress().equals(addr02)){
        return;
      }
      if(!advertisedDevice.haveServiceUUID()) return;
      if(!advertisedDevice.getServiceUUID().equals(serviceUUID)) return;
      
      if(!advertisedDevice.haveServiceData()) return;
      String s = advertisedDevice.getServiceData();
      if(!advertisedDevice.getServiceDataUUID().equals(serviceDataUUID)) return;
      
      const char* servicedata = s.c_str();
      int battery = servicedata[2] & 0b01111111;
      bool isTemperatureAboveFreezing = servicedata[4] & 0b10000000;
      float temperature = ( servicedata[3] & 0b00001111 ) / 10.0 + ( servicedata[4] & 0b01111111 );
      if(!isTemperatureAboveFreezing){
        temperature = -temperature;
      }
      int humidity = servicedata[5] & 0b01111111;
      
      int dev = (advertisedDevice.getAddress().equals(addr01)) ? 0 : 1;
      temp[dev] = temperature;
      humi[dev] = humidity;
    }
};

void setup() {
    Serial.begin(115200);

    // バッテリー接続確認
    //  PC接続(USB)の時はDeepSleepさせたくない
    bool bBattery = false;
    for(int i=0; i<10 && !Serial; i++) {
        delay(100);
    }
    if(!Serial) {
        // バッテリー駆動
        bBattery = true;
        Serial.println("use Battery");
    }

    // Battery
    pinMode(A0, INPUT);         // battery volt: Configure A0 as ADC input
    uint32_t Vbatt = 0;
    for(int i = 0; i < 16; i++) {
        Vbatt += analogReadMilliVolts(A0); // Read and accumulate ADC voltage
    }
    float Vbattf = 2 * Vbatt / 16 / 1000.0;     // Adjust for 1:2 divider and convert to volts

    // SwitchBot
    BLEDevice::init("");
    pBLEScan = BLEDevice::getScan(); //create new scan
    pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks(), true);
    pBLEScan->setActiveScan(true); //active scan uses more power, but get results faster
    pBLEScan->setInterval(100);
    pBLEScan->setWindow(99);  // less or equal setInterval value

    for(int i=0; i<10 && (temp[0] == 0.0 || temp[1] == 0.0); i++) { // 取れるまで10回チャレンジ
        BLEScanResults* foundDevices = pBLEScan->start(scanTime, false);
        pBLEScan->clearResults();   // delete results fromBLEScan buffer to release memory
    }
    BLEDevice::deinit();

    // WiFi
    if (connect()) {
        // Ambient
        sendAmbient(temp[0], humi[0], temp[1], humi[1], Vbattf);
    }

    // 完全にWi-Fi関連の電源をオフ
    WiFi.disconnect(true);

    if(bBattery) {
        deepSleep();
    }

    Serial.println("No Battery");
}

void loop() {
}

bool connect()
{
    // initialize WiFi
    int wait = 100;
    if (!WiFi.config(ip, gateway, subnet, DNS))
    { // Set fixed IP address
        Serial.println("STA Failed to configure");
        return false;
    }
    delay(10);
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED)
    {
        delay(500);
        Serial.print(".");
        if (wait <= 0)
            return false;
        wait--;
    }
    Serial.print("Connected to ");
    Serial.println(ssid);

    // initialize Ambient
    if(!ambient.begin(channelId, writeKey, &client)) {
        Serial.println("faild to ambient.begin");
        return false;
    }

    return true;
}

void sendAmbient(float temp1, int humi1, float temp2, int humi2, float voltage)
{
    // Ambient に送る
    Serial.print("send to Ambient:");
    Serial.print("temp1: ");
    Serial.print(temp1);
    Serial.print(",humi1: ");
    Serial.print(humi1);
    Serial.print(",temp2: ");
    Serial.print(temp2);
    Serial.print(",humi2: ");
    Serial.print(humi2);
    Serial.print(",voltage: ");
    Serial.println(voltage);
    ambient.set(1, temp1);
    ambient.set(2, humi1);
    ambient.set(3, temp2);
    ambient.set(4, humi2);
    ambient.set(5, voltage);
    if(!ambient.send()) {
        Serial.println("faild to ambient.send");
    }
    delay(1000);
}

#define uS_TO_S_FACTOR 1000000ULL  /* Conversion factor for micro seconds to seconds */

void deepSleep(){
    esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR);

    Serial.println("Going to sleep now");
    Serial.flush(); 
    esp_deep_sleep_start();
}

```
# テスト考察

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203949/a818f1ef-d901-4aea-8a9d-67eadc36eec8.png" width=50%>

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203949/8d171f85-a964-4baf-bac4-0ca05a049f94.png" width=80%>

バッテリー：AEC551140 210mAh 3.7V

| 開始 | 終了 |
|:-:|:-:|
| 2025/3/13 16:48:40 |2025/3/21 3:21:44|
| 4.00V  |3.04V|

計測時間：178:33:04 (7日と10時間33分)
アップロード試行回数：1071、
  そのうち 成功回数：887、失敗回数：184

バッテリーが想定通りの曲線を描いたので満足。
210mAh だとこんなもんなんだという感じ。
WiFiのアップロード失敗率１７％。ちょっとこれは問題。
ほとんどが ```WiFi.begin() で WiFi.status() が WL_CONNECTED``` にならないパターン。
うちのLAN環境のせいなのかな・・・・。

:::note alert
後で気付いたけど、このバッテリーは 1C ってことなので、210mA以上流してはいけません！！バッテリー壊れます！！ESP32はWiFi時に300mA超える場合がありますｎ
:::

あと、過放電を防ぐためにも急落する前の3.4V辺りで止めないといけないことが分かった。

# Special thanks

https://qiita.com/takeru@github/items/f42381e8482c3bf484e7