UbuntuのDockerコンテナでesp-idfを使う

Ubuntu上で複数の開発環境を使用しているのですが、環境設定が複雑になってきたため、各環境を"Docker"で個別に構築しています。
この記事ではESP-IDFをDockerコンテナで動かすために実行した内容を **覚書** として関係するリンクと内容を簡単にまとめます。

苦労したポイントは、コンテナからUSBデバイスのシリアルポートにアクセスする部分で **コンテナ起動時の引数とコンテナ内でのシリアルポートのアクセス権の設定** です。


## 開発環境
* ホスト: Ubuntu 22.04.5 LTS
* Docker: 28.0.1, build 068a01e

## Dockerのインストール

以下の記事を参考にDocker Engineをインストールします。

https://qiita.com/tf63/items/c21549ba44224722f301

### 事前準備
必要なパッケージをインストールします。
```shell-session
$ sudo apt update
$ sudo apt install ca-certificates curl gnupg
```

aptリポジトリを追加します。
```shell-session
$ sudo install -m 0755 -d /etc/apt/keyrings
$ sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
$ sudo chmod a+r /etc/apt/keyrings/docker.asc
$ echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
$ sudo apt update
```

dockerグループを作成し、ユーザーをグループに追加します。これによってroot以外のユーザーもDockerを実行できるようになります。
```shell-session
$ sudo groupadd docker
$ sudo usermod -aG docker "ユーザー名"
```

### Docker Engine のインストール
以下のコマンドでDocker Engineをインストールします。
```shell-session
$ sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
```

インストールされたことを確認しておきます。
```shell-session
$ docker --version
```

## Dockerコンテナの作成

### Dockerイメージの取得
Docker Hubのubuntuリポジトリからコンテナイメージを取得します。
今回は最新版ではなく22.04を使用しました。
```shell-session
$ docker pull ubuntu:22.04
```

Dockerイメージが取得されたことを確認します。
```shell-session
$ docker images
```

正常に取得されていたら、以下のように表示されます。
```shell-session:Response
$ docker images
REPOSITORY    TAG       IMAGE ID       CREATED        SIZE
ubuntu        22.04     a24be041d957   6 weeks ago    77.9MB
```

### コンテナの作成
取得したイメージ(ubuntu:22.04)からコンテナを作成します。
```shell-session
$ docker run -it ubuntu:22.04
```

以下のようにrootでログインした状態のコンテナが自動で起動します。
```shell-session:Container
root@xxx:/#
```

### コンテナの初期設定
コンテナに基本的なツールをインストールします。
```shell-session:Container
root@xxx:/# apt update
root@xxx:/# apt install sudo
root@xxx:/# sudo apt install vim
```

usbutilsとnet-toolsもインストールしておきます。
```shell-session:Container
root@xxx:/# sudo apt install usbutils
root@xxx:/# sudo apt install net-tools
```

ツールを実行するためにユーザを追加しておきます。
途中でパスワード設定が要求されますので設定しておきます。
```shell-session:Container
root@xxx:/# adduser "ユーザー名"
```

```shell-session:Response
Adding user `user' ...
Adding new group `user' (1000) ...
Adding new user `user' (1000) with group `user' ...
Creating home directory `/home/user' ...
Copying files from `/etc/skel' ...
New password: 
Retype new password: 
passwd: password updated successfully
Changing the user information for user
Enter the new value, or press ENTER for the default
	Full Name []: 
	Room Number []: 

	Work Phone []: 	Home Phone []: 
	Other []: 
