ESPHomeで OTA safe mode への強制移行

M5Stack Gray で esp32_ble_tracker で、Bluetooth Low Energy (BLE)のadvertise をscan して得られた アドレスと名前をとりあえず表示する YAML を作成してみたが、esp32_ble_tracker: を含めると Setting WiFi mode failed! となり WiFi が使えなくなる。

その場合でも esp32_ble_tracker 自体は動作しているため、電源ボタンでリセットしても、WiFiで書き込みができる Over-The-Air (OTA) safe mode に移行することはなく、同じことになり、OTAでの書き込みができなくなる。WiFiが使えない状態なので、WiFi経由でOTA safe mode に移行させることもできない。

OTAでのアップデートができないのは不便なので、M5Stack Gray の右ボタン(Button C)を押したら、強制的に OTA safe mode に移行するYAML設定ファイルを作成してみた。プログラムから強制的に OTA safe mode でリブートするための関数を調べるのに手間取ったので、備忘録として残しておく。

YAML設定ファイル

substitutions:
  device_name: m5stack_grey_with_ILI9342C
  _friendly_name: "ESPHome Web c5a2d0 Grey (ILI9342C)"

esphome:
  name: esphome-web-c5a2d0
  friendly_name: $_friendly_name

esp32:
  board: m5stack-grey # M5Stack Grey
  framework:
    type: arduino

# Enable logging
logger:
  level: VERBOSE # Remove this line if normal use

# Enable Home Assistant API
api:
  encryption:
    key: "sASMDEVYpJ1rRw5bTmbv0U7iB6eZfs4Ct9osYp5PFfo="

# Enable OTA (Over-The-Air)
ota: 
  id: my_ota
    
# WiFi Access Point
wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Esphome-Web-C5A2D0"
    password: "fPKaK7CujFeA"

# Fallback mechanism when WiFi Access Point cannot be connected to
captive_portal:

# SPI setting (needed for display)
spi: # M5Stack Grey
  clk_pin: GPIO18
  mosi_pin: GPIO23
  miso_pin: GPIO19

# Fonts (needed for text display)
font:
  - file: "gfonts://Roboto" # Google Fonts
    id: roboto24
    size: 24

# Dimmable led (PWM) for display backlight 
output: # M5Stack Grey
  - platform: ledc
    pin: GPIO32
    id: gpio_32_backlight_pwm

# Display backlight
light:  # M5Stack Grey
  - platform: monochromatic
    output: gpio_32_backlight_pwm
    name: "Display Backlight"
    id: back_light
    restore_mode: ALWAYS_ON

# Display for M5Stack Grey (ILI9342C)
display:
  - platform: ili9xxx
    model: M5STACK # M5Stack Grey
    cs_pin: GPIO14
    dc_pin: GPIO27
    reset_pin: GPIO33
    #rotation: 0
    id: basic_display
    #
    update_interval: 2s
    lambda: |-
      if (!id(ble_data_map).empty()) {
        int i = 0;
        for (const auto &kv : id(ble_data_map)) {
          it.printf(0, i * 25, id(roboto24), "%s: %s", kv.first.c_str(), kv.second.c_str());
          i++;
        }
      } else {
        it.print(0, 0, id(roboto24), "No Data");
      }

# M5Stack Basic Buttons
binary_sensor: # M5Stack Grey
  - platform: gpio
    pin:
      number: GPIO39
      inverted: false
    name: ButtonA
    on_press:
      then:
        - esp32_ble_tracker.start_scan

  - platform: gpio
    pin:
      number: GPIO38
      inverted: false
    name: ButtonB
    on_press:
      then:
        - esp32_ble_tracker.stop_scan

  - platform: gpio
    pin:
      number: GPIO37
      inverted: false
    name: ButtonC
    on_press:
      then:
        - lambda: |-
            id(my_ota).set_safe_mode_pending(true); // set next boot to safe mode
            App.safe_reboot(); // OTA safe boot

# BLE Tracker
esp32_ble_tracker:
  scan_parameters:
    continuous: false
  on_ble_advertise:
    then:
      - lambda: |-
          id(ble_data_map)[x.address_str()] = x.get_name();

# Global variable
globals:
  - id: ble_data_map
    type: std::map<std::string, std::string>

プログラムの動作

BLE scan

正しくWiFiが動作しないので意味がないが、上記の設定ファイルでは、左ボタン(Button A)を押すと esp32_ble_tracker.start_scan によってBLE の scan を開始し、右ボタン (Button B)を押すと esp32_ble_tracker.stop_scan によって scan を停止する。

次のコードで BLEの advertise を受信したら、 ble_data_map に addressとnameを蓄積する。

  on_ble_advertise:
    then:
      - lambda: |-
          id(ble_data_map)[x.address_str()] = x.get_name();

ble_data_map は std::map<std::string, std::string> 型のグローバル変数である。

globals:
  - id: ble_data_map
    type: std::map<std::string, std::string>

ディスプレイへの表示は、display: の lambda で ble_data_map から要素を順に取り出してaddr と name を表示する。

    lambda: |-
      if (!id(ble_data_map).empty()) {
        int i = 0;
        for (const auto &kv : id(ble_data_map)) {
          it.printf(0, i * 25, id(roboto24), "%s: %s", kv.first.c_str(), kv.second.c_str());
          i++;
        }
      } else {
        it.print(0, 0, id(roboto24), "No Data");
      }

OTA safe mode

先述のように Button C を押すと次のコードで OTA safe mode になるようにreboot する。set_safe_mode_pending(true)で次のブートでsafe mode になるよう設定する。App.safe_reboot()は、safe mode にすべきかのフラグに応じて、safe mode もしくは通常モードでリブートする。

            id(my_ota).set_safe_mode_pending(true); // set next boot to safe mode
            App.safe_reboot(); // OTA safe boot

このコードを使う場合には、 ota: で id を定義する必要がある。

# Enable OTA (Over-The-Air)
ota: 
  id: my_ota

上記コードの部分が実行された際に、USB シリアル経由でログをとると、次のように Over-The-Air で待ちに入ることがわかる。