Arduino-CLIでESP32の開発環境を整える

# はじめに

「おうちハック」で定番のESP32。ESP-IDFをご利用の方も多いとは思いますが、秋月や千石、マルツで買ってきたセンサーを手っ取り早く使うにはArduinoのライブラリをそのまま使えるArduino IDEのほうが都合よかったりします。

そんなわけで私もESP32をArduinoで使っております、が、しかし、ArduinoのIDEは使いにくい...

また、esp32のArduinoボードにはバージョン1系統（2023秋時点で最新はv1.0.6）とバージョン2系統（同2.0.14）がございますが、微妙に2系では安定して動かないということもあります。

ということで、本記事では以下のことを目指します。

- CLIでESP32 Arduinoの開発を行う（ArduinoのGUIは使わない）
- LSPと`Makefile`を使う
- 「ボード」のバージョン切り替えを可能にする
- OTAもmakeから可能とする
- 実行時例外発生時の遺言からの犯人探しをする（シリアル接続時・リモート時どちらも）
- macosまたはlinux環境
- Windows？ VScode？ PlatformIO？ 知らない子ですね...

追記: PlatformIOではLSPが使えないと思っていたのですが、どうやらclangdを自分でコンパイルするとLSPできるようです。

# ディレクトリ構成

| path | 説明  |
| --- | --- |
| ~/src/arduino/ | 開発環境のroot |
| ~/src/arduino/cli-config/ | arduino-cliのyamlの場所 |
| ~/src/arduino/core/esp32/x.y/ | ボードインストール先(x.yはバージョン) |
| ~/src/arduino/core/espota.py | espota.py |
| ~/src/arduino/core/decoder/ | 死因解明ツールの置き場 |
| ~/src/arduino/core/download/ | ダウンロードしたものを置く場所 |

# インストールと初期設定

## Arduino-CLIインストール

homebrewなどで適当にインストールします。

```shell-session
$ brew install arduino-cli
```

https://qiita.com/miso_develop/items/a02734d21c8363b6fd1c

## yamlの用意

`arduino-cli config init` で雛形を作ります。 macosの場合、`~/Library/Arduino15/arduino-cli.yaml` にできます。

残念ながら環境変数は展開してくれないようですのでバージョンに合わせて複数つくります。

```shell-session
$ arduino-cli config init
$ cp ~/Library/Arduino15/arduino-cli.yaml \
     ~/src/arduino/cli-config/arduino-cli-1.0.yaml
$ cp ~/Library/Arduino15/arduino-cli.yaml \
     ~/src/arduino/cli-config/arduino-cli-2.0.yaml
```

```~/src/arduino/cli-config/arduino-cli-1.0.yaml
board_manager:
  additional_urls:
    - https://espressif.github.io/arduino-esp32/package_esp32_index.json
directories:
  data: /home/sweethome/src/arduino/core/esp32/1.0/
  user: /home/sweethome/src/arduino/
  downloads: /home/sweethome/src/arduino/core/download/
```

```~/src/arduino/cli-config/arduino-cli-2.0.yaml
board_manager:
  additional_urls:
    - https://espressif.github.io/arduino-esp32/package_esp32_index.json
directories:
  data: /home/sweethome/src/arduino/core/esp32/2.0/
  user: /home/sweethome/src/arduino/
  downloads: /home/sweethome/src/arduino/core/download/
```

esp32のボードのURLはこちらに記載。

https://docs.espressif.com/projects/arduino-esp32/en/latest/installing.html

## ボードのインストール

```shell-session
$ arduino-cli --config-file ~/src/arduino/cli-config/arduino-cli-1.0.yaml \
  core install esp32:esp32@1.0.6
$ arduino-cli --config-file ~/src/arduino/cli-config/arduino-cli-2.0.yaml \
  core install esp32:esp32@2.0.14
```

## ccacheの追加

ccacheを追加してコンパイルを高速化します。ccacheなんて使うの、linux kernelの自前ビルドをしていた頃以来ですわ（老害発言）。

https://qiita.com/hidenorly/items/d150f1944b5eb274e27d

`find ~/src/arduino/core/ -name platform.txt` して見つけ出し、`recipe.c.o.pattern`と`recipe.cpp.o.pattern`の頭にccacheを追加します。

## OTAの用意

http(s)やmqttではないotaをするには、esp32のSSIDのLANと開発マシンの間でいくつかtcpが通る必要があります。 以下では、開発マシン→esp32に18266/tcp, esp32→開発マシンに28266/tcpを通しています（家庭内でVLANを切る変態）。

スケッチを格納しているディレクトリの名前でesp32のホスト名を登録しておきます。 つまり、`hoge/hoge.ino`を書き込んだesp32には`ping hoge`ができるということです。

Makefileの中で使いやすいようにsymlinkしておきます。

```shell-session
$ cd ~/src/arduino/core/
$ ln -s $(find esp32/2.0 -name espota.py) .
```

私の場合、常時otaを待機させるのではなく特定のmqttを送った後にのみota出来るようにしてメモリの節約をしています。 (wifi, ble, webserver, otaを全部同時に使うのは結構きついです...) そのため、espota.pyは直接Makefileから呼ばずにmqttを含んだラッパーを作っています。

## stack trace decoderのインストール

実行時クラッシュ時、シリアルに出てくるesp32の遺言を解析してくれる検視官を雇います。

https://github.com/littleyoda/EspStackTraceDecoder

```shell-session
$ mkdir ~/src/arduino/core/decoder/
$ cd !$
$ wget -nd https://github.com/littleyoda/EspStackTraceDecoder/releases/download/untagged-59a763238a6cedfe0362/EspStackTraceDecoder.jar
$ mkdir 1.0 2.0
$ cd 1.0
$ find ../../esp32/1.0/ -name \*addr2line
#適切なものを自分で選ぶ
$ ln -s ${みつけたもの} addr2line
$ cd -
$ cd 2.0
$ find ../../esp32/2.0/ -name \*addr2line
#適切なものを自分で選ぶ
$ ln -s ${みつけたもの} addr2line
```

## esp-coredumpのインストール

https://github.com/espressif/esp-coredump

```shell-session:
$ git clone https://github.com/espressif/esp-coredump.git
$ cd esp-coredump
$ pip3 install .
```

coreダンプ救出コードはこちらが参考になります。

https://github.com/MathieuDeprez/ESP32_CoreDump_Arduino_1.0.6

起動した時、前世で生成したcoredumpがフラッシュにあったら読み出してネットワーク経由で吐き出すようにしてあげます。
syslogにhexで出すのが良いでしょう。

https://github.com/arcao/Syslog

`xtensa-esp32-elf-gdb` は `core/esp32/2.0/` の下にあるものを使えばokです。

上記の方法でdumpした場合、最初の20バイトはヘッダーのようですのでスキップするとELFになります。

```
$ esp-coredump info_corefile \
    --core ${corefile} \
    --core-format elf \
    build-esp32/${name}.ino.elf
===============================================================
==================== ESP32 CORE DUMP START ====================

================== CURRENT THREAD REGISTERS ===================
pc             0x40094164          0x40094164 <tlsf_malloc+308>
lbeg           0x4008a9d4          1074309588
lend           0x4008a9de          1074309598
lcount         0x0                 0
sar            0x1d                29
ps             0x60823             395299
threadptr      <unavailable>
br             <unavailable>
...
```

rsyslogdが動いているマシンからlogをscpしてきて、hexdumpをbinaryにして20バイトスキップして上記のコマンドを実行するラッパー`esp32-coredecoder`を作っておきます。

これで、シリアルが繋がっていない時に死亡しても死因の解明ができるようになります。

## arduino-language-serverのインストール

https://github.com/arduino/arduino-language-server

```shell-session
$ git clone https://github.com/arduino/arduino-language-server.git
$ cd arduino-language-server
$ go build
$ install arduino-language-server ~/bin/
```

言語サーバの起動は以下のようになりますので、お使いのエディッタやIDEの設定に入れておきます。

```sh
arduino-language-server \
         -clangd /opt/homebrew/bin/clangd \
         -cli /opt/homebrew/bin/arduino-cli \
         -cli-config $HOME/src/arduino/cli-config/arduino-cli-2.0.yaml \
         -fqbn esp32:esp32:esp32
```

emacsの場合、`.dir_locals.el`を使うことで、host nativeの`clangd`を阻害することなくArudino開発ができます。
なお、emacsの自動生成lock fileがプロジェクトのディレクトリに生成されるとclangdが落ちるので場所を変更するか無効化します。

https://www.emacswiki.org/emacs/LockFiles

# Makefile