Is the information correct? [Y/n] y
```

sudoグループに追加します。
```shell-session:Container
root@xxx:/# gpasswd -a "ユーザー名" sudo
```

ここまで設定したらコンテナを終了します。
```shell-session:Container
root@xxx:/# exit
```

### 初期設定済のDockerイメージの生成

コンテナの稼働状況を確認します。
```shell-session
$ docker ps -a
```

```shell-session:Response
CONTAINER ID   IMAGE          COMMAND       CREATED          STATUS                          PORTS     NAMES
2cf86377b941   ubuntu:22.04   "/bin/bash"   32 minutes ago   Exited (0) About a minute ago             trusting_chaum
```

設定済のコンテナからDockerイメージを生成します。(少し時間がかかります)
```shell-session
$ docker commit trusting_chaum ubuntu_init:22.04
```

イメージが生成されたことを確認します。
```shell-session
$ docker images
```

```shell-session:Response
REPOSITORY    TAG       IMAGE ID       CREATED              SIZE
ubuntu_init   22.04     8840e33bb613   About a minute ago   201MB
ubuntu        22.04     a24be041d957   6 weeks ago          77.9MB
```

## 初期設定済のDockerイメージからESP-IDF用のコンテナを起動

### 使用するデバイスの接続状況を確認
使用するデバイスをPCに接続し、ホストOSでデバイスの接続情報を確認します。
```shell-session
$ lsusb
```

```shell-session:Responseの例
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 003: ID 8087:0a2a Intel Corp. Bluetooth wireless interface
Bus 001 Device 002: ID 303a:1001 Espressif USB JTAG/serial debug unit
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
```

デバイスが認識されていたら、シリアルポートの名称を確認します。
```shell-session
$ sudo dmesg | grep tty
```

```shell-session:Responseの例
[    0.215010] printk: legacy console [tty0] enabled
[    5.742179] cdc_acm 1-4:1.0: ttyACM0: USB ACM device
```

この例では、"ttyACM0"という名称で認識されています。

### Dockerコンテナにデバイスをマウントして起動
Dockerコンテナをデバイスをマウントした状態で、先程追加したユーザー名(user)でログインして起動します。

```shell-session
$ docker run -it --user user --device=/dev/ttyACM0 -v /dev/ttyACM0:/dev/ttyACM0 --name esp-idf ubuntu_init:22.04
```
コンテナで"ttyACM0"が認識されているのを確認します。
```shell-session:Container
user@xxx:/$ ls /dev/tty*
```
```shell-session:Responseの例
user@7b260fb6bd53:/$ ls -la /dev/tty*
crw-rw-rw- 1 root root      5, 0 Mar 16 00:16 /dev/tty
crw-rw---- 1 root dialout 166, 0 Mar 15 22:39 /dev/ttyACM0
```

ユーザーのhomeに移動します。
```shell-session:Container
user@xxx:/$ cd
```

## ESP-IDFのツールチェーンのセットアップ
下記の手順に従い、ESP-IDFのツールチェーンをセットアップしていきます。

https://docs.espressif.com/projects/esp-idf/en/stable/esp32/get-started/linux-macos-setup.html

### 必要なパッケージを導入する
ESP-IDFを使用してコンパイルするために、次のコマンドでパッケージを導入します。
```shell-session:Container
user@xxx:~$ sudo apt-get install git wget flex bison gperf python3 python3-pip python3-venv cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0
```

### ESP-IDFを取得する
ESP-IDFをインストールするディレクトリに移動し、次の手順でリポジトリをクローンします。今回は以下の設定で実行しました。
* インストールディレクトリ: ~/esp/
* バージョン: v5.4 (2025/03/16時点の最新版)
ESP-IDFのバージョンについては[こちら](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/)を参照してください。
```shell-session:Container
user@xxx:~$ mkdir -p ~/esp
user@xxx:~$ cd ~/esp
user@xxx:~/esp$ git clone -b v5.4 --recursive https://github.com/espressif/esp-idf.git
```
ESP-IDFのクローンは **~/esp/esp-idf** にダウンロードされます。

### 必要なツールを設定する
ESP32をサポートするプロジェクトでは、ESP-IDFの他に、コンパイラ、デバッガー、Python パッケージなど、ESP-IDFで使用されるツールもインストールする必要があります。
以下のコマンドを参考にターゲットとするチップをリストしてスクリプトを実行します。
必要なツールはLinux 上のユーザーホームディレクトリ( **~/.espressif** )にインストールされます。
```shell-session:Container
user@xxx:~/esp$ cd ~/esp/esp-idf
user@xxx:~/esp/esp-idf$ ./install.sh esp32,esp32s3
```

サポートされているすべてのターゲット用のツールをインストールするには、次のコマンドを実行します。
```shell-session:Container
user@xxx:~/esp$ cd ~/esp/esp-idf
user@xxx:~/esp/esp-idf$ ./install.sh all
```

### 環境変数を設定する
ツールをコマンドラインから使用するためには、いくつかの環境変数を設定する必要があります。ESP-IDFには、それを実行するスクリプトが用意されていますので、次のコマンドを実行します。
**先頭のドットとパスの間のスペースに注意してください。**
```shell-session:Container
user@xxx:~$ . ~/esp/esp-idf/export.sh
```

以下のメッセージが表示されたら、設定完了です。
```shell-session:Response
Checking "python3" ...
Python 3.10.12
"python3" has been detected
Activating ESP-IDF 5.4
* Checking python version ... 3.10.12
* Checking python dependencies ... OK
* Deactivating the current ESP-IDF environment (if any) ... OK
* Establishing a new ESP-IDF environment ... OK
* Identifying shell ... bash
* Detecting outdated tools in system ... OK - no outdated tools found
* Shell completion ... Autocompletion code generated

