CircuitPython開発の小技集(2024年6月版)

## 自己紹介

JINS Advent calendar 6日目です。キラキラしたサーバーレスの話の後はがっつりベアメタル(？)の話をウェアラブルチームの菰田がお送りします。

自分はJINS内で各種ハードウェアの研究開発、バイタルデータの分析をハンドリングしています。ハンドリングと言ってもJINSは文系の会社なので、社内にソフトウェアエンジニアっぽい人は増えてきたものの、この分野は一人体制です（募集していますよ！）。そしてそもそも私はソフトウェアエンジニアでもハードウェアエンジニアでもなく半導体とかデータ分析が元々の専門のため、なにかハードウェアが関わるPoCを行いたいときにC++が要求されるArduinoですら辛かったりします。

## CircuitPythonと私

そんな中、MicroPython/CircuitPythonは簡単にマイコンを使用したプロトタイプが作れるということで近年注目しています。両者の違いに関してはこの記事では言及しませんが、以下の理由からCircuitPythonをメインに使用しています。
- MicroPython はオレオレビルドや秘伝のタレが多いけど、CircuitPython は本家に情報がまとまっていることが多い
- CircuitPython は.pyファイルをドライブに書き込むだけなのでひと手間少なく開発できる

そんな感じで2023年はCircuitPythonをかなり使い込んでおり、コミュニティ活動としてボードやIMUドライバの追加に関するPRを本家にマージしていただいたりしています。いろいろToolを変えたりして試行錯誤していたのですが最近は落ち着いてきたので、この記事では開発に関する小技をご紹介したいと思います。いわゆるPoCを行う上でのサンプル集という意味では[circuitpython-tricks](https://github.com/todbot/circuitpython-tricks)([和訳](https://steam-tokyo.com/circuitpython-tricks/))をご覧になることをオススメいたしますが、こちらではどちらかというと本当に使い込もうとした時に困ってくるような課題に対するTipsのようなものを載せていけたらと思います。

（画像）2023年に追加したボード[M5AtomS3](https://qiita.com/komde/items/f1958006108878ca48ff)
![m5atoms3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103775/8cde689f-74e5-120b-50b3-52d2e707e629.jpeg)

## 開発の基本

- USB接続パターン
- BLE接続パターン
- WIFI接続パターン

の[3つあります](https://docs.circuitpython.org/en/latest/docs/workflows.html)が、USBドライブ化できないESP32, ESP32C3, ESP32C6のボード※を使う時以外は、**設定不要でどのOSでも安定しているUSB接続がオススメ**です。BLE/WIFI接続は稼働している端末に対してファームウェア書き換えがOTAでできるのが魅力的ですが、繋がるまで不安定なこともあったりします。

※JTAGのみ使用可能で汎用USBにはなっていない仕様のため、USBドライブアクセスができません。

USB接続を使用する場合、気にすることは2つしかありません。

- 接続するとUSBメモリのようにCIRCUITPYフォルダが見えるようになり、そこのPythonファイルを更新するだけでコードが再実行される。
- consoleへprint出力したメッセージをUSBシリアル通信で拾う。

なので、この2つが気持ちよく動作する、自分に合う組み合わせを見つけることが重要になります。

## IDE / Editor

まず[本家の見解がこちら](https://learn.adafruit.com/welcome-to-circuitpython/recommended-editors)です。どのエディタを使う場合でも、デバイスを外すときにumountすれば特に問題ないかと思います。VSCodeにはExtensionがありますが、個人的には型ヒントを出すためだけに使用し、シリアルコンソールは別プログラムで確認しています。


- 普段から使っているeditorがVSCodeかSublime→**VSCode** か Sublime
- Python初めてだからシンプルな見た目のeditorがいい→**Mu** か Thonny
- 10秒以内にcircuitpythonを試したい→ **[Web Editor](https://code.circuitpython.org/)** か **[CircuitPython Online IDE](https://urfdvw.github.io/CircuitPython-online-IDE/)**

## シリアルコンソール接続

以下の３つをよく使用します。VSCode内に出すなら上２つですが、あえて別ウィンドウで見たい時などはブラウザ開くだけで楽チンな３つ目を重宝したりします。

- cu
- VSCodeのエクステンション（USBのVID,PIDが正しく設定されていなものでは動作しません）
- [Web serialを使用したターミナル](https://googlechromelabs.github.io/serial-terminal/)

（画像）Web serialを使用したターミナル
![スクリーンショット 2023-12-01 8.08.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103775/796a1ad7-8c5f-bbab-4864-b0f37a7188b5.png)


## gitとの連動

あまり大規模なコードを書くことは無く、毎回2-3ファイルしか更新しないと思いますので、以下な感じに落ち着きました。

- CIRCUITPYドライブからgitのフォルダに手でコピー
- CIRCUITPYドライブからgitのフォルダにsyncコマンド等で同期を取る

gitのフォルダからCIRCUITPYドライブにコピー、ということもやったのですが、開発中はしょっちゅう更新するため逆方向をメインとしました。ちなみにCIRCUITPYドライブにgit直設定という荒業を試していた時期もあったのですが、当然ながらキャッシュが増えてくると容量オーバーして動かなくなるので、長期間メンテするようなリポジトリでは直設定は避けたほうが良いでしょう、、。


## 主要なトラブルとその対応方針

主要なトラブルとその対応方針は[こちら](https://learn.adafruit.com/welcome-to-circuitpython/troubleshooting)にまとまっています。自分がハマったのは以下の問題です。

### MacOS Sonoma で変なSoft rebootが発生する対策

MacOS Sonomaで以下の現象が発生します。

- ファイルを保存したはずが、反映前のコードが実行されている
- 変なタイミング（保存後10-15秒）でSoft rebootが発生する

こちら[本家でも認識](https://github.com/adafruit/circuitpython/issues/8449)されており、Mac OSで小さいサイズのファイルをすぐにFSに書き込まないことが原因で、[work around](https://learn.adafruit.com/welcome-to-circuitpython/troubleshooting#macos-sonoma-14-dot-x-disk-errors-writing-to-circuitpy-3160304)で対策可能です。

自分は Windows/Mac/Chromebook 全て使いますが、CircuitPython開発はマシンパワーは必要ないので、正直Chromebookが一番開発しやすいかもですね、、。

## 起動(リセット)の仕組みを理解する

こちら、言われるまで意外と気がつきにくいことであったりしますので簡単に説明しておきます。詳細は[こちら](https://learn.adafruit.com/circuitpython-essentials/circuitpython-resetting
)をご覧ください。

### 起動の順番

- 通常: boot.py -> code.pyの順に実行される。 boot.pyの結果はboot_out.txtにログられます。また、**boot.pyで定義した変数はcode.pyでは忘れ去られるので、何か保持しておきたい時はnvm等外側から読み込むようにして下さい**。
- 激しくクラッシュした場合: safemodeに移行します。抜けるにはhard resetをかけます。

### リセットの種類

- Soft reset: code.pyのみ再実行。通常コードを上書きした時はこちらの挙動になり、boot.pyをいじった時は Hard reset をかけないと反映されないので注意して下さい。
- Hard reset: boot.pyから再実行される。リセットボタンを押した時はこちらになります。

## boardで定義されていないPINにアクセスする

マイコンは同じだけど他の基板のファームウェアを使用した時などに、boardでPINが定義されていないことがあります。その場合は以下のようにmicrocontroller経由でアクセスします。

```
import microcontroller

microcontroller.pin.GPIO10
```

## ボタンレスでファイルアクセスを切り替える

長時間動かした時のログをデバイス内に残しておきたい時にファイル操作を行いたいことがあります。Pythonは高級言語なのにそんなこと困るの？と思われるかもしれませんが、普通に起動した時の**CIRCUITPYドライブが表示されているStorageモードの時はコード内からはファイルシステムがRead onlyになっている** という制約があります。そのため [CircuitPython Storage](https://learn.adafruit.com/circuitpython-essentials/circuitpython-storage) に書いてあるとおり、特定ピンをGNDに落として起動した時にモードを切り替えたりする工夫が必要です。

ただし、あまり結線が簡単でないボードだったりボタンがなかったりとかでそういうことができないケースも多いと思います。そういう時は以下のようにNVM(不揮発性メモリ領域)にフラグを残しておいて、起動時に読み込み分岐するようにします。

```boot.py
import microcontroller
import storage

boot_mode = microcontroller.nvm[0:1][0]
print("boot_mode: " + hex(boot_mode))

# 0xDBが書き込まれていたらWritableにする
if (boot_mode == 0xDB):
    storage.remount("/", False)
    # CIRCUITPYドライブが見えない方が良い場合は以下も実行
    storage.disable_usb_drive()

# 次回起動のために戻す（ちなみに初期値が0xFFです）
microcontroller.nvm[0:1] = bytearray([0xFF])

# code.pyでも使用する場合は、現在のモードをセットしておき、code.pyで読み込む
microcontroller.nvm[1:2] = bytearray([boot_mode])
```

モードの切り替えはREPLから実施します。

```REPL.py
import microcontroller
microcontroller.nvm[0:1] = bytearray([0x10]) #次回ファイル書き込みモードで起動する
```

## ファイルにtracebackを記録する

ファイルアクセスが可能な状態で以下のようにすることでファイルにtracebackを記録しておくことが可能です。これ実は丸1日試行錯誤していて、通常のPythonで行うような sys.stdout をファイルストリームに置き換える、という技は使えないみたいなのでご注意ください(AttributeError: can't set attribute 'stdout' と言われます、、)。

```traceback.py
import traceback

try:
	a = 1/0 # ZeroDivisionError を生成
except Exception as err:
	fp = open("./traceback.txt", "a")　# aで累積されて記録される
	traceback.print_exception(err, file = fp)
	fp.close()
```

先ほどの*ボタンレスでのファイルアクセス*と組み合わせると以下のようになります。

```code.py
import microcontroller
import traceback

# boot.pyでFS　writableにされていて、モードが記録されている
boot_mode = microcontroller.nvm[1:2][0]

# 処理のメイン
def main():
    ...

if (boot_mode != 0xDB):
    main()
else:
    try:
        main()
    except Exception as err:
        fp = open("./traceback.txt", "a")
        traceback.print_exception(err, file=fp)
        fp.close()
```

## なんか65秒後に止まることがある

こんな処理を書いてました（1秒に10回処理をしたい）。

```code.py
while True:
    start_ts = supervisor.ticks_ms()

    # モニョモニョ
    ...

    et = supervisor.ticks_ms() - start_ts
    wait = 0.1 - et / 1000

    if (0 < wait):
        time.sleep(wait)
```

このモニョモニョの部分がそれなりに見えるぐらいの時間がかかる処理だったので `time.sleep(0.1)` とやらずに残り時間を計算して待たせていました。ただこのコード、たまに止まります。毎回でなくたまに止まるのも厄介です。最初stacktraceを取ろうとしていたのですがどうもstackしていない。ハードクラッシュでもなさそう。

ある時、運よくシリアルコンソールと繋がっている時に止まりました。で、見てみるとエラーは出さず、 `time.sleep(wait)` で止まっているだけです。そこでsupervisor.ticks_ms()のマニュアルを見てみると、 **この関数は数日で値が一周するので（デバッグしやすいように）最初の６５秒目で一周するよ** と書いてありました。えー、、、、。つまりこのモニョモニュ中に一周してしまうと `et`　に6日がセットされてしまうという不具合でした。**supervisor.ticks_ms()ご使用の際はお気をつけ下さい**。

## デバイスを完全リセットする(RP2040)

なんか調子悪いとか、もうCircuitPython使わなくなったなどで完全にリセットしたい場合は、[nuke UF2](https://learn.adafruit.com/getting-started-with-raspberry-pi-pico-circuitpython/circuitpython) を書き込みます(ネーミングが、、)。

## 赤外線送受信

[ライブラリ](https://github.com/adafruit/Adafruit_CircuitPython_IRRemote) がなんか色々おかしかったのでPR送ってマージしていただきました(記事も徐々に修正されているようです)。1つ解決できなかった点があって、ESPでパルス長がヘッダー含め128しか受信しないっぽく、長めのIRコマンド（エアコンとか）を受信しきることができません。

## プログラム全体をDumpして、バックアップやデバイスコピーをする(RP2040)

こちら、CircuitPythonに限らずArduinoとかでも同じ技が使えるかと思います。例えば同じプログラムのデバイスを作りたい時にCircuitPythonをインストールした後CIRCUITPYドライブにファイルコピーするより、picotoolを使用すると、より手早くできます（Seeed Xiao RP2040で検証しています）。

- picotoolのインストール（macではhomebrewでインストール可能）
- Bootselモードにする
- 読み出し: picotool save --all test.uf2 (picoではないRP2040デバイスでは--allなどの使用が必要)
- 書き込み: picotool load test.uf2

他のチップでは以下あたりを使えば同じことができる気がします（情報あれば教えて下さい！）

- nRF...nrfjprog
- esp32...esptool

## socketpool とadafruit connection managerの違いは？

socketpoolは組み込みモジュール。とadafruit connection managerはライブラリでどちらもsocketpoolを作れます。基本は組み込みにあればsocketpoolを使う、で良い気がしますが、adafruit_minimqttの使用時にsocketpoolが少し不安定かも？と感じることがあり、調査中です。


## 最後に

CircuitPythonは慣れてくるとどんどん開発速度があげられるので習得が楽しい言語です。あっという間にマイコンを使ったプロトタイプを作れるのは本当に驚きです。これからも何か面白い小技を編み出した時はここに追記していきたいと思います。

明日も引き続き私からさらに地味なAdvent Calendarをお送りいたします。