```makefile:Makefile
VERSION   = 2.0
PARTITION = min_spiffs
PORT      = /dev/cu.usbserial-0001
SPEED     = 74880

ifneq ("$(wildcard env.mk)","")
   include env.mk
endif

SKETCH       = $(notdir $(CURDIR)).ino
NAME         = $(notdir $(CURDIR))
TARGET_DIR   = $(CURDIR)/build-esp32

FQBN         = esp32:esp32:esp32
BOARDOPTIONS = PSRAM=disabled,PartitionScheme=$(PARTITION),CPUFreq=240,FlashMode=qio,FlashFreq=80,FlashSize=4M,UploadSpeed=115200,DebugLevel=none

CLI_YAML     = $(HOME)/src/arduino/cli-config/arduino-cli-$(VERSION).yaml

ESPOTA       = python3 $(HOME)/src/arduino/core/espota.py --debug --port 18266 --host_port 28266 --auth=secret --progress
DECODER      = $(HOME)/src/arduino/core/decoder/EspStackTraceDecoder.jar
ADDR2LINE    = $(HOME)/src/arduino/core/decoder/$(VERSION)/addr2line

# syslogにでたcoreをデコードするラッパーを作っておく
COREDECODER  = $(HOME)/bin/esp32-coredecoder

.PHONY: build

put:  build ota
sput: build upload monitor

build:
    @ mkdir -p $(TARGET_DIR)
    arduino-cli compile \
        --config-file $(CLI_YAML) \
        --fqbn $(FQBN) \
        --board-options $(BOARDOPTIONS) \
        --build-path $(TARGET_DIR) \
        $(CURDIR)

clean:
    rm -rf $(TARGET_DIR)

ota:
    $(ESPOTA) --ip $(NAME) --file $(TARGET_DIR)/$(SKETCH).elf

upload:
    arduino-cli upload \
        --config-file $(CLI_YAML) \
        --fqbn $(FQBN) \
        --board-options $(BOARDOPTIONS) \
        --input-dir $(TARGET_DIR) \
        --port $(PORT)

monitor:
    arduino-cli monitor \
        --config-file $(CLI_YAML) \
        --fqbn $(FQBN) \
        --port $(PORT) \
        --config baudrate=$(SPEED)
        
decode: trace.txt
    java -jar $(DECODER) $(ADDR2LINE) $(TARGET_DIR)/$(SKETCH).elf trace.txt

decodecore:
    $COREDECODER --name $(NAME) --elf $(TARGET_DIR)/$(SKETCH).elf

```

# 開発

スケッチのあるディレクトリでその個体固有の設定を `env.mk` に書きます。

```makefile:env.mk
VERSION = 1.0
PORT    = /dev/cu.wusbserial-1234
```

あとはガリガリ書いて`make put`するだけ。

うまく行かないときでも前世のcoreがとれていれば、`make decodecore` です。
「そんなcore産んだ覚えはありません」となったら、泣きながらttyを接続して`make monitor`して遺言を`trace.txt`に書き、`make decode`すると犯人がわかります。
otaができない時も泣きながらシリアルを繋げて`make upload`します。

# ところでinoは？

うちの`*.ino`はほとんど空っぽで、似非シングルトンなc++のクラスの中で全部処理しています。
完全に中身空っぽ(0バイト、つまり`rm hoge.ino; touch hoge.ino`)で、`.cpp`に以下を入れておいても問題ありません。

```c++:hoge/hoge.ino
#include "Hoge.h"

HogeBase *hoge = 0;

void setup(void)
{
  hoge = new Hoge();
  hoge->setup();
}

void loop(void)
{
  hoge->loop();
}
```

多数のesp32で遊んでいるので、共通した処理（自分がよく使うセンサー、mqtt処理、influxdbやvictoria metricsへの書き込み、prometheusの`/metrics`など）の入った基底クラスをつくっておいて、個体ごとに違うセンサーなどは継承したクラスの中で処理しています。
調子に乗ってなんでも入れるとフラッシュが足りなくなるのですが...

Arduinoのライブラリは使いたいがinoはキモいので嫌い、c++の機能はしっかり使いたい、でもLSPに助けてもらいたい、というわがまま。

# おわりに

以上でemacsで`c++-ts-mode`にeglotして`M-x compile`といった人も幸せになれます。 

Macも次はARMからコイツに移行するのではないか、といわれているRISC-Vを搭載したESP32C3が混在するときも、同じような方法で`Makefile`や`env.mk`, `.dir_locals.el`に細工すればいけます。

Happy おうちHacking!

# 付録: rosettaless

M1が出てもう4年、ディアボロ天才少女のロゼッタさんもそろそろ引退です。
2つほどx86_64の残党を追放します。

## esptool

```shell-session
$ brew install esptool
```

```:platform.txt
tools.esptool_py.path=/opt/homebrew/bin/  
tools.esptool_py.cmd=esptool.py
```

## ctags

https://github.com/arduino/arduino-ide/issues/2337

https://github.com/arduino/ctags/issues/20

```shell-session
$ git clone https://github.com/arduino/ctags.git
$ cd ctags
$ ./configure
$ vi general.h
%% 60行目付近のifをelseのみ有効にする
$ make
$ install ctags ~/src/arduino/core/esp32/2.0/packages/builtin/tools/ctags/5.8-arduino11/
```

そのうちrosetta廃止で強いメモリモデル互換モードを捨てて少しスリムになるのかもしれませんね。