Done! You can now compile ESP-IDF projects.
Go to the project directory and run:

  idf.py build
```

## サンプルプロジェクトで動作確認をする
ESP-IDFのプロジェクトを開始する準備が完了しましたので、実際にサンプルプロジェクト(hello_world")をビルドしてターゲット上で実行してみます。

### サンプルプロジェクトをコピーする
ESP-IDFのexamplesディレクトリからhello_worldプロジェクトをコピーします。
```shell-session:Container
user@xxx:~$ cd ~/esp
user@xxx:~/esp$ cp -r ~/esp/esp-idf/examples/get-started/hello_world ./
```

### プロジェクトを構成する
hello_worldディレクトリに移動し **ESP32S3** をターゲットとして設定します。
```shell-session:Container
user@xxx:~$ cd ~/esp/hello_world
user@xxx:~/esp/hello_world$ idf.py set-target esp32s3
```

プロジェクト構成ユーティリティ(menuconfig)を実行します。
```shell-session:Container
user@xxx:~/esp/hello_world$ idf.py menuconfig
```

この手順が正しく実行された場合、次のメニューが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120903/b3a66c1a-a374-790f-bef0-a232ff4c4787.png)

このメニューを使用して、Wi-Fi ネットワーク名とパスワード、プロセッサ速度、フラッシュの容量などのプロジェクト固有の変数を設定します。今回のプロジェクト(hello_world)はデフォルト構成で実行しますので、デフォルトのまま[Q]で終了します。

### プロジェクトをビルドする
次のコマンドを実行してプロジェクトをビルドします。このコマンドは、アプリケーションとすべてのESP-IDFコンポーネントをコンパイルし、ブートローダ、パーティション テーブル、およびアプリケーションバイナリを生成します。
```shell-session:Container
user@xxx:~/esp/hello_world$ idf.py build
```

"Project build complete."が表示されたら、ビルド完了です。エラーがなければ、ファームウェアのバイナリ(.bin)ファイルが生成されてビルドが終了します。
```shell-session:Response
Executing action: all (aliases: build)
Running ninja in directory /home/***/esp/hello_world/build
Executing "ninja all"...
[0/1] Re-running CMake...

～中略～

Project build complete. To flash, run:
 idf.py flash
or
 idf.py -p PORT flash
or
 python -m esptool --chip esp32s3 -b 460800 --before default_reset --after hard_reset write_flash --flash_mode dio --flash_size 2MB --flash_freq 80m 0x0 build/bootloader/bootloader.bin 0x8000 build/partition_table/partition-table.bin 0x10000 build/hello_world.bin
or from the "/home/tomorrow56/esp/hello_world/build" directory
 python -m esptool --chip esp32s3 -b 460800 --before default_reset --after hard_reset write_flash "@flash_args"
```

### デバイスにフラッシュする
デバイスに書き込む前に"/dev/ttyACM0"のアクセス権を"666"に修正します。
```shell-session:Container
user@xxx:~/esp/hello_world$ sudo chmod 666 /dev/ttyACM0
```

ESP32S3用にビルドしたバイナリを次のコマンドを実行してデバイスのポート(/dev/ttyACM0)に書き込みます。
```shell-session:Container
user@xxx:~/esp/hello_world$ idf.py -p /dev/ttyACM0 flash
```

問題がなければ、ボードが再起動し **hello_world** アプリケーションが起動します。
```shell-session:Response
Executing action: flash
Running ninja in directory /home/tomorrow56/esp/hello_world/build
Executing "ninja flash"...

～中略～

Compressed 3072 bytes to 103...
Wrote 3072 bytes (103 compressed) at 0x00008000 in 0.0 seconds (effective 559.3 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting via RTS pin...
Done
```

### シリアル出力をモニターする
**hello_world** が実際に実行されているかどうかを確認するために、IDFモニターアプリケーションを起動します。
```shell-session:Container
user@xxx:~/esp/hello_world$ idf.py -p /dev/ttyACM0 monitor
```

ログに"Hello world!"と表示されていれば問題なくプログラムが動いています。
IDFモニターを終了するには、ショートカット(Ctrl+])を使用します。
```shell-session:Response
Executing action: monitor
Running idf_monitor in directory /home/tomorrow56/esp/hello_world
Executing "/home/tomorrow56/.espressif/python_env/idf5.4_py3.10_env/bin/python /home/tomorrow56/esp/esp-idf/tools/idf_monitor.py -p /dev/ttyACM0 -b 115200 --toolchain-prefix xtensa-esp32s3-elf- --target esp32s3 --revision 0 /home/tomorrow56/esp/hello_world/build/hello_world.elf -m '/home/tomorrow56/.espressif/python_env/idf5.4_py3.10_env/bin/python' '/home/tomorrow56/esp/esp-idf/tools/idf.py' '-p' '/dev/ttyACM0'"...
--- esp-idf-monitor 1.5.0 on /dev/ttyACM0 115200

～中略～

Hello world!
This is esp32s3 chip with 2 CPU core(s), WiFi/BLE, silicon revision v0.2, 2MB external flash
Minimum free heap size: 390340 bytes
Restarting in 10 seconds...
Restarting in 9 seconds...
```

無事に動作したら、次回開始時に環境設定を読み込むように".bashrc"に以下を追記しておきます。

```shell-session:.bashrc
. ~/esp/esp-idf/export.sh
```


## ESP-IDF設定済のDockerイメージを生成

無事に動作したら、ESP-IDF設定済のコンテナからDockerイメージを生成しておきます。

まず、Dockerコンテナから抜けます。

```shell-session:Container
user@xxx:~/esp/hello_world$ exit
```

コンテナの稼働状況を確認します。
```shell-session
$ docker ps -a
```

```shell-session:Response
CONTAINER ID   IMAGE             COMMAND       CREATED          STATUS                          PORTS     NAMES
def595ae9ac0   esp-idf-2:22.04   "/bin/bash"   30 minutes ago   Exited (0) About a minute ago             esp-idf
```

コンテナが稼働中の場合は停止します。
```shell-session
$ docker stop esp-idf
```

ESP-IDFが設定済のコンテナからDockerイメージを生成します。(少し時間がかかります)
```shell-session
$ docker commit esp-idf esp-idf:22.04
```

イメージが生成されたことを確認します。
```shell-session
$ docker images
```

```shell-session:Response
REPOSITORY    TAG       IMAGE ID       CREATED              SIZE
esp-idf       22.04     e3a774de9044   5 hours ago      7.21GB
ubuntu_init   22.04     8840e33bb613   About a minute ago   201MB
ubuntu        22.04     a24be041d957   6 weeks ago          77.9MB
```

作業は以上になります。お疲れ様でした。

## その他の操作
### フラッシュ消去
フラッシュを消去することも可能です。フラッシュ メモリ全体を消去するには、次のコマンドを実行します。
```:Ubuntu
idf.py -p <PORT> erase-flash
```

OTA データが存在する場合は、消去には次のコマンドを実行します。
```:Ubuntu
idf.py -p <PORT> erase-otadata
```

## 参考情報(リンク)
### Build System
idf.pyのオプションの説明があります。

https://docs.espressif.com/projects/esp-idf/en/v4.3.4/esp32/api-guides/build-system.html

### m5stack のソフトウェアを esp-idf 環境で開発して動かす
5GFXライブラリを導入しサンプルを動かす方法が説明されています。

https://zenn.dev/mai/articles/72519e289973c6