# Open Thread Border Router (ESP32-C6)

**URL:** https://forum.ficusonline.com/t/topic/562
**Category:** Firmware
**Tags:** thread, esp-idf, otbr, platformio, esp32
**Created:** [2026 年 7 月 3 日午後 11:59 UTC](https://forum.ficusonline.com/t/topic/562 "2026-07-03T23:59:09Z")
**Posts on this page:** 11
**Page:** 1

<div class="post-metadata">

### Author: ![tk-fuse](https://forum.ficusonline.com/user_avatar/forum.ficusonline.com/tk-fuse/32/255_2.png) [@tk-fuse](https://forum.ficusonline.com/u/tk-fuse)
#### Post date: [2026 年 7 月 3 日午後 11:59 UTC](https://forum.ficusonline.com/t/topic/562/1 "2026-07-03T23:59:09Z")

</div>

## Open Thread Boder Router （OTBR）サンプルコード

**ESP32-C6** の開発ボードにOTBRのサンプルファームウェアを書き込みます。

> 注） **ESP32-C6** はWi-Fi（2.4GHz）とThread（802.15.4, 2.4GHz）が **同じ2.4GHz帯のRFを共有** しているため、 **同時使用時は干渉を避けるためコエグジスタンス（共存）制御** が行われます。このためスループットが下がる場合があります。

> 注）Wi-FiとThreadを独立して運用するため、 **ホスト（Wi-Fi担当）にESP32系チップ** 、 **RCP（Radio Co-Processor、Thread担当）にESP32-H2** をUART接続した構成が選ばれます。

### 開発環境

VSCode + PlatformIO, esp32-c6-devkitc-1

> **[ESP32-C6-DevKitC-1 v1.2 - ESP32-C6 - — esp-dev-kits latest documentation](https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32c6/esp32-c6-devkitc-1/user_guide.html)**

 ![esp32-c6-devkitc-1-pin-layout](https://forum.ficusonline.com/uploads/default/original/2X/d/d21afda60696dec0df538f70df3739c326eaf636.png)

> **[CMake - Espressif IoT Development Framework — PlatformIO latest documentation](https://docs.platformio.org/en/latest/frameworks/espidf.html#project-structure)**
>
> Project Structure

- 
### [PlatformIO Registry](https://registry.platformio.org/tools/platformio/framework-espidf)
- 
### [Embedding Binary Data](https://docs.platformio.org/en/latest/platforms/espressif32.html#embedding-binary-data)
- 
### [arduino-esp32/tools/partitions/default\_16MB.csv at master · espressif/arduino-esp32 · GitHub](https://github.com/espressif/arduino-esp32/blob/master/tools/partitions/default_16MB.csv)

* * *

`platformio.ini`

```auto
[platformio]
src_dir = main

[env:esp32-c6-devkitc-1]
platform = espressif32
board = esp32-c6-devkitc-1
framework = espidf

upload_port = /dev/ttyACM0
monitor_port = /dev/ttyACM0
monitor_speed = 115200

upload_flags =
    --before=default_reset
    --after=hard_reset

board_build.partitions = partitions.csv

```

OTBRサンプルコード

> **[esp-idf/examples/openthread/ot\_br at master · espressif/esp-idf](https://github.com/espressif/esp-idf/tree/master/examples/openthread/ot_br)**
>
> master/examples/openthread/ot\_br

> 注） `[platformio]`で`src_dir=main`を指定すれば、mainディレクトリをsrcディレクトリへ名称変更する必要なし

上記のplatformio.iniをプロジェクトフォルダ`ot_br`へコピー。

VSCodeのPIO HomeからOpen Projectをクリックし、ot\_brフォルダを選択します。

以下のボード設定ファイルが自動生成されるので、WiFiとThreadを同時に有効にするため  
一部編集します。

`sdkconfig.esp32-c6-devkitc-1`

```ini
#
# Wireless Coexistence
#
# default:
CONFIG_ESP_COEX_ENABLED=y
# default:
CONFIG_ESP_COEX_SW_COEXIST_ENABLE=y
# default:
# CONFIG_ESP_COEX_EXTERNAL_COEXIST_ENABLE is not set
# default:
# CONFIG_ESP_COEX_POWER_MANAGEMENT is not set
# default:
# CONFIG_ESP_COEX_GPIO_DEBUG is not set
# end of Wireless Coexistence

#
# ESP-STDIO
#
# default:
CONFIG_ESP_CONSOLE_UART_DEFAULT=y
# default:
# CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG is not set
# default:
# CONFIG_ESP_CONSOLE_UART_CUSTOM is not set
# default:
# CONFIG_ESP_CONSOLE_NONE is not set
# default:
# CONFIG_ESP_CONSOLE_SECONDARY_NONE is not set
# default:
CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG=y
# default:
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG_ENABLED=y
# default:
CONFIG_ESP_CONSOLE_UART=y
# default:
CONFIG_ESP_CONSOLE_UART_NUM=0
# default:
CONFIG_ESP_CONSOLE_ROM_SERIAL_PORT_NUM=0
# default:
CONFIG_ESP_CONSOLE_UART_BAUDRATE=115200
# end of ESP-STDIO

#
# Thread 15.4 Radio Link
#
CONFIG_OPENTHREAD_RADIO_NATIVE=y
# CONFIG_OPENTHREAD_RADIO_SPINEL_UART is not set
# CONFIG_OPENTHREAD_RADIO_SPINEL_SPI is not set
# CONFIG_OPENTHREAD_RADIO_154_NONE is not set
# end of Thread 15.4 Radio Link

```

ビルド（ビルドアイコン_レ_または下記コマンド）

```auto
$ pio run
or
$ rm -rf .pio && pio run

```

PIOのバグ？によるエラー（131行をコメントアウト）

`~/.platformio/packages/framework-espidf/components/openthread/CMakeLists.txt`

```ini
PRIVATE
 "PACKAGE_VERSION=\"${IDF_VERSION_FOR_OPENTHREAD_PACKAGE}-${OPENTHREAD_VERSION}\""
#"OPENTHREAD_BUILD_DATETIME=\"${OT_BUILD_TIMESTAMP}\""
        )

```

ボード詳細確認

```auto
$ pio pkg exec -- esptool.py --port /dev/ttyACM0 flash_id

```

OTBRのWiFi、Thread設定

```shell
$ pio device monitor
esp32c6> ot wifi
---wifi parameter---
connect -s <ssid> -p <psk> : connect to a wifi network with an ssid and a psk
connect -s <ssid> : connect to a wifi network with an ssid
disconnect : wifi disconnect
state : get wifi state, disconnect, connect or reconnecting
mac <role> : get mac address of wifi netif, <role> can be 'sta' or 'ap'
config : get stored wifi configurations
config clear : clear stored wifi configurations
---example---
join a wifi:
ssid: threadcertAP
psk: threadcertAP : wifi connect -s threadcertAP -p threadcertAP
join a wifi:
ssid: threadAP
does not have a psk : wifi connect -s threadAP
get wifi state : wifi state
wifi disconnect once : wifi disconnect
get mac address of Wi-Fi station : wifi mac sta
get mac address of Wi-Fi soft-AP : wifi mac ap
get stored wifi configurations : wifi config
clear stored wifi configurations : wifi config clear
Done

esp32c6> ot help
attachtime
ba
bbr
br
bufferinfo
ccathreshold
ccm
channel
child
childip
childmax
childrouterlinks
childsupervision
childtimeout
coap
contextreusedelay
counters
csl
dataset
debug
delaytimermin
detach
deviceprops
diag
discover
dns
domainname
dua
eidcache
eui64
extaddr
extpanid
factoryreset
fake
fem
ifconfig
instanceid
ipaddr
ipmaddr
joinerport
keysequence
leaderdata
leaderweight
log
mac
meshdiag
mleadvimax
mliid
mode
multiradio
nat64
neighbor
netdata
netstat
networkdiagnostic
networkidtimeout
networkkey
networkkeyref
networkname
nexthop
panid
parent
parentpriority
partitionid
ping
platform
pollperiod
preferrouterid
prefix
promiscuous
pskc
pskcref
rcp
region
releaserouterid
reset
rloc16
route
router
routerdowngradethreshold
routereligible
routeridrange
routerselectionjitter
routerupgradethreshold
scan
service
singleton
srp
state
targetpower
test
thread
timeinqueue
tvcheck
txpower
udp
unsecureport
uptime
vendor
verhoeff
version
curl
heapdiag
ip
loglevel
mcast
tcpsockclient
tcpsockserver
udpsockclient
udpsockserver
wifi
Done

```

WiFi接続

```shell
esp32s6> ot wifi connect -s threadcertAP -p threadcertAP

```

WiFiステータス

```shell
esp32c6> ot wifi state

```

Threadネットワーク作成

```shell
esp32c6> ot dataset init new
esp32c6> ot dataset commit active
esp32c6> ot ifconfig up
esp32c6> ot thread start

```

Threadステータス

```shell
esp32c6> ot state

```

### Threadネットワークの確認・設定

#### 確認コマンド

```shell
esp32c6> ot dataset active -x

```

現在のデータセットを16進数で表示（全情報含む）

個別に確認する場合：

```shell
esp32c6> ot networkname
esp32c6> ot channel
esp32c6> ot panid
esp32c6> ot networkkey
esp32c6> ot extpanid

```

* * *

#### 設定コマンド（新規作成の場合）

```shell
esp32c6> ot dataset init new
esp32c6> ot dataset networkname MyNetwork
esp32c6> ot dataset channel 15
esp32c6> ot dataset networkkey 00112233445566778899aabbccddeeff
esp32c6> ot dataset panid 0x1234
esp32c6> ot dataset commit active

```

* * *

#### 既存ネットワークに参加する場合

他のThreadデバイスと同じネットワークキー・チャンネル・PANIDを設定してから：

```shell
esp32c6> ot ifconfig up
esp32c6> ot thread start

```

* * *

### 各種操作

#### sdkconfigファイルの設定・変更

以下のコマンドを実行するか、直接デバイスのsdkconfigファイルを編集して下さい。

```shell
$ pio run -t menuconfig
(Top)
                 Espressif IoT Development Framework Configuration
    Build type --->
    App Update config ----
    Bootloader config --->
    Security features --->
    Application manager --->
    Boot ROM Behavior --->
    ESP-TEE (Trusted Execution Environment) --->
    Serial flasher config --->
    ↓↓↓↓↓↓↓↓↓↓↓↓↓↓
[Space/Enter] Toggle/enter [ESC] Leave menu [S] Save
[O] Load [?] Symbol info [/] Jump to symbol
[F] Toggle show-help mode [C] Toggle show-name mode [A] Toggle show-all mode
[Q] Quit (prompts for save) [D] Save minimal config (advanced) [R] Reset to default value

```

#### ビルド環境・パッケージの削除（リセット）

```auto
$ rm -rf .pio managed_components dependencies.lock

```

* * *

## Open Thread CLI リファレンス

> <https://github.com/openthread/openthread/blob/main/src/cli/README.md>

```auto
esp32c6> ot attachtime
2d.07:03:04
Done

esp32c6> ot bbr
BBR Primary:
server16: 0x0C00
seqno: 54
delay: 5 secs
timeout: 3600 secs
Done

```

---

<div class="post-metadata">

### Author: ![tk-fuse](https://forum.ficusonline.com/user_avatar/forum.ficusonline.com/tk-fuse/32/255_2.png) [@tk-fuse](https://forum.ficusonline.com/u/tk-fuse)
#### Post date: [2026 年 7 月 3 日午後 11:59 UTC](https://forum.ficusonline.com/t/topic/562/2 "2026-07-03T23:59:23Z")

</div>

## OTBR推奨モデル

WiFiとThreadのRFを独立させるため、以下のモデルの組合わせによる構成をメーカは推奨しています。

> **[1. Hardware Platforms - - — ESP Thread BR latest documentation](https://docs.espressif.com/projects/esp-thread-br/en/latest/hardware_platforms.html)**

> **[GitHub - espressif/esp-thread-br: Espressif Thread Border Router SDK](https://github.com/espressif/esp-thread-br)**
>
> Espressif Thread Border Router SDK

Recommended main processor and RCP combinations:

| Main Processor | RCP | Use Case |
| --- | --- | --- |
| [ESP32-P4](https://www.espressif.com/en/products/socs/esp32-p4) | ESP32-C6 | High-performance applications with AI/ML capabilities |
| [ESP32-C5](https://www.espressif.com/en/products/socs/esp32-c5) | ESP32-H2 | Dual-band Wi-Fi (2.4 GHz + 5 GHz) support |
| [ESP32-C61](https://www.espressif.com/en/products/socs/esp32-c61) | ESP32-H2 | Wi-Fi 6 (802.11ax) support |
| [ESP32-S3](https://www.espressif.com/en/products/socs/esp32-s3) | ESP32-H2 | General-purpose with display/camera support |

---

<div class="post-metadata">

### Author: ![tk-fuse](https://forum.ficusonline.com/user_avatar/forum.ficusonline.com/tk-fuse/32/255_2.png) [@tk-fuse](https://forum.ficusonline.com/u/tk-fuse)
#### Post date: [2026 年 7 月 4 日午前 12:21 UTC](https://forum.ficusonline.com/t/topic/562/3 "2026-07-04T00:21:20Z")

</div>

前記事のesp-idfのサンプルコードを参考に、下記の開発キット用otbrのコード（ESP32をWiFi用とThread用に2個必要）をesp32-c6-devkitc-1単独で適用できるか検討。ボードのビルド条件の変更に加え、コードの修正も必要。OTA以外の動作確認。

> **[ESP32-C6-DevKitC-1 v1.2 - ESP32-C6-DevKitC-1 v1.2 - ESP32-C6 - — esp-dev-kits...](https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32c6/esp32-c6-devkitc-1/user_guide.html#esp32-c6-devkitc-1-v1-2)**
>
> \[中文\]

 ![esp32-c6-devkitc-1-v1.2-annotated-photo](https://forum.ficusonline.com/uploads/default/original/2X/0/0aa280cb43063e9ed34c0bcb2781e29e5f7575b5.jpeg)

 ![Screenshot from 2026-09-10 18-33-48](https://forum.ficusonline.com/uploads/default/original/2X/e/e7cbfa1bae4fb030bef1fa6b8c31ade579317e4d.png)

> **[esp-thread-br/examples/basic\_thread\_border\_router at main ·...](https://github.com/espressif/esp-thread-br/tree/main/examples/basic_thread_border_router)**
>
> main/examples/basic\_thread\_border\_router

```auto
$ pio run -t menuconfig

```

```sh
(Top) → ESP Thread Border Router Example

    Border router board type (Standalone dev kits) --->
    Board Configuration --->
    Border router RCP target (ESP32-C6) --->
[*] Enable the automatic start mode in Thread Border Router. (default value)
[*] Enable the web server in Thread Border Router. (default value)
[*] Enable SoftAP Wi-Fi configuration mode (default value)

```

```sh
(Top) → Component config → ESP-STDIO

    Channel for console output (Default: UART0) --->
    Channel for console secondary output (USB_SERIAL_JTAG PORT) --->

```

```sh
(Top) → Serial flasher config

[] Disable download stub (default value)
    Flash SPI mode (DIO) --->
    Flash Sampling Mode (STR Mode) --->
    Flash SPI speed (80 MHz) --->
    Flash size (16 MB) --->
[*] Detect flash size when flashing bootloader (default value)
    Before flashing (Reset to bootloader) --->
    After flashing (Reset after flashing) --->

```

```sh
(Top) → Config for OpenThread Examples

[*] Enable the automatic start mode of Thread network.
    External Console Commands --->

```

```sh
(Top) → Component config → Wi-Fi

[] WiFi AMPDU TX
[] WiFi AMPDU RX

(20) Minimum active time (default value)

```

```sh
(Top) → Component config → OpenThread → OpenThread → Thread Core Features

[*] Enable timing optimization

```

```sh
(Top) → Component config → OpenThread → OpenThread → Thread Core Features → OpenThread Stack Parameters

(20) The size of max MLE children entries

(8) Maximum backoffs times before declaring a channel access failure.

```

* * *

> 注）ESP32-C6単体でWiFiとRCP機能を有効にしたため、下記GPIOピンの設定は不要。

`sdkconfig.esp32-c6-devkitc-1`

```auto
#
# Board Configuration
#
# default:
CONFIG_PIN_TO_RCP_RESET=7
# default:
CONFIG_PIN_TO_RCP_BOOT=8
# default:
CONFIG_PIN_TO_RCP_TX=4
# default:
CONFIG_PIN_TO_RCP_RX=5
# end of Board Configuration

#
# OpenThread RCP Update
#
# default:
# CONFIG_AUTO_UPDATE_RCP is not set
# CONFIG_CREATE_OTA_IMAGE_WITH_RCP_FW is not set
# default:
CONFIG_DEFAULT_PIN_TO_RCP_RESET=7
# default:
CONFIG_DEFAULT_PIN_TO_RCP_BOOT=8
# default:
CONFIG_DEFAULT_PIN_TO_RCP_TX=17
# default:
CONFIG_DEFAULT_PIN_TO_RCP_RX=18
# end of OpenThread RCP Update

```

```sh
(Top) → ESP Thread Border Router Example → Board Configuration
                                                                                                       Espressif IoT Development Framework Configuration
(7) Pin to RCP reset (default value)
(8) Pin to RCP boot (default value)
(4) Pin to RCP TX (default value)
(5) Pin to RCP RX (default value)

```

```sh
(Top) → Component config → OpenThread RCP Update
                                                                                                       Espressif IoT Development Framework Configuration
[] Update RCP automatically (default value)
[] Create the OTA image with rcp for border router
(7) Default Pin to RCP reset (default value)
(8) Default Pin to RCP boot (default value)
(17) Default Pin to RCP TX (default value)
(18) Default Pin to RCP RX (default value)

```

* * *

### ソースのバックアップ（.pioとmanaged\_componentsは除外）

```auto
$ tar -czf $(date +%Y%m%d)_esp32-otbr-example.tar.gz \
--exclude='examples/basic_thread_border_router/.pio' \
--exclude='examples/basic_thread_border_router/managed_components' \
--exclude='examples/basic_thread_border_router/dependencies.lock' \
examples

```

* * *

PCやモバイルでOTBRのアクセスポイントへ接続し、ブラウザで以下のURLへアクセス。

[http://192.168.4.1](http://192.168.4.1)

 ![Screenshot from 2026-07-04 11-24-21](https://forum.ficusonline.com/uploads/default/original/2X/2/2db591d893aea8b9dde5adb57534e484290f1a03.png)

シリアルモニターで動作確認

```shell
$ pio devive monitor
esp32>ot wifi state
connected
Done

esp32c6> ot state
leader
Done

esp32c6> ot dataset active 
Active Timestamp: 1
Channel: 13
Wake-up Channel: 13
Channel Mask: 0xxxxxxxxxxxx
Ext PAN ID: xxxxxxxxxxxxxxxxxxxxx
Mesh Local Prefix: fdxx:xx:xx:xx::/64
Network Key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Network Name: OpenThread-xxxx
PAN ID: 0xadd9
PSKc: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Security Policy: 672 onrc 0
Done

```

* * *

STAモード後ルータのIPを確認してルータ管理ページへアクセス  
[http://192.168.xx.xx](http://192.168.xx.xx)

 ![Screenshot from 2026-07-08 15-53-46](https://forum.ficusonline.com/uploads/default/original/2X/3/3bf87210daf5c68c5ebd83235ae01527e2571e72.png)

Wifi設定やThreadネットワークの設定をするためのウェブUIが用意されていますが、別途以下コマンドで作成する必要があります。

#### SPIFFSイメージの生成

```bash
$ python ~/.platformio/packages/framework-espidf/components/spiffs/spiffsgen.py 0x200000 \
../../components/esp_ot_br_server/frontend \
.pio/build/esp32-c6-devkitc-1/web_storage.bin

```

`spiffsgen.py` はESP-IDFに付属するツールで、指定したディレクトリの中身をSPIFFS（SPI Flash File System）形式のイメージファイルにまとめます。

- **`0x200000`** : 生成するSPIFFSイメージのサイズ（バイト単位の16進数）。この場合は 2,097,152 バイト = **2MB** を指定しています。パーティションテーブル`partitions.csv`で定義したSPIFFS用パーティションのサイズと一致させる必要があります。
- **`../../components/esp_ot_br_server/frontend`** : イメージ化する元のディレクトリ。Web UI用のHTML/CSS/JSファイルなどが入っています。
- **`.pio/build/esp32-c6-devkitc-1/web_storage.bin`** : 出力先のバイナリファイル名。ビルド後にこのファイルとしてSPIFFSイメージが生成されます。

#### フラッシュへの書き込み

```bash
$ python ~/.platformio/packages/tool-esptoolpy/esptool.py --chip esp32c6 -p /dev/ttyACM0 \
-b 460800 write_flash 0x820000 .pio/build/esp32-c6-devkitc-1/web_storage.bin

```

`esptool.py` はEspressif製チップにシリアル経由でファームウェアやデータを書き込むための標準ツールです。

- **`--chip esp32c6`** : 対象チップがESP32-C6であることを指定。
- **`-p /dev/ttyACM0`** : 書き込みに使うシリアルポート（Linuxのデバイスファイル）。
- **`-b 460800`** : 通信ボーレート（460800bps）。高速書き込みのための設定です。
- **`write_flash 0x820000 ...web_storage.bin`** : `0x820000`という **フラッシュ上のアドレス** に、先ほど生成した`web_storage.bin`を書き込みます。このアドレスはパーティションテーブル`partitions.csv`で定義されたSPIFFS（またはstorage）パーティションの開始オフセットと一致している必要があります。

`partitions.csv`

```auto
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x10000,
otadata, data, ota, 0x19000, 0x2000,
phy_init, data, phy, 0x1b000, 0x1000,
ota_0, app, ota_0, 0x20000, 4M,
ota_1, app, ota_1, 0x420000, 4M,
web_storage, data, spiffs, 0x820000, 2M,
rcp_fw, data, spiffs, 0xa20000, 2M,

```

> 注）rcp\_fw：OTBRをESP32-C3とESP32-C6の2台で構成（デフォルトの構成）しWi-FiとRCPをそれぞれ専用のマイコンで制御する場合

今回は`Platformio->Build Filesystem Image`は使用せず（使用する場合、dataディレクトリを作成し、ここにhtmlファイルを格納）、以下のPythonスクリプトによりFS用イメージを作成・アップロードを行います。

build\_spiffs.py

```python
# build_spiffs.py
Import("env")
import subprocess

def before_build(source, target, env):
    subprocess.run([
        "python",
        env.subst("$PROJECT_PACKAGES_DIR") + "/framework-espidf/components/spiffs/spiffsgen.py",
        "0x200000",
        "../../components/esp_ot_br_server/frontend",
        env.subst("$BUILD_DIR") + "/web_storage.bin"
    ], check=True)

def after_upload(source, target, env):
    port = env.subst("$UPLOAD_PORT")
    subprocess.run([
        "python",
        env.subst("$PROJECT_PACKAGES_DIR") + "/tool-esptoolpy/esptool.py",
        "--chip", "esp32c6",
        "-p", port,
        "-b", "460800",
        "write_flash", "0x820000",
        env.subst("$BUILD_DIR") + "/web_storage.bin"
    ], check=True)

env.AddPreAction("buildprog", before_build)
env.AddPostAction("upload", after_upload)

```

* * *

## 機能拡張コマンド

> <https://github.com/espressif/esp-thread-br/blob/main/components/esp_ot_cli_extension/README.md>

```shell
esp32c6> ot ip
---ip parameter---
print : print all ip on each interface of lwip
add <ifname> <ifaddr> : add an address onto an interface of lwip
del <ifname> <ifaddr> : delete an address from an interface of lwip
Done
esp32c6> 
esp32c6> ot loglevel
---loglevel---
set <TAG> <level> : set log level of the <TAG> to <level>
---example---
loglevel set * 3 : set log level of all the tags to INFO
loglevel set OPENTHREAD 0 : set log level of OpenThread to None
loglevel set wifi 4 : set log level of Wi-Fi to DEBUG
----Note----
Support 6 levels : 0(NONE), 1(ERROR), 2(WARN), 3(INFO), 4(DEBUG), 5(VERBOSE)
The log level of the tags cannot be bigger than the maximum log level
You can set the maximum log level via:
idf.py menuconfig > Component config---> Log output---> Maximum log verbosity
Done

```

---

<div class="post-metadata">

### Author: ![tk-fuse](https://forum.ficusonline.com/user_avatar/forum.ficusonline.com/tk-fuse/32/255_2.png) [@tk-fuse](https://forum.ficusonline.com/u/tk-fuse)
#### Post date: [2026 年 7 月 8 日午後 1:08 UTC](https://forum.ficusonline.com/t/topic/562/4 "2026-07-08T13:08:03Z")

</div>

## IKEA Matter デバイスの登録

事前に、_Settings —\> Device & Services—\> Integrations_ で Matter, Thread, Open Thread Boarder Router の登録を行って下さい。

 ![Screenshot from 2026-07-08 21-57-34](https://forum.ficusonline.com/uploads/default/original/2X/a/abb84a243c07f906cebf2746f1804f04d43fa764.png)

#### Open Thread Border Router

ベーシック認証にした場合、ユーザ:パスワードを追加。

> `http://user:password@<otbr_local_ip>`

### IKEA対象デバイス

- KAJPLATSLED bulb E17 810 lumen, smart white spectrum/globe/opal white

> ファクトリリセットはON/OFF6回

- BILRESA Remote control, white smart/dual button

> ファクトリリセットは表面ケースを開けて現れる真ん中のボタンを長押し。

 ![kajplats-starter-kit-smart-white-spectrum](https://forum.ficusonline.com/uploads/default/original/2X/d/d49401ba847ffd6067febcb416754de11b984389.jpeg)

BILRESA Remote control を単独のMatterデバイスとしてHome AssistantやApple Homeなどに登録すると、現時点ではMatter Binding（デバイス同士の直接関連付け）をサポートしていないため、KAJPLATS LED bulb E17 810 lumen と直接ペアリングして制御することはできません。

**直接ペアリングを使うか、Home Assistant経由で制御するかの二択** になります。

### 登録手続き

登録手続きができるのはモバイルアプリからのみとなっています。予めAndroid, iOS版Home Assistantアプリをインストールして下さい。

> **[Home Assistant Companion Docs](https://companion.home-assistant.io/)**
>
> Welcome to the Home Assistant App docs

上記のモバイルアプリ経由で登録手続きを行います。`Thread`をクリックすると、デバイスの登録から選択したOTBRが表示されます(OpenThread-add9)。これを`Preferred network`に追加して下さい。

 ![Screenshot_20260708-205719](https://forum.ficusonline.com/uploads/default/original/2X/3/361e2743c7102ba6c544f98d5077190964e2b366.png)

`Used for Android + iOS Credentials` のチェックは必須です。

 ![Screenshot_20260708-205803](https://forum.ficusonline.com/uploads/default/original/2X/e/e9ca456db7253eb853230c53a261fc5e579a1b92.png)

Matterデバイスを登録する前に、以下のコンパニオンアプリからトラブルシューティングを選択し、Thread認証情報の同期を行って下さい（Threadネットワークのリセットなど、ネットワーク名を変更した場合に必要な手続きです）。

 ![Screenshot_20260708-205927](https://forum.ficusonline.com/uploads/default/original/2X/3/36016183b0d52e57db6515628bd2429371399aaf.png)

 ![Screenshot_20260708-205944](https://forum.ficusonline.com/uploads/default/original/2X/5/5b7141e80dec500112c0d5727174b2f11ea81051.png)

#### Matterデバイスの登録

> 注）携帯のWiFiとESP32-C6によるOTBRは、同じ2.4GHz帯のSSIDである必要があります。

Matterをクリックし、`+Add device` → `No. It's new`

デバイスの外装や説明書に添付されているQRコードをスキャンし登録します。登録後デバイスはリストアップされます。

 ![Screenshot from 2026-07-08 20-53-24](https://forum.ficusonline.com/uploads/default/original/2X/3/3612ca51a9161ddd0564a2fc48d8ce6468d50187.png)

 ![Screenshot from 2026-07-08 20-53-06](https://forum.ficusonline.com/uploads/default/original/2X/b/b0ca78406b730430ce3ee5ad3b672822070c3f2d.png)

 ![Screenshot from 2026-07-08 20-54-04](https://forum.ficusonline.com/uploads/default/original/2X/a/a139482c313a4b3781363e9db9c53a3d19d9cba0.png)

#### トポロジー：OTBRとMatterデバイスの接続マップ

 ![Screenshot from 2026-07-09 09-25-02](https://forum.ficusonline.com/uploads/default/original/2X/9/9bb386916503ae1f9446041a805493f121f20ad9.png)

* * *

### オートメーションの作成

リモコンでLEDのON/OFF制御を行うには、オートメーションルールへの登録が必要です。

TriggerにはDeviceではなく **Entityを選択** 。

 ![Screenshot from 2026-07-10 10-02-35](https://forum.ficusonline.com/uploads/default/original/2X/3/3c66ab2a03497643a5709e882bdca9afb4030080.png)

optionalは選択しないで下さい（optionalを選択すると機能しません）。

 ![Screenshot from 2026-07-10 09-57-40](https://forum.ficusonline.com/uploads/default/original/2X/5/5069277cc5b39f23a07efdb6a6f095c4514776a9.png)

> 注）ペアリングで使用する場合には、ボタンの長押しで調光操作ができるのですが、オートメーションルールでボタンの長押し操作はイベントとして認識でき調光操作も可能です。  
> ボタン操作に機能を追加するには以下のプループリントを利用する方法もあります。

> **[IKEA Bilresa Dual Button Remote](https://community.home-assistant.io/t/ikea-bilresa-dual-button-remote/965815)**
>
> Blueprint for Ikea Bilresa Dual Button Remote What’s this all about? I wanted the IKEA Bilresa dual button remote to be as useful as possible without wiring every interaction by hand. This blueprint gives each button five different behaviors so...

> 注） BILRESAは、HAやMatter統合（Matter Server）が再起動した際、またはデバイス自身が再ネットワーク参加した際に、 **最後に押されたステータスを誤って再送する、あるいは無効な値を送り出す** 挙動が報告されています。これにより、HA側がイベントを正しく処理できなくなるケースがあります。

> **[IKEA BILRESA Matter Button 👈 \[Blueprint\] for Ikea \*Matter over Thread\* Button...](https://community.home-assistant.io/t/ikea-bilresa-matter-button-blueprint-for-ikea-matter-over-thread-button-actions-scripts-and-entities/962344)**
>
> IKEA BILRESA E2489 Matter Smart Button - Blueprint This blueprint provides automation for controlling the IKEA BILRESA E2489 Matter Smart Button in Home Assistant. Options for use (tested with lights and scripts): Turn on/off lights Control...

* * *

### ボタンの長押し+リリースを他のデバイス制御に利用

 ![Screenshot from 2026-07-24 22-05-39](https://forum.ficusonline.com/uploads/default/original/2X/2/2540d72dc053efd87468a3480e84dd0b43937b54.png)

* * *

## GRILLPLATSスマートプラグ追加

 ![grillplats-plug-smart](https://forum.ficusonline.com/uploads/default/original/2X/3/37de46fd56a19d5b8bac9d35b5bb0bec69573b87.jpeg)

 ![Screenshot from 2026-09-05 22-07-59](https://forum.ficusonline.com/uploads/default/original/2X/8/8624c8be4415a101233186d571d1a3db8190521f.png)

---

<div class="post-metadata">

### Author: ![tk-fuse](https://forum.ficusonline.com/user_avatar/forum.ficusonline.com/tk-fuse/32/255_2.png) [@tk-fuse](https://forum.ficusonline.com/u/tk-fuse)
#### Post date: [2026 年 7 月 24 日午前 4:35 UTC](https://forum.ficusonline.com/t/topic/562/5 "2026-07-24T04:35:00Z")

</div>

## トラブルシュート

> #### BILRESA Remote controlの反応が非常に遅い、または無反応。

原因はホストPC（HAコンテナのネットワークモードはhost）のファイヤーウォールによるmDNSのマルチキャストパケット拒否。

```auto
[ESP32-C6 (OTBR)] --Wi-Fi--> [HostPCのwpan0] == (host network) == [HAコンテナ]
                                        ↑
                                   ここのufwがブロック

```

```auto
$ sudo ufw allow in on wpan0 ### host LAN or Wifi interface
$ sudo ufw reload

```

> **[Ikea Bilresa switches - huge delays in responding - SOLVED](https://community.home-assistant.io/t/ikea-bilresa-switches-huge-delays-in-responding-solved/1004932/3)**
>
> @pk1966 I made an account just to thank you for this solution. I had been stuck on this issue for a while too, but the closest I got to solving it was extending the UDP packet timeout. @Homeassistant7051 You didn’t mention what OS or firewall you...

* * *

> #### Wi-Fi 再接続エラー

ルータ側の再起動時など接続断になった際 ot wifi state —\> reconnecting となり再接続しない。

再接続プロセスが欠如しているため追加。

`esp-thread-br/examples/basic_thread_border_router/main/idf_component.yml`

```diff
@@ -1,8 +1,8 @@
 ## IDF Component Manager Manifest File
 dependencies:
   espressif/mdns: "^1.0.0"
- espressif/esp_ot_cli_extension:
- version: "~2.0.0"
+ esp_ot_cli_extension:
+ path: ../../../components/esp_ot_cli_extension
   espressif/esp_rcp_update:
     version: "~1.6.0"
   esp_br_http_ota:

```

`esp-thread-br/components/esp_ot_cli_extension/src/esp_ot_wifi_cmd.c`

```diff
@@ -89,6 +89,12 @@
 #define PASSWORDMAXLEN 64
 ESP_EVENT_DEFINE_BASE(WIFI_ADDRESS_EVENT);
 
+/* ---- Wi-Fi reconnect watchdog ---- */
+static TaskHandle_t s_wifi_watchdog_task_handle = NULL;
+
+/* wifi_watchdog_task より前方で使うための前方宣言 */
+static esp_err_t wifi_join(const char *ssid, const char *password);
+
 static void handle_wifi_addr_init(void)
 {
     ESP_ERROR_CHECK(esp_event_handler_register(WIFI_ADDRESS_EVENT, WIFI_ADDRESS_EVENT_ADD_IP6,
@@ -105,6 +111,36 @@
                                                esp_netif_get_handle_from_ifkey("WIFI_STA_DEF")));
 }
 
+/*
+ * ウォッチドッグタスク本体。
+ * wifi_join() はブロッキング呼び出しのため、WIFI_EVENT_STA_DISCONNECTED の
+ * イベントハンドラ(handler_on_wifi_disconnect)から直接呼ばず、
+ * ここ(独立タスク)に処理を委譲することでイベントループタスクを長時間占有しないようにする。
+ */
+static void wifi_watchdog_task(void *arg)
+{
+ for (;;) {
+ /* handler_on_wifi_disconnect からの通知を待つ */
+ ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
+
+ char ssid[SSIDMAXLEN] = "";
+ char password[PASSWORDMAXLEN] = "";
+ esp_ot_wifi_config_get_ssid(ssid);
+ esp_ot_wifi_config_get_password(password);
+
+ if (strlen(ssid) == 0) {
+ /* 保存された設定がなければ何もしない */
+ continue;
+ }
+
+ ESP_LOGW(OT_EXT_CLI_TAG, "example_connect gave up, immediately retrying rejoin to \"%s\"", ssid);
+ wifi_join(ssid, password);
+ if (err != ESP_OK) {
+ ESP_LOGW(OT_EXT_CLI_TAG, "Watchdog rejoin attempt failed (0x%x), will retry on next disconnect cycle", err);
+ }
+ }
+}
+
 static void handler_on_wifi_disconnect(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
 {
     wifi_conn_retry_nums++;
@@ -112,6 +148,10 @@
     if (wifi_conn_retry_nums > CONFIG_EXAMPLE_WIFI_CONN_MAX_RETRY) {
         wifi_conn_retry_nums = 0;
         s_wifi_state = OT_WIFI_DISCONNECTED;
+ /* example_connect が諦めたと同時に、間を置かず自前で再接続を試みる */
+ if (s_wifi_watchdog_task_handle != NULL) {
+ xTaskNotifyGive(s_wifi_watchdog_task_handle);
+ }
     } else
 #endif
     {
@@ -193,6 +233,16 @@
         ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_MAX_MODEM));
         handle_wifi_addr_init();
         s_wifi_initialized = true;
+
+ /* ウォッチドッグ用タイマーとタスクは初回接続時に一度だけ生成する */
+ if (s_wifi_watchdog_task_handle == NULL) {
+ BaseType_t ok = xTaskCreate(wifi_watchdog_task, "wifi_watchdog", 4096, NULL,
+ tskIDLE_PRIORITY + 3, &s_wifi_watchdog_task_handle);
+ if (ok != pdPASS) {
+ ESP_LOGE(OT_EXT_CLI_TAG, "Failed to create wifi watchdog task");
+ s_wifi_watchdog_task_handle = NULL;
+ }
+ }
     }
 
     if (!s_wifi_handler_registered) {

```

* * *

> #### APIによるデータセット送信エラー

Changed channelでOKが出力されるが、実際はESP32のOTBR内でランダムなデータセットとして処理される。このためチャンネルの変更は反映されない。

 ![Screenshot from 2026-09-05 16-09-01](https://forum.ficusonline.com/uploads/default/original/2X/3/3fdb8f479df167565720c44776a4328f05f2ce35.png)

**ESP32側** :`handle_ot_resource_node_set_dataset_request`の`memset`漏れ

```auto
E (32362) web_api: handle_ot_resource_node_set_dataset_request(365): Cannot set Pending DatasetTlvs

```

`esp-thread-br/components/esp_ot_br_server/src/esp_br_web_api.c`

```diff
@@ -303,6 +303,7 @@
     otOperationalDatasetTlvs datasetTlvs;
     otOperationalDatasetTlvs datasetUpdateTlvs;
     otError ret = OT_ERROR_NONE;
+ memset(&dataset, 0, sizeof(otOperationalDataset));
     const char *content_format =
         cJSON_GetStringValue(cJSON_GetObjectItemCaseSensitive(request, ESP_OT_REST_CONTENT_TYPE_HEADER));
     const char *dataset_type =

```

**ESP32側** :`NOT_FOUND_handler`が404ステータスを返していなかった → HAの検出誤判定の原因

`python-otbr-api` がキーフォーマットをCAMAL CASE と誤判定—\> 実際はPASCAL CASE

`httpd_resp_set_status(req, HTTPD_404)`の呼び出しがなく、実際のHTTPステータスが常にデフォルトの200になっていたため。

HAのログ確認

```auto
homeassistant | 2026-09-05 11:03:31.417 DEBUG (MainThread) [python_otbr_api] Detected OTBR JSON key format: KeyFormat.CAMEL_CASE

```

`esp-thread-br/components/esp_ot_br_server/src/esp_br_web.c`

```diff
 /*-----------------------------------------------------
  Note：Handling for Client request
 -----------------------------------------------------*/
 static esp_err_t NOT_FOUND_handler(httpd_req_t *req)
 {
     esp_err_t ret = ESP_OK;
+ httpd_resp_set_status(req, HTTPD_404);
     cJSON *response = resource_status("404", "404 Not Found");
     ESP_GOTO_ON_ERROR(httpd_send_packet(req, response), exit, WEB_TAG, "Failed to response %s", req->uri);

```

> <https://github.com/home-assistant-libs/python-otbr-api/commit/71b97dceafa442b2db7ca9d632efd739ec522d7d>
>
> \* Support camel case and pascal case simultaneously
> 
> \* Rename init tests to lega…cy
> 
> \* Rename new tests to replace
> 
> \* Adjust comment
> 
> \* Flake8
> 
> \* Fix mypy
> 
> \* Detect version automatically

**HA側** :`set_channel()`が既存のPending Datasetをチェックせず上書きしようとしていた → 2026年の比較的最近の修正で解消済み(HA/ライブラリの更新が必要)

> <https://github.com/home-assistant-libs/python-otbr-api/commit/ea030abe08806863c157f6e49370c69ee5ece72f>
>
> set\_channel() builds its pending dataset from the active dataset alone,
> stepping… that timestamp by one second. A Thread mesh ignores a pending
> dataset that is not newer than the one it already holds, while the
> border router accepts the write all the same, so a channel change issued
> while another pending dataset was still waiting out its delay -- a
> previous channel change, or a network migration -- was silently dropped
> by the mesh with nothing telling the caller.
> 
> Superseding the in-flight dataset with a newer stamp would be the other
> option, but it races the delay timer: a device that misses the newer
> dataset before its timer expires applies the superseded one and has to
> find the network again through MLE Announce. Refusing is the honest
> answer; the caller can retry once the pending delay has passed.
> 
> Assisted-By: Claude Fable 5

---

<div class="post-metadata">

### Author: ![tk-fuse](https://forum.ficusonline.com/user_avatar/forum.ficusonline.com/tk-fuse/32/255_2.png) [@tk-fuse](https://forum.ficusonline.com/u/tk-fuse)
#### Post date: [2026 年 8 月 26 日午前 3:08 UTC](https://forum.ficusonline.com/t/topic/562/6 "2026-08-26T03:08:15Z")

</div>

## Firmware Update from WEB Broｗser

ブラウザからファームウェアを指定してESP32へアップロードする機能を追加。

 ![firm_update](https://forum.ficusonline.com/uploads/default/original/2X/d/dbd54215e766011a07aa04b60e2ec63b1289dc4e.png)

 ![about_version](https://forum.ficusonline.com/uploads/default/original/2X/7/7352e4da0378c31f1a38b4de5c206a152bd0833b.png)

以下新規・更新ファイル

新規作成  
`esp-thread-br/components/esp_ot_br_server/frontend/ota.html`

```auto
<!--
SPDX-FileCopyrightText: 2026
SPDX-License-Identifier: Apache-2.0
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport"
      content="width=device-width, initial-scale=1">
<title>Thread Border Router - Firmware Update</title>
<link rel="stylesheet"
      href="/static/style.css">
<link rel="icon"
      href="/favicon.ico">
</head>

<body>
<nav class="nav" id="nav"></nav>
<div class="container">
    <h1 class="page-title">Firmware Update</h1>
    <p class="page-desc">
        Update the Thread Border Router firmware.
    </p>
    <div class="card section">
        <div class="card-header">
            Firmware Update
        </div>
        <p style="
            font-size:13px;
            color:var(--text-muted);
            margin-bottom:14px
        ">
            Select an ESP32-C6 firmware image and upload it to the
            inactive OTA partition.
        </p>
        <form id="otaForm"
              onsubmit="return false">
            <div class="form-group">
                <label class="form-label"
                       for="firmware">
                    Firmware Image
                </label>
                <input
                    class="form-input"
                    id="firmware"
                    name="firmware"
                    type="file"
                    accept=".bin,application/octet-stream">
                <div class="form-hint">
                    Select the firmware.bin generated by PlatformIO.
                </div>
            </div>
            <div style="
                display:flex;
                align-items:center;
                gap:12px;
                flex-wrap:wrap
            ">
                <button
                    type="button"
                    class="btn btn-primary"
                    id="uploadButton"
                    onclick="uploadFirmware()">
                    Upload Firmware
                </button>
                <span
                    id="otaStatus"
                    style="font-size:13px">
                </span>
            </div>
        </form>
    </div>
    <div class="card section">
        <div class="card-header">
            Update Information
        </div>
        <div class="info-row">
            <span class="info-label">
                Target
            </span>
            <span class="info-value">
                ESP32-C6
            </span>
        </div>
        <div class="info-row">
            <span class="info-label">
                OTA
            </span>
            <span class="info-value">
                Dual Partition
            </span>
        </div>
        <div class="info-row">
            <span class="info-label">
                Maximum Firmware Size
            </span>
            <span class="info-value">
                4 MB
            </span>
        </div>
    </div>
    <div class="footer">
        Copyright &copy; 2026 Espressif Systems. All rights reserved.
    </div>
</div>

<script src="/static/api.js"></script>

<script>
/*
 * Wait for the ESP32 to reboot and become reachable again.
 *
 * During reboot, /node/state will fail to connect.
 * Once the Web server is running again, the request succeeds.
 */
function waitForReboot()
{
    var status =
        document.getElementById('otaStatus');
    var attempts = 0;
    var maxAttempts = 30;
    status.textContent =
        'Device is rebooting...';
    status.style.color =
        'var(--success)';

    function checkDevice()
    {
        attempts++;
        fetch('/node/state', {
            method: 'GET',
            cache: 'no-store'
        })
        .then(function(response) {
            if (!response.ok) {
                throw new Error(
                    'HTTP ' + response.status
                );
            }
            return response.json();
        })

        .then(function() {
            /*
             * The ESP32 is responding again.
             */
            status.textContent =
                'Device restarted successfully.';
            status.style.color =
                'var(--success)';
            showToast(
                'Device restarted successfully.',
                'success'
            );

            /*
             * Give the user a short moment to see
             * the success message, then reload the page.
             */
            setTimeout(function() {
                showLoading(false);
                window.location.reload();
            }, 800);
        })
        .catch(function() {
            /*
             * The ESP32 is still rebooting.
             */
            if (attempts >= maxAttempts) {
                status.textContent =
                    'Device reboot detection timed out.';
                status.style.color =
                    'var(--danger)';
                showToast(
                    'Device reboot detection timed out.',
                    'error'
                );
                showLoading(false);
                return;
            }

            /*
             * Check again after one second.
             */
            setTimeout(
                checkDevice,
                1000
            );
        });
    }

    /*
     * Give the ESP32 some time to actually reboot
     * before starting the first request.
     */
    setTimeout(
        checkDevice,
        1500
    );
}

function uploadFirmware()
{
    var input =
        document.getElementById('firmware');
    var button =
        document.getElementById('uploadButton');
    var status =
        document.getElementById('otaStatus');

    if (!input.files.length) {
        showToast(
            'Please select a firmware image.',
            'error'
        );
        status.textContent =
            'Please select a firmware image.';
        status.style.color =
            'var(--danger)';
        return;
    }

    var file =
        input.files[0];

    if (file.size === 0) {
        showToast(
            'The selected file is empty.',
            'error'
        );
        return;
    }

    var sizeMB =
        (file.size / 1024 / 1024).toFixed(2);

    if (!confirm(
        'Upload this firmware image?\n\n' +
        file.name + '\n' +
        sizeMB + ' MB\n\n' +
        'The device will reboot automatically after the update.'
    )) {
        return;
    }

    button.disabled = true;
    input.disabled = true;

    status.textContent =
        'Uploading...';
    status.style.color =
        'var(--primary)';
    showLoading(true);
    fetch('/api/ota', {
    method: 'POST',
    headers: {
        'Content-Type':
            'application/octet-stream'
    },
    body: file
    })
    .then(function(response) {
        return response.text().then(function(text) {
            /*
            * Check HTTP status before parsing JSON.
            */
            if (!response.ok) {
                throw new Error(
                    'OTA update failed (HTTP ' +
                    response.status +
                    '): ' +
                    text
                );
            }
            var result;
            try {
                result = JSON.parse(text);
            } catch (e) {
                throw new Error(
                    'Invalid server response: ' + text
                );
            }
            if (result.error) {
                throw new Error(
                    result.message ||
                    'OTA update failed'
                );
            }
            return result;
        });
    })

    .then(function(result) {
        status.textContent =
            result.message ||
            'OTA successful. Rebooting...';
        status.style.color =
            'var(--success)';
        showToast(
            'Firmware update successful.',
            'success'
        );

        /*
        * Wait for the ESP32 to reboot and become
        * reachable again.
        */
        waitForReboot();

    })

    .catch(function(error) {
        status.textContent =
            error.message ||
            'Firmware update failed.';
        status.style.color =
            'var(--danger)';
        showToast(
            error.message ||
            'Firmware update failed.',
            'error'
        );

        button.disabled = false;
        input.disabled = false;

        /*
         * The OTA request itself failed, so the loading
         * overlay can be removed immediately.
         */
        showLoading(false);
    });
}
</script>
</body>
</html>

```

更新ファイル

`esp-thread-br/components/esp_ot_br_server/frontend/static/api.js`

```diff
@@ -137,7 +137,8 @@
       {href : '/network.html', label : 'Network'},
       {href : '/commission.html', label : 'Commissioner'},
       {href : '/addresses.html', label : 'Addresses'},
- {href : '/tools.html', label : 'Tools'}
+ {href : '/tools.html', label : 'Tools'},
+ {href : '/ota.html', label : 'Firmware Update'}
     ]
   },
   {href : '/topology.html', label : 'Topology'},

```

`esp-thread-br/components/esp_ot_br_server/src/esp_br_web.c`

```diff
@@ -24,6 +24,7 @@
 #include "esp_heap_caps.h"
 #include "esp_http_server.h"
 #include "esp_log.h"
+#include "esp_ota_ops.h"
 #include "esp_openthread.h"
 #include "esp_openthread_border_router.h"
 #include "esp_spiffs.h"
@@ -223,6 +224,7 @@
 static esp_err_t esp_otbr_ipaddr_get_handler(httpd_req_t *req);
 static esp_err_t esp_otbr_add_ipaddr_post_handler(httpd_req_t *req);
 static esp_err_t esp_otbr_delete_ipaddr_post_handler(httpd_req_t *req);
+static esp_err_t esp_otbr_ota_post_handler(httpd_req_t *req);
 
 static httpd_uri_t s_web_gui_handlers[] = {
     {
@@ -303,6 +305,12 @@
         .handler = esp_otbr_delete_ipaddr_post_handler,
         .user_ctx = &s_server.data,
     },
+ {
+ .uri = "/api/ota",
+ .method = HTTP_POST,
+ .handler = esp_otbr_ota_post_handler,
+ .user_ctx = NULL,
+ },
 };
 
 /*-----------------------------------------------------
@@ -1066,6 +1074,255 @@
     return ret;
 }
 
+static esp_err_t esp_otbr_ota_post_handler(httpd_req_t *req)
+{
+ esp_err_t ret = ESP_OK;
+ esp_ota_handle_t ota_handle = 0;
+ const esp_partition_t *update_partition = NULL;
+ bool ota_started = false;
+
+ if (req == NULL) {
+ return ESP_ERR_INVALID_ARG;
+ }
+
+ if (req->content_len <= 0) {
+ httpd_resp_send_err(
+ req,
+ HTTPD_400_BAD_REQUEST,
+ "Invalid firmware size"
+ );
+ return ESP_ERR_INVALID_SIZE;
+ }
+
+ ESP_LOGI(
+ WEB_TAG,
+ "OTA upload started: %d bytes",
+ req->content_len
+ );
+
+ /*
+ * Select the OTA partition that is not currently running.
+ */
+ update_partition = esp_ota_get_next_update_partition(NULL);
+
+ if (update_partition == NULL) {
+ ESP_LOGE(
+ WEB_TAG,
+ "No OTA partition available"
+ );
+
+ httpd_resp_send_err(
+ req,
+ HTTPD_500_INTERNAL_SERVER_ERROR,
+ "No OTA partition available"
+ );
+
+ return ESP_ERR_NOT_FOUND;
+ }
+
+ ESP_LOGI(
+ WEB_TAG,
+ "OTA target: %s @ 0x%" PRIx32 ", size=%" PRIu32,
+ update_partition->label,
+ update_partition->address,
+ update_partition->size
+ );
+
+ /*
+ * Make sure the uploaded firmware fits in the OTA partition.
+ *
+ * Your partition table gives ota_0 and ota_1 4 MiB each.
+ */
+ if ((uint32_t)req->content_len > update_partition->size) {
+ ESP_LOGE(
+ WEB_TAG,
+ "Firmware too large: %d > %" PRIu32,
+ req->content_len,
+ update_partition->size
+ );
+
+ httpd_resp_send_err(
+ req,
+ HTTPD_400_BAD_REQUEST,
+ "Firmware image is too large"
+ );
+
+ return ESP_ERR_INVALID_SIZE;
+ }
+
+ /*
+ * Start OTA.
+ */
+ ret = esp_ota_begin(
+ update_partition,
+ OTA_WITH_SEQUENTIAL_WRITES,
+ &ota_handle
+ );
+
+ if (ret != ESP_OK) {
+ ESP_LOGE(
+ WEB_TAG,
+ "esp_ota_begin failed: %s",
+ esp_err_to_name(ret)
+ );
+
+ httpd_resp_send_err(
+ req,
+ HTTPD_500_INTERNAL_SERVER_ERROR,
+ "Failed to start OTA"
+ );
+
+ return ret;
+ }
+
+ ota_started = true;
+
+ char buffer[FILE_CHUNK_SIZE];
+ int remaining = req->content_len;
+
+ while (remaining > 0) {
+
+ int recv_size = remaining;
+
+ if (recv_size > (int)sizeof(buffer)) {
+ recv_size = sizeof(buffer);
+ }
+
+ int recv_len = httpd_req_recv(
+ req,
+ buffer,
+ recv_size
+ );
+
+ if (recv_len == HTTPD_SOCK_ERR_TIMEOUT) {
+ continue;
+ }
+
+ if (recv_len <= 0) {
+ ESP_LOGE(
+ WEB_TAG,
+ "Failed to receive firmware data: %d",
+ recv_len
+ );
+
+ ret = ESP_FAIL;
+ goto ota_error;
+ }
+
+ ret = esp_ota_write(
+ ota_handle,
+ buffer,
+ recv_len
+ );
+
+ if (ret != ESP_OK) {
+ ESP_LOGE(
+ WEB_TAG,
+ "esp_ota_write failed: %s",
+ esp_err_to_name(ret)
+ );
+
+ goto ota_error;
+ }
+
+ remaining -= recv_len;
+
+ ESP_LOGD(
+ WEB_TAG,
+ "OTA progress: %d / %d",
+ req->content_len - remaining,
+ req->content_len
+ );
+ }
+
+ /*
+ * Finish and validate the OTA image.
+ */
+ ret = esp_ota_end(ota_handle);
+
+ ota_started = false;
+
+ if (ret != ESP_OK) {
+ ESP_LOGE(
+ WEB_TAG,
+ "esp_ota_end failed: %s",
+ esp_err_to_name(ret)
+ );
+
+ httpd_resp_send_err(
+ req,
+ HTTPD_400_BAD_REQUEST,
+ "Invalid firmware image"
+ );
+
+ return ret;
+ }
+
+ /*
+ * Select the new firmware as the next boot partition.
+ */
+ ret = esp_ota_set_boot_partition(update_partition);
+
+ if (ret != ESP_OK) {
+ ESP_LOGE(
+ WEB_TAG,
+ "Failed to set boot partition: %s",
+ esp_err_to_name(ret)
+ );
+
+ httpd_resp_send_err(
+ req,
+ HTTPD_500_INTERNAL_SERVER_ERROR,
+ "Failed to set boot partition"
+ );
+
+ return ret;
+ }
+
+ ESP_LOGI(
+ WEB_TAG,
+ "OTA successful. Boot partition: %s",
+ update_partition->label
+ );
+
+ /*
+ * Send the HTTP response before restarting.
+ */
+ httpd_resp_set_type(
+ req,
+ "application/json"
+ );
+
+ ret = httpd_resp_sendstr(
+ req,
+ "{\"error\":0,\"message\":\"OTA successful. Rebooting...\"}"
+ );
+
+ /*
+ * Give the HTTP response some time to reach the browser.
+ */
+ vTaskDelay(pdMS_TO_TICKS(500));
+
+ esp_restart();
+
+ return ret;
+
+
+ota_error:
+
+ if (ota_started) {
+ esp_ota_abort(ota_handle);
+ }
+
+ httpd_resp_send_err(
+ req,
+ HTTPD_500_INTERNAL_SERVER_ERROR,
+ "OTA failed"
+ );
+
+ return ret;
+}
+
 /*-----------------------------------------------------
  Note：Handling for Client request
 -----------------------------------------------------*/

```

`esp-thread-br/examples/basic_thread_border_router/CMakeLists.txt`

```diff
@@ -2,5 +2,7 @@
 # in this exact order for cmake to work correctly
 cmake_minimum_required(VERSION 3.5)
 
+set(PROJECT_VER "1.0.2")
+
 include($ENV{IDF_PATH}/tools/cmake/project.cmake)
 project(esp_ot_br)

```

`esp-thread-br/components/esp_ot_br_server/CMakeLists.txt`

```diff
@@ -1,4 +1,4 @@
-set(requires mdns fatfs spiffs esp_eth nvs_flash freertos openthread esp_http_server protocol_examples_common esp_wifi esp_app_format esp_timer)
+set(requires mdns fatfs spiffs esp_eth nvs_flash freertos openthread esp_http_server app_update protocol_examples_common esp_wifi esp_app_format esp_timer)
 
 # For IDF < 6.0, use IDF's built-in json component; for IDF >= 6.0 use managed cJSON component through idf_component.yml
 if("${IDF_VERSION_MAJOR}" VERSION_LESS "6")

```

`esp-thread-br/examples/basic_thread_border_router/main/esp_ot_br.c`

```diff
@@ -5,6 +5,7 @@
  */
 #include <stdio.h>
 #include <string.h>
+#include <inttypes.h>
 
 #include "sdkconfig.h"
 
@@ -13,6 +14,7 @@
 #include "esp_event.h"
 #include "esp_log.h"
 #include "esp_netif.h"
+#include "esp_ota_ops.h"
 #include "esp_openthread.h"
 #include "esp_openthread_border_router.h"
 #include "esp_openthread_netif_glue.h"
@@ -35,6 +37,120 @@
 extern const uint8_t server_cert_pem_start[] asm("_binary_ca_cert_pem_start");
 extern const uint8_t server_cert_pem_end[] asm("_binary_ca_cert_pem_end");
 
+/*
+ * Convert OTA image state to a readable string.
+ */
+static const char *ota_state_to_string(esp_ota_img_states_t state)
+{
+ switch (state) {
+ case ESP_OTA_IMG_NEW:
+ return "NEW";
+ case ESP_OTA_IMG_PENDING_VERIFY:
+ return "PENDING_VERIFY";
+ case ESP_OTA_IMG_VALID:
+ return "VALID";
+ case ESP_OTA_IMG_INVALID:
+ return "INVALID";
+ case ESP_OTA_IMG_ABORTED:
+ return "ABORTED";
+ case ESP_OTA_IMG_UNDEFINED:
+ return "UNDEFINED";
+ default:
+ return "UNKNOWN";
+ }
+}
+
+/*
+ * Check the currently running OTA partition and
+ * confirm the firmware image if rollback is enabled.
+ */
+static void check_ota_state(void)
+{
+ const esp_partition_t *running =
+ esp_ota_get_running_partition();
+ if (running == NULL) {
+ ESP_LOGE(
+ TAG,
+ "Failed to get running OTA partition"
+ );
+ return;
+ }
+
+ ESP_LOGI(
+ TAG,
+ "Running firmware partition: %s @ 0x%" PRIx32,
+ running->label,
+ running->address
+ );
+
+ /*
+ * Get the current OTA image state.
+ */
+ esp_ota_img_states_t ota_state;
+ esp_err_t ret =
+ esp_ota_get_state_partition(
+ running,
+ &ota_state
+ );
+ if (ret == ESP_OK) {
+ ESP_LOGI(
+ TAG,
+ "OTA image state before confirmation: %s",
+ ota_state_to_string(ota_state)
+ );
+ } else {
+ ESP_LOGW(
+ TAG,
+ "Failed to get OTA image state: %s",
+ esp_err_to_name(ret)
+ );
+ }
+
+ /*
+ * Confirm the currently running firmware.
+ *
+ * This changes ESP_OTA_IMG_PENDING_VERIFY to
+ * ESP_OTA_IMG_VALID and cancels rollback.
+ */
+ ret =
+ esp_ota_mark_app_valid_cancel_rollback();
+
+ if (ret == ESP_OK) {
+ ESP_LOGI(
+ TAG,
+ "OTA image confirmed as valid"
+ );
+ } else {
+ ESP_LOGW(
+ TAG,
+ "OTA image confirmation failed: %s",
+ esp_err_to_name(ret)
+ );
+ }
+
+ /*
+ * Check the state again after confirmation.
+ */
+ ret =
+ esp_ota_get_state_partition(
+ running,
+ &ota_state
+ );
+ if (ret == ESP_OK) {
+ ESP_LOGI(
+ TAG,
+ "OTA image state after confirmation: %s",
+ ota_state_to_string(ota_state)
+ );
+ } else {
+ ESP_LOGW(
+ TAG,
+ "Failed to get OTA image state after confirmation: %s",
+ esp_err_to_name(ret)
+ );
+ }
+}
+
 static esp_err_t init_spiffs(void)
 {
 #if CONFIG_AUTO_UPDATE_RCP
@@ -54,13 +170,23 @@
 
 void app_main(void)
 {
+ /*
+ * ---------------------------------------------------------
+ * OTA / rollback status
+ * ---------------------------------------------------------
+ *
+ * Check which OTA partition is currently running and
+ * confirm the firmware image.
+ */
+ check_ota_state();
+
     // Used eventfds:
     // * netif
     // * task queue
     // * border router
     size_t max_eventfd = 3;
 
-#if CONFIG_OPENTHREAD_RADIO_SPINEL_SPI
+#if CONFIG_OPENTHREAD_RADIO_NATIVE || CONFIG_OPENTHREAD_RADIO_SPINEL_SPI
     // * SpiSpinelInterface (The Spi Spinel Interface needs an eventfd.)
     max_eventfd++;
 #endif

```

---

<div class="post-metadata">

### Author: ![tk-fuse](https://forum.ficusonline.com/user_avatar/forum.ficusonline.com/tk-fuse/32/255_2.png) [@tk-fuse](https://forum.ficusonline.com/u/tk-fuse)
#### Post date: [2026 年 9 月 3 日午前 1:47 UTC](https://forum.ficusonline.com/t/topic/562/7 "2026-09-03T01:47:09Z")

</div>

## ベーシック認証・ユーザ名・パスワード変更機能追加

ベーシック認証とユーザ名・パスワードを変更するページを追加

 ![Screenshot from 2026-09-03 10-34-53](https://forum.ficusonline.com/uploads/default/original/2X/f/f77c7c1ca4ecd373a8d8672ad2c66f8f31f179a0.png)

`esp-thread-br/components/esp_ot_br_server/frontend/credentials.html`

```html
<!--
  SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
  SPDX-License-Identifier: Apache-2.0
-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Thread Border Router - Change Credentials</title>
    <link rel="stylesheet" href="/static/style.css">
    <link rel="icon" href="/favicon.ico">
</head>
<body>
<nav class="nav" id="nav"></nav>
<div class="container">
    <h1 class="page-title">Change Credentials</h1>
    <p class="page-desc">Update the username and password used to access this web interface</p>

    <!-- Credentials Section -->
    <div class="card section">
        <div class="card-header">Login Credentials</div>
        <p style="font-size:13px;color:var(--text-muted);margin-bottom:14px">
            After saving, you will need to log in again with the new username and password the next time you access this device.
        </p>
        <form id="credentialsForm" onsubmit="return false">
            <div class="form-group">
                <label class="form-label">New Username *</label>
                <input class="form-input" name="username" id="credUsername" type="text"
                       autocomplete="username" maxlength="31" required>
                <div class="form-hint">Up to 31 characters</div>
            </div>
            <div class="form-row">
                <div class="form-group">
                    <label class="form-label">New Password *</label>
                    <input class="form-input" name="password" id="credPassword" type="password"
                           autocomplete="new-password" maxlength="63" minlength="4" required>
                    <div class="form-hint">4–63 characters</div>
                </div>
                <div class="form-group">
                    <label class="form-label">Confirm New Password *</label>
                    <input class="form-input" id="credPasswordConfirm" type="password"
                           autocomplete="new-password" maxlength="63" minlength="4" required>
                    <div class="form-hint">Re-enter the password above</div>
                </div>
            </div>
            <div class="flex-between">
                <button type="button" class="btn btn-primary" id="credBtn" onclick="saveCredentials()">Save Changes</button>
                <span id="credStatus" style="font-size:13px"></span>
            </div>
        </form>
    </div>

</div>
<div class="footer">Copyright &copy; 2026 Espressif Systems. All rights reserved.</div>

<script src="/static/api.js"></script>
<script>
function saveCredentials() {
    var username = document.getElementById('credUsername').value.trim();
    var password = document.getElementById('credPassword').value;
    var passwordConfirm = document.getElementById('credPasswordConfirm').value;

    if (!username) {
        showToast('Please enter a username', 'error');
        return;
    }
    if (password.length < 4) {
        showToast('Password must be at least 4 characters', 'error');
        return;
    }
    if (password !== passwordConfirm) {
        showToast('Passwords do not match', 'error');
        return;
    }

    var btn = document.getElementById('credBtn');
    var status = document.getElementById('credStatus');

    btn.disabled = true;
    btn.textContent = 'Saving...';
    status.textContent = 'Saving credentials...';
    status.style.color = 'var(--primary)';

    apiPut('/api/credentials', {username: username, password: password}).then(function(data) {
        btn.disabled = false;
        btn.textContent = 'Save Changes';

        if (data.error !== 0) {
            status.textContent = data.message || 'Failed to update credentials';
            status.style.color = 'var(--danger)';
            showToast(data.message || 'Failed to update credentials', 'error');
            return;
        }

        status.textContent = 'Saved';
        status.style.color = 'var(--success)';
        showToast('Credentials updated. Please log in again with your new credentials.', 'success');
        document.getElementById('credentialsForm').reset();

    }).catch(function() {
        btn.disabled = false;
        btn.textContent = 'Save Changes';
        status.textContent = 'Connection error';
        status.style.color = 'var(--danger)';
        showToast('Connection error', 'error');
    });
}
</script>
</body>
</html>

```

`esp-thread-br/components/esp_ot_br_server/frontend/static/api.js`

```diff
@@ -40,6 +40,15 @@
       .then(function(r) { return r.json(); });
 }
 
+function apiPut(url, data) {
+ return fetch(url, {
+ method : 'PUT',
+ headers : {'Content-Type' : 'application/json'},
+ body : JSON.stringify(data)
+ })
+ .then(function(r) { return r.json(); });
+}
+
 /**
  * Get cached response for a GET endpoint, or null if not available.
  * Use this to render immediately on page load before the live fetch completes.
@@ -137,7 +146,9 @@
       {href : '/network.html', label : 'Network'},
       {href : '/commission.html', label : 'Commissioner'},
       {href : '/addresses.html', label : 'Addresses'},
       {href : '/tools.html', label : 'Tools'},
+ {href : '/ota.html', label : 'Firmware Update'},
+ {href : '/credentials.html', label : 'Change Credentials'}
     ]
   },
   {href : '/topology.html', label : 'Topology'}, {href : '/tools.html', label : 'Tools'}

```

`esp_projects/esp-thread-br/components/esp_ot_br_server/src/esp_br_web.c`

```diff
@@ -24,6 +24,7 @@
 #include "esp_heap_caps.h"
 #include "esp_http_server.h"
 #include "esp_log.h"
+#include "esp_ota_ops.h"
 #include "esp_openthread.h"
 #include "esp_openthread_border_router.h"
 #include "esp_spiffs.h"
@@ -38,6 +39,8 @@
 #include "openthread/thread.h"
 #include "openthread/thread_ftd.h"
 
+#include "mbedtls/base64.h"
+
 #define MAX_FILE_SIZE (200 * 1024) // 200 KB
 #define MAX_FILE_SIZE_STR "200KB"
 #define SCRATCH_BUFSIZE 1024 /* Scratch buffer size */
@@ -46,6 +49,116 @@
 #define FILE_CHUNK_SIZE 4096
 #define WEB_TAG "obtr_web"
 
+#include "nvs.h"
+
+#define BR_WEB_AUTH_NVS_NAMESPACE "br_web_auth"
+#define BR_WEB_AUTH_USERNAME_KEY "username"
+#define BR_WEB_AUTH_PASSWORD_KEY "password"
+#define BR_WEB_AUTH_USERNAME_MAXLEN 32
+#define BR_WEB_AUTH_PASSWORD_MAXLEN 64
+
+/* 初回起動時、NVSに何も保存されていない場合のみ使われるデフォルト値 */
+#define BR_WEB_AUTH_DEFAULT_USERNAME "admin"
+#define BR_WEB_AUTH_DEFAULT_PASSWORD "changeme"
+
+static nvs_handle_t s_web_auth_nvs_handle = 0;
+
+esp_err_t esp_br_web_auth_init(void)
+{
+ static bool s_auth_init = false;
+ if (!s_auth_init) {
+ esp_err_t err = nvs_open(BR_WEB_AUTH_NVS_NAMESPACE, NVS_READWRITE, &s_web_auth_nvs_handle);
+ ESP_RETURN_ON_ERROR(err, WEB_TAG, "Failed to open br_web_auth NVS namespace (0x%x)", err);
+ s_auth_init = true;
+ }
+ return ESP_OK;
+}
+
+esp_err_t esp_br_web_auth_set_username(const char *username)
+{
+ ESP_RETURN_ON_FALSE((s_web_auth_nvs_handle != 0), ESP_FAIL, WEB_TAG, "auth NVS handle is invalid");
+ ESP_RETURN_ON_ERROR(nvs_set_str(s_web_auth_nvs_handle, BR_WEB_AUTH_USERNAME_KEY, username), WEB_TAG,
+ "No buffers for username");
+ ESP_RETURN_ON_ERROR(nvs_commit(s_web_auth_nvs_handle), WEB_TAG, "Failed to commit username");
+ return ESP_OK;
+}
+
+esp_err_t esp_br_web_auth_get_username(char *username, size_t maxlen)
+{
+ ESP_RETURN_ON_FALSE((s_web_auth_nvs_handle != 0), ESP_FAIL, WEB_TAG, "auth NVS handle is invalid");
+ size_t length = maxlen;
+ if (nvs_get_str(s_web_auth_nvs_handle, BR_WEB_AUTH_USERNAME_KEY, username, &length) != ESP_OK) {
+ /* 未設定なら初回起動とみなし、デフォルト値をそのままNVSにも書き込んでおく */
+ strlcpy(username, BR_WEB_AUTH_DEFAULT_USERNAME, maxlen);
+ esp_br_web_auth_set_username(username);
+ }
+ return ESP_OK;
+}
+
+esp_err_t esp_br_web_auth_set_password(const char *password)
+{
+ ESP_RETURN_ON_FALSE((s_web_auth_nvs_handle != 0), ESP_FAIL, WEB_TAG, "auth NVS handle is invalid");
+ ESP_RETURN_ON_ERROR(nvs_set_str(s_web_auth_nvs_handle, BR_WEB_AUTH_PASSWORD_KEY, password), WEB_TAG,
+ "No buffers for password");
+ ESP_RETURN_ON_ERROR(nvs_commit(s_web_auth_nvs_handle), WEB_TAG, "Failed to commit password");
+ return ESP_OK;
+}
+
+esp_err_t esp_br_web_auth_get_password(char *password, size_t maxlen)
+{
+ ESP_RETURN_ON_FALSE((s_web_auth_nvs_handle != 0), ESP_FAIL, WEB_TAG, "auth NVS handle is invalid");
+ size_t length = maxlen;
+ if (nvs_get_str(s_web_auth_nvs_handle, BR_WEB_AUTH_PASSWORD_KEY, password, &length) != ESP_OK) {
+ strlcpy(password, BR_WEB_AUTH_DEFAULT_PASSWORD, maxlen);
+ esp_br_web_auth_set_password(password);
+ }
+ return ESP_OK;
+}
+
+/*-----------------------------------------------------
+ Note：Http Server Basic Auth
+-----------------------------------------------------*/
+static bool check_basic_auth(httpd_req_t *req)
+{
+ size_t hdr_len = httpd_req_get_hdr_value_len(req, "Authorization");
+ if (hdr_len == 0) {
+ return false;
+ }
+
+ char *auth_hdr = malloc(hdr_len + 1);
+ if (!auth_hdr) {
+ return false;
+ }
+ if (httpd_req_get_hdr_value_str(req, "Authorization", auth_hdr, hdr_len + 1) != ESP_OK) {
+ free(auth_hdr);
+ return false;
+ }
+
+ bool ok = false;
+ const char *prefix = "Basic ";
+ if (strncmp(auth_hdr, prefix, strlen(prefix)) == 0) {
+ const char *b64 = auth_hdr + strlen(prefix);
+ unsigned char decoded[128] = {0};
+ size_t decoded_len = 0;
+ if (mbedtls_base64_decode(decoded, sizeof(decoded) - 1, &decoded_len, (const unsigned char *)b64,
+ strlen(b64)) == 0) {
+ decoded[decoded_len] = '\0';
+ char stored_username[BR_WEB_AUTH_USERNAME_MAXLEN] = {0};
+ char stored_password[BR_WEB_AUTH_PASSWORD_MAXLEN] = {0};
+ esp_br_web_auth_get_username(stored_username, sizeof(stored_username));
+ esp_br_web_auth_get_password(stored_password, sizeof(stored_password));
+
+ char expected[BR_WEB_AUTH_USERNAME_MAXLEN + BR_WEB_AUTH_PASSWORD_MAXLEN + 1];
+ snprintf(expected, sizeof(expected), "%s:%s", stored_username, stored_password);
+ ok = (strcmp((char *)decoded, expected) == 0);
+ }
+ }
+
+ free(auth_hdr);
+ return ok;
+}
+
+
 /*-----------------------------------------------------
  Note：Http Server
 -----------------------------------------------------*/
@@ -72,11 +185,11 @@
 /**
  * @brief The basic parameter definition for parsing url
  */
-#define PROTLOCOL_MAX_SIZE 12
+#define PROTOCOL_MAX_SIZE 12
 #define FILENAME_MAX_SIZE 64
 #define FILEPATH_MAX_SIZE (FILENAME_MAX_SIZE + ESP_VFS_PATH_MAX)
 typedef struct request_url {
- char protocol[PROTLOCOL_MAX_SIZE];
+ char protocol[PROTOCOL_MAX_SIZE];
     uint16_t port;
     char file_name[FILENAME_MAX_SIZE];
     char file_path[FILEPATH_MAX_SIZE];
@@ -223,6 +336,8 @@
 static esp_err_t esp_otbr_ipaddr_get_handler(httpd_req_t *req);
 static esp_err_t esp_otbr_add_ipaddr_post_handler(httpd_req_t *req);
 static esp_err_t esp_otbr_delete_ipaddr_post_handler(httpd_req_t *req);
+static esp_err_t esp_otbr_ota_post_handler(httpd_req_t *req);
+static esp_err_t esp_otbr_auth_credentials_put_handler(httpd_req_t *req);
 
 static httpd_uri_t s_web_gui_handlers[] = {
     {
@@ -303,9 +418,57 @@
         .handler = esp_otbr_delete_ipaddr_post_handler,
         .user_ctx = &s_server.data,
     },
+ {
+ .uri = "/api/credentials",
+ .method = HTTP_PUT,
+ .handler = esp_otbr_auth_credentials_put_handler,
+ .user_ctx = &s_server.data,
+ },
+ {
+ .uri = "/api/ota",
+ .method = HTTP_POST,
+ .handler = esp_otbr_ota_post_handler,
+ .user_ctx = NULL,
+ },
 };
 
 /*-----------------------------------------------------
+ Note：Http Server Basic Auth (wrapper)
+-----------------------------------------------------*/
+static esp_err_t send_401_unauthorized(httpd_req_t *req)
+{
+ httpd_resp_set_status(req, "401 Unauthorized");
+ httpd_resp_set_hdr(req, "WWW-Authenticate", "Basic realm=\"ESP Thread BR\"");
+ httpd_resp_send(req, NULL, 0);
+ return ESP_FAIL;
+}
+
+typedef struct {
+ esp_err_t (*orig_handler)(httpd_req_t *req);
+ void *orig_user_ctx;
+ bool wrapped;
+} auth_wrapped_ctx_t;
+
+/* s_resource_handlers / s_web_gui_handlers はこの直前で既に定義済みなので sizeof が使える */
+static auth_wrapped_ctx_t s_resource_handler_ctx[sizeof(s_resource_handlers) / sizeof(httpd_uri_t)];
+static auth_wrapped_ctx_t s_web_gui_handler_ctx[sizeof(s_web_gui_handlers) / sizeof(httpd_uri_t)];
+static auth_wrapped_ctx_t s_default_uri_ctx;
+
+static esp_err_t auth_wrapper_handler(httpd_req_t *req)
+{
+ auth_wrapped_ctx_t *ctx = (auth_wrapped_ctx_t *)req->user_ctx;
+
+ if (!check_basic_auth(req)) {
+ return send_401_unauthorized(req);
+ }
+
+ req->user_ctx = ctx->orig_user_ctx;
+ esp_err_t ret = ctx->orig_handler(req);
+ req->user_ctx = ctx;
+ return ret;
+}
+
+/*-----------------------------------------------------
  Note：Http Tools
 -----------------------------------------------------*/
 static cJSON *pack_response(cJSON *error, cJSON *result, cJSON *message)
@@ -338,10 +501,17 @@
     return root;
 }
 
-static esp_err_t httpd_server_register_http_uri(const http_server_t *server, httpd_uri_t *uris, uint8_t size)
+static esp_err_t httpd_server_register_http_uri(const http_server_t *server, httpd_uri_t *uris, auth_wrapped_ctx_t *ctx_storage, uint8_t size)
 {
     ESP_RETURN_ON_FALSE((server->handle && uris), ESP_ERR_INVALID_ARG, WEB_TAG, "Invalid argument");
     for (int i = 0; i < size; i++) {
+ if (!ctx_storage[i].wrapped) {
+ ctx_storage[i].orig_handler = uris[i].handler;
+ ctx_storage[i].orig_user_ctx = uris[i].user_ctx;
+ ctx_storage[i].wrapped = true;
+ }
+ uris[i].handler = auth_wrapper_handler;
+ uris[i].user_ctx = &ctx_storage[i];
         ESP_RETURN_ON_ERROR(httpd_register_uri_handler(server->handle, &uris[i]), WEB_TAG,
                             "Failed to register %s for %d", uris[i].uri, i);
     }
@@ -1066,6 +1236,285 @@
     return ret;
 }
 
+static esp_err_t esp_otbr_auth_credentials_put_handler(httpd_req_t *req)
+{
+ esp_err_t ret = ESP_OK;
+ cJSON *request = httpd_request_convert2_json(req, cJSON_Object);
+ ESP_RETURN_ON_FALSE(request, ESP_FAIL, WEB_TAG, "Failed to parse credentials request");
+
+ cJSON *username = cJSON_GetObjectItem(request, "username");
+ cJSON *password = cJSON_GetObjectItem(request, "password");
+
+ bool ok = true;
+ if (cJSON_IsString(username) && strlen(username->valuestring) > 0 &&
+ strlen(username->valuestring) < BR_WEB_AUTH_USERNAME_MAXLEN) {
+ ok = ok && (esp_br_web_auth_set_username(username->valuestring) == ESP_OK);
+ }
+ if (cJSON_IsString(password) && strlen(password->valuestring) > 0 &&
+ strlen(password->valuestring) < BR_WEB_AUTH_PASSWORD_MAXLEN) {
+ ok = ok && (esp_br_web_auth_set_password(password->valuestring) == ESP_OK);
+ }
+
+ cJSON *error = cJSON_CreateNumber(ok ? 0 : 1);
+ cJSON *result = cJSON_CreateString(ok ? "successful" : "failed");
+ cJSON *message = cJSON_CreateString(ok ? "Credentials updated" : "Failed to update credentials");
+ cJSON *response = pack_response(error, result, message);
+ ESP_GOTO_ON_ERROR(httpd_send_packet(req, response), exit, WEB_TAG, "Failed to respond %s", req->uri);
+exit:
+ cJSON_Delete(request);
+ cJSON_Delete(response);
+ return ret;
+}
+
+static esp_err_t esp_otbr_ota_post_handler(httpd_req_t *req)
+{
+ esp_err_t ret = ESP_OK;
+ esp_ota_handle_t ota_handle = 0;
+ const esp_partition_t *update_partition = NULL;
+ bool ota_started = false;
+
+ if (req == NULL) {
+ return ESP_ERR_INVALID_ARG;
+ }
+
+ if (req->content_len <= 0) {
+ httpd_resp_send_err(
+ req,
+ HTTPD_400_BAD_REQUEST,
+ "Invalid firmware size"
+ );
+ return ESP_ERR_INVALID_SIZE;
+ }
+
+ ESP_LOGI(
+ WEB_TAG,
+ "OTA upload started: %d bytes",
+ req->content_len
+ );
+
+ /*
+ * Select the OTA partition that is not currently running.
+ */
+ update_partition = esp_ota_get_next_update_partition(NULL);
+
+ if (update_partition == NULL) {
+ ESP_LOGE(
+ WEB_TAG,
+ "No OTA partition available"
+ );
+
+ httpd_resp_send_err(
+ req,
+ HTTPD_500_INTERNAL_SERVER_ERROR,
+ "No OTA partition available"
+ );
+
+ return ESP_ERR_NOT_FOUND;
+ }
+
+ ESP_LOGI(
+ WEB_TAG,
+ "OTA target: %s @ 0x%" PRIx32 ", size=%" PRIu32,
+ update_partition->label,
+ update_partition->address,
+ update_partition->size
+ );
+
+ /*
+ * Make sure the uploaded firmware fits in the OTA partition.
+ *
+ * Your partition table gives ota_0 and ota_1 4 MiB each.
+ */
+ if ((uint32_t)req->content_len > update_partition->size) {
+ ESP_LOGE(
+ WEB_TAG,
+ "Firmware too large: %d > %" PRIu32,
+ req->content_len,
+ update_partition->size
+ );
+
+ httpd_resp_send_err(
+ req,
+ HTTPD_400_BAD_REQUEST,
+ "Firmware image is too large"
+ );
+
+ return ESP_ERR_INVALID_SIZE;
+ }
+
+ /*
+ * Start OTA.
+ */
+ ret = esp_ota_begin(
+ update_partition,
+ OTA_WITH_SEQUENTIAL_WRITES,
+ &ota_handle
+ );
+
+ if (ret != ESP_OK) {
+ ESP_LOGE(
+ WEB_TAG,
+ "esp_ota_begin failed: %s",
+ esp_err_to_name(ret)
+ );
+
+ httpd_resp_send_err(
+ req,
+ HTTPD_500_INTERNAL_SERVER_ERROR,
+ "Failed to start OTA"
+ );
+
+ return ret;
+ }
+
+ ota_started = true;
+
+ char buffer[FILE_CHUNK_SIZE];
+ int remaining = req->content_len;
+
+ while (remaining > 0) {
+
+ int recv_size = remaining;
+
+ if (recv_size > (int)sizeof(buffer)) {
+ recv_size = sizeof(buffer);
+ }
+
+ int recv_len = httpd_req_recv(
+ req,
+ buffer,
+ recv_size
+ );
+
+ if (recv_len == HTTPD_SOCK_ERR_TIMEOUT) {
+ continue;
+ }
+
+ if (recv_len <= 0) {
+ ESP_LOGE(
+ WEB_TAG,
+ "Failed to receive firmware data: %d",
+ recv_len
+ );
+
+ ret = ESP_FAIL;
+ goto ota_error;
+ }
+
+ ret = esp_ota_write(
+ ota_handle,
+ buffer,
+ recv_len
+ );
+
+ if (ret != ESP_OK) {
+ ESP_LOGE(
+ WEB_TAG,
+ "esp_ota_write failed: %s",
+ esp_err_to_name(ret)
+ );
+
+ goto ota_error;
+ }
+
+ remaining -= recv_len;
+
+ ESP_LOGD(
+ WEB_TAG,
+ "OTA progress: %d / %d",
+ req->content_len - remaining,
+ req->content_len
+ );
+ }
+
+ /*
+ * Finish and validate the OTA image.
+ */
+ ret = esp_ota_end(ota_handle);
+
+ ota_started = false;
+
+ if (ret != ESP_OK) {
+ ESP_LOGE(
+ WEB_TAG,
+ "esp_ota_end failed: %s",
+ esp_err_to_name(ret)
+ );
+
+ httpd_resp_send_err(
+ req,
+ HTTPD_400_BAD_REQUEST,
+ "Invalid firmware image"
+ );
+
+ return ret;
+ }
+
+ /*
+ * Select the new firmware as the next boot partition.
+ */
+ ret = esp_ota_set_boot_partition(update_partition);
+
+ if (ret != ESP_OK) {
+ ESP_LOGE(
+ WEB_TAG,
+ "Failed to set boot partition: %s",
+ esp_err_to_name(ret)
+ );
+
+ httpd_resp_send_err(
+ req,
+ HTTPD_500_INTERNAL_SERVER_ERROR,
+ "Failed to set boot partition"
+ );
+
+ return ret;
+ }
+
+ ESP_LOGI(
+ WEB_TAG,
+ "OTA successful. Boot partition: %s",
+ update_partition->label
+ );
+
+ /*
+ * Send the HTTP response before restarting.
+ */
+ httpd_resp_set_type(
+ req,
+ "application/json"
+ );
+
+ ret = httpd_resp_sendstr(
+ req,
+ "{\"error\":0,\"message\":\"OTA successful. Rebooting...\"}"
+ );
+
+ /*
+ * Give the HTTP response some time to reach the browser.
+ */
+ vTaskDelay(pdMS_TO_TICKS(500));
+
+ esp_restart();
+
+ return ret;
+
+
+ota_error:
+
+ if (ota_started) {
+ esp_ota_abort(ota_handle);
+ }
+
+ httpd_resp_send_err(
+ req,
+ HTTPD_500_INTERNAL_SERVER_ERROR,
+ "OTA failed"
+ );
+
+ return ret;
+}
+
 /*-----------------------------------------------------
  Note：Handling for Client request
 -----------------------------------------------------*/
@@ -1190,7 +1639,7 @@
         .file_path = "",
     };
     ret.port = parse_url->port;
- if ((parse_url->field_set & (1 << UF_SCHEMA)) != 0 && PROTLOCOL_MAX_SIZE > parse_url->field_data[UF_SCHEMA].len) {
+ if ((parse_url->field_set & (1 << UF_SCHEMA)) != 0 && PROTOCOL_MAX_SIZE > parse_url->field_data[UF_SCHEMA].len) {
         memcpy(ret.protocol, uri + parse_url->field_data[UF_SCHEMA].off, parse_url->field_data[UF_SCHEMA].len);
         ret.protocol[parse_url->field_data[UF_SCHEMA].len] = '\0';
     }
@@ -1332,11 +1781,18 @@
 
     httpd_uri_t default_uris_get = {.uri = "/*", // Match all URIs of type /path/to/file
                                     .method = HTTP_GET,
- .handler = default_urls_get_handler,
- .user_ctx = &s_server.data};
+ .handler = auth_wrapper_handler,
+ .user_ctx = &s_default_uri_ctx};
+ if (!s_default_uri_ctx.wrapped) {
+ s_default_uri_ctx.orig_handler = default_urls_get_handler;
+ s_default_uri_ctx.orig_user_ctx = &s_server.data;
+ s_default_uri_ctx.wrapped = true;
+ }
 
- httpd_server_register_http_uri(&s_server, s_resource_handlers, sizeof(s_resource_handlers) / sizeof(httpd_uri_t));
- httpd_server_register_http_uri(&s_server, s_web_gui_handlers, sizeof(s_web_gui_handlers) / sizeof(httpd_uri_t));
+ httpd_server_register_http_uri(&s_server, s_resource_handlers, s_resource_handler_ctx,
+ sizeof(s_resource_handlers) / sizeof(httpd_uri_t));
+ httpd_server_register_http_uri(&s_server, s_web_gui_handlers, s_web_gui_handler_ctx,
+ sizeof(s_web_gui_handlers) / sizeof(httpd_uri_t));
     httpd_register_uri_handler(s_server.handle, &default_uris_get);
 
     // Show the login address in the console
@@ -1404,6 +1860,7 @@
 
 void esp_br_web_start(char *base_path)
 {
+ esp_br_web_auth_init(); /* NVSからユーザー名/パスワードを読めるようにしておく */
     ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &handler_got_ip_event, base_path));
     ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &handler_got_ip_event, base_path));
 }

```

---

<div class="post-metadata">

### Author: ![tk-fuse](https://forum.ficusonline.com/user_avatar/forum.ficusonline.com/tk-fuse/32/255_2.png) [@tk-fuse](https://forum.ficusonline.com/u/tk-fuse)
#### Post date: [2026 年 9 月 5 日午前 3:40 UTC](https://forum.ficusonline.com/t/topic/562/8 "2026-09-05T03:40:22Z")

</div>

## API

`esp-thread-br/components/esp_ot_br_server/private_include/esp_br_web_api.h`

```auto
/*---------------------------------------------------------------------
            ESP Thread Border Router Wer Server REST API
----------------------------------------------------------------------*/
/* HTTP GET */
#define ESP_OT_REST_API_DIAGNOSTICS_PATH "/diagnostics"
#define ESP_OT_REST_API_NODE_PATH "/node"
#define ESP_OT_REST_API_NODE_RLOC_PATH "/node/rloc"
#define ESP_OT_REST_API_NODE_RLOC16_PATH "/node/rloc16"
#define ESP_OT_REST_API_NODE_STATE_PATH "/node/state"
#define ESP_OT_REST_API_NODE_EXTADDRESS_PATH "/node/ext-address"
#define ESP_OT_REST_API_NODE_NETWORKNAME_PATH "/node/network-name"
#define ESP_OT_REST_API_NODE_LEADERDATA_PATH "/node/leader-data"
#define ESP_OT_REST_API_NODE_NUMBEROFROUTER_PATH "/node/num-of-router"
#define ESP_OT_REST_API_NODE_EXTPANID_PATH "/node/ext-panid"
#define ESP_OT_REST_API_NODE_BORDERAGENTID_PATH "/node/ba-id"
#define ESP_OT_REST_API_NODE_DATASET_ACTIVE_PATH "/node/dataset/active"
#define ESP_OT_REST_API_NODE_DATASET_PENDING_PATH "/node/dataset/pending"
#define ESP_OT_REST_API_PROPERTIES_PATH "/get_properties"
#define ESP_OT_REST_API_AVAILABLE_NETWORK_PATH "/available_network"
#define ESP_OT_REST_API_NODE_INFORMATION_PATH "/node_information"
#define ESP_OT_REST_API_TOPOLOGY_PATH "/topology"
/* HTTP POST */
#define ESP_OT_REST_API_JOIN_NETWORK_PATH "/join_network"
#define ESP_OT_REST_API_FORM_NETWORK_PATH "/form_network"
#define ESP_OT_REST_API_ADD_NETWORK_PREFIX_PATH "/add_prefix"
#define ESP_OT_REST_API_DELETE_NETWORK_PREFIX_PATH "/delete_prefix"
#define ESP_OT_REST_API_PING_PATH "/ping"
#define ESP_OT_REST_API_IPADDR_PATH "/ipaddr"
#define ESP_OT_REST_API_ADD_IPADDR_PATH "/add_ipaddr"
#define ESP_OT_REST_API_DELETE_IPADDR_PATH "/delete_ipaddr"
/* To implement in the future */
#define ESP_OT_REST_API_COMMISSION_PATH "/commission"
#define ESP_OT_REST_API_NETWORK "/networks"
#define ESP_OT_REST_API_NETWORK_CURRENT "/networks/current"
#define ESP_OT_REST_API_NETWORK_CURRENT_COMMISSION "/networks/commission"
#define ESP_OT_REST_API_NETWORK_CURRENT_PREFIX "/networks/current/prefix"

```

APIへのアクセス

```auto
$ curl -u user_name:password http://<local_IP>/get_properties
$ curl -u user_name:password http://<local_IP>/node/dataset/pending
$ curl -u user_name:password http://<local_IP>/node/dataset/active

```

---

<div class="post-metadata">

### Author: ![tk-fuse](https://forum.ficusonline.com/user_avatar/forum.ficusonline.com/tk-fuse/32/255_2.png) [@tk-fuse](https://forum.ficusonline.com/u/tk-fuse)
#### Post date: [2026 年 9 月 5 日午前 3:44 UTC](https://forum.ficusonline.com/t/topic/562/9 "2026-09-05T03:44:34Z")

</div>

## Thread(IEEE 802.15.4)チャンネル

Wi-Fi 2.4GHzチャンネル(1〜13)とThread(IEEE 802.15.4)チャンネルの対応表

日本国内ではチャンネル13まで利用可能なので、全て含めています。

### 対応表

| Wi-Fiチャンネル | 中心周波数 | 占有帯域(目安、幅22MHz) | 重なるThreadチャンネル |
| --- | --- | --- | --- |
| 1 | 2412 MHz | 2401–2423 MHz | 11, 12, 13, 14 |
| 2 | 2417 MHz | 2406–2428 MHz | 12, 13, 14, 15 |
| 3 | 2422 MHz | 2411–2433 MHz | 13, 14, 15, 16 |
| 4 | 2427 MHz | 2416–2438 MHz | 14, 15, 16, 17 |
| 5 | 2432 MHz | 2421–2443 MHz | 15, 16, 17, 18 |
| 6 | 2437 MHz | 2426–2448 MHz | 16, 17, 18, 19 |
| 7 | 2442 MHz | 2431–2453 MHz | 17, 18, 19, 20 |
| 8 | 2447 MHz | 2436–2458 MHz | 18, 19, 20, 21 |
| 9 | 2452 MHz | 2441–2463 MHz | 19, 20, 21, 22 |
| 10 | 2457 MHz | 2446–2468 MHz | 20, 21, 22, 23 |
| 11 | 2462 MHz | 2451–2473 MHz | 21, 22, 23, 24 |
| 12 | 2467 MHz | 2456–2478 MHz | 22, 23, 24, 25 |
| 13 | 2472 MHz | 2461–2483 MHz | 23, 24, 25, 26 |

### 読み方と注意点

- **中心周波数の計算式** :Wi-Fi = `2412 + 5×(チャンネル番号−1)` MHz、Thread = `2405 + 5×(チャンネル番号−11)` MHz
- **占有帯域は目安** です。実際の干渉の強さは変調方式・送信出力・距離によって変わるため、「重なる」と表にあっても実運用上は問題にならないケースもあります。逆に隣接帯域からの漏れ込みで、表にないチャンネルにも軽微な影響が出ることがあります。
- **Wi-Fiでよく推奨される「非重複チャンネル」は1, 6, 11** (海外)または **1, 6, 11, 13** (日本など13ch対応国)です。この3〜4チャンネルはWi-Fi同士では重ならない設計になっています。

### 実用的な組み合わせの例

もしWi-Fiルーターが **チャンネル1** を使っているなら、Threadは表の右列(11〜14)を避けて、 **チャンネル20以降** を選ぶと干渉を大きく減らせます。

逆にWi-Fiが **チャンネル11** を使っているなら、Threadは **チャンネル11〜17あたり** (低い方)を選ぶのが安全です。

---

<div class="post-metadata">

### Author: ![tk-fuse](https://forum.ficusonline.com/user_avatar/forum.ficusonline.com/tk-fuse/32/255_2.png) [@tk-fuse](https://forum.ficusonline.com/u/tk-fuse)
#### Post date: [2026 年 9 月 11 日午前 11:35 UTC](https://forum.ficusonline.com/t/topic/562/10 "2026-09-11T11:35:12Z")

</div>

## WiFiリセット機能追加

bootボタン長押しによるWiFi設定のリセット機能追加。WiFiリセットによりAPモードへ自動的に移行。

`esp-thread-br/examples/basic_thread_border_router/main/CMakeLists.txt`

```txt
idf_component_register(SRCS "esp_ot_br.c" "wifi_reset.c"
                       INCLUDE_DIRS "."
                       EMBED_TXTFILES ${project_dir}/server_certs/ca_cert.pem
                       )

```

`esp-thread-br/examples/basic_thread_border_router/main/esp_ot_br.c`

```diff
extern const uint8_t server_cert_pem_start[] asm("_binary_ca_cert_pem_start");
extern const uint8_t server_cert_pem_end[] asm("_binary_ca_cert_pem_end");

+ extern void wifi_reset_button_start(void);
.....
.....
.....
.....
.....
#if CONFIG_OPENTHREAD_BR_START_WEB
    esp_br_web_start("/spiffs");
#endif

    launch_openthread_border_router(&openthread_config, &rcp_update_config);

+ wifi_reset_button_start();

}

```

`esp-thread-br/examples/basic_thread_border_router/main/wifi_reset.c`

```c
/*
 * wifi_reset.c
 *
 * Pressing and holding the BOOT button (ESP32-C6-DevKitC-1: GPIO9, active-low)
 * for 3 seconds clears the Wi-Fi settings stored by esp_ot_wifi_cmd.c in its
 * custom "wifi_config" NVS namespace (keys: ssid / password), then reboots
 * the device.
 *
 * IMPORTANT: In this configuration, calling esp_wifi_restore() has no effect.
 * The SSID/PSK actually used for automatic Wi-Fi reconnection is stored in
 * the custom storage ("wifi_config" namespace), which is accessed through
 * esp_ot_wifi_config_get_ssid() / esp_ot_wifi_config_get_password().
 * Therefore, the settings must be cleared using the
 * esp_ot_wifi_config_clear() function provided by esp_ot_wifi_cmd.c.
 * This performs exactly the same operation as the
 * `wifi config clear` command in the serial console.
 *
 * PREREQUISITE: The BOOT button on GPIO9 is unrelated to the RCP pins
 * (GPIO7/8) and can be freely used as a GPIO input while the application
 * is running. GPIO8 is currently used by the LED driver, so it must not
 * be touched.
 */

#include "esp_log.h"
#include "esp_system.h"
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_ot_wifi_cmd.h" /* esp_ot_wifi_config_init/clear/get_ssid, esp_ot_wifi_disconnect, esp_ot_wifi_state_get */

static const char *TAG = "wifi_reset_btn";

#define WIFI_RESET_BUTTON_GPIO GPIO_NUM_9 /* BOOT button */
#define WIFI_RESET_HOLD_MS 3000 /* 3 seconds hold to reset */
#define WIFI_RESET_POLL_MS 100

static void wifi_reset_button_task(void *arg)
{
    gpio_config_t io_conf = {
        .pin_bit_mask = 1ULL << WIFI_RESET_BUTTON_GPIO,
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_ENABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_DISABLE,
    };
    gpio_config(&io_conf);

    int held_ms = 0;

    while (1) {
        if (gpio_get_level(WIFI_RESET_BUTTON_GPIO) == 0) { /* push down (Low) */
            held_ms += WIFI_RESET_POLL_MS;
            if (held_ms >= WIFI_RESET_HOLD_MS) {
                ESP_LOGW(TAG, "BOOT button held %dms, clearing stored Wi-Fi config...", held_ms);

                /* Disconnect if currently connected to prevent reconnection */
                if (esp_ot_wifi_state_get() == OT_WIFI_CONNECTED) {
                    esp_ot_wifi_disconnect();
                    vTaskDelay(pdMS_TO_TICKS(200));
                }

                /* Ensure the "wifi_config" NVS namespace is open before clearing it */
                esp_err_t init_err = esp_ot_wifi_config_init();
                if (init_err != ESP_OK) {
                    ESP_LOGE(TAG, "esp_ot_wifi_config_init failed: %s", esp_err_to_name(init_err));
                } else {
                    esp_err_t clr_err = esp_ot_wifi_config_clear();
                    if (clr_err != ESP_OK) {
                        ESP_LOGE(TAG, "esp_ot_wifi_config_clear failed: %s", esp_err_to_name(clr_err));
                    } else {
                        ESP_LOGW(TAG, "Stored Wi-Fi ssid/password cleared. Rebooting...");
                    }
                }

                vTaskDelay(pdMS_TO_TICKS(500));
                esp_restart();
            }
        } else {
            held_ms = 0; /* Reset the counter when the button is released */
        }
        vTaskDelay(pdMS_TO_TICKS(WIFI_RESET_POLL_MS));
    }
}

void wifi_reset_button_start(void)
{
    xTaskCreate(wifi_reset_button_task, "wifi_reset_btn", 3072, NULL, 5, NULL);
}

```

---

<div class="post-metadata">

### Author: ![tk-fuse](https://forum.ficusonline.com/user_avatar/forum.ficusonline.com/tk-fuse/32/255_2.png) [@tk-fuse](https://forum.ficusonline.com/u/tk-fuse)
#### Post date: [2026 年 10 月 25 日午前 5:24 UTC](https://forum.ficusonline.com/t/topic/562/11 "2026-10-25T05:24:09Z")

</div>

## ソースコード

> **[GitHub - capitalfuse/esp-thread-br: ESP Thread Border Router SDK (ESP32-C6 single-chip...](https://github.com/capitalfuse/esp-thread-br)**
>
> ESP Thread Border Router SDK (ESP32-C6 single-chip edition)

```auto
$ cd esp-thread-br
$ git switch -c standalone-design # 作業用ブランチを作成
$ git add .
$ git commit -m "Thread Border Router with the standalone ESP32-C6"

$ git remote rename origin upstream # リモートのoriginが元のレポジトリのURLのため、名称変更してoriginを新規作成するレポジトリ用に確保しておく
$ git remote -v
upstream	https://github.com/espressif/esp-thread-br.git (fetch)
upstream	https://github.com/espressif/esp-thread-br.git (push)

$ gh auth status #githubへのログイン状態確認

$ gh repo create esp-thread-br --public --description "ESP Thread Border Router SDK (ESP32-C6 single-chip edition)" --source=. --remote=origin --push # remoteをoriginにしてpush

$ git push -u origin --all # --push は現在のブランチだけを送るので、main と standalone-desogn の両方を送る場合に実行

```
