編集履歴一覧に戻る
uchanのアイコン画像

uchan が 2021年01月24日11時41分42秒 に編集

コメント無し

本文の変更

洗濯物を後で干そうとして、あるいは洗濯機の終了音に気づかずそのまま朝まで忘れたことはありませんか?私はちょくちょくやってしまいます。放置しちゃうとシワになるし、匂いも気になりますよね。 この記事では、洗濯物が残っていることを感知してLINEで通知を飛ばす装置の構成と成果物を紹介します。要素技術としてはフォトリフレクタ(反射型光センサ)やESP32マイコンを搭載したWiFiモジュールの使い方を扱います。1,500円ほどで制作可能なので、皆さんも是非作ってみてください。 ![洗濯機に設置された通知器](https://camo.elchika.com/10498b0ab53177d2324989e5ddc648ca6b0318b3/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f63333936313234302d643365342d346361652d396632662d3965396366383634616562342f62326331393932612d663130642d343232612d623133632d656663633163333663613064/) ## 目次

-

- [システム構成図](#h_システム構成図)

+

- <a href="#system-arch">システム構成図</a>

- [検知部分の仕組み](#h_検知部分の仕組み) - [部品リスト](#h_部品リスト) - [回路の説明](#h_回路の説明) - [さらなる省電力化にむけて](#h_さらなる省電力化にむけて) - [ESP32の動作](#h_ESP32の動作) - [ソースコードの紹介](#h_ソースコードの紹介) - [main.c](#h_main.c) - [my_ulp.S](#h_my_ulp.S) - [ハマりどころ](#h_ハマりどころ)

-

## システム構成図

+

## <a name="system-arch">システム構成図</a>

システム全体の構成は次のようにしました。 ESP32マイコンで洗濯物が残っていることを検知し、LINE Notify APIを使ってスマホに通知を飛ばします。 :::plantuml:システム構成 @startuml archimate #Technology "ESP32" as esp32 <<technology-device>> cloud "LINE Notify" as line archimate #Technology "スマホ" as phone <<technology-device>> rectangle 洗濯機 cloud "WiFi AP" as wifi 洗濯機 <- esp32 : 監視 esp32 -up-> wifi : 通知 wifi -> line line -down-> phone @enduml ::: LINE Notify APIを使ったのは使い方が簡単そうだったからです。 スマホに通知するAPIとしてこれほど簡単なものが他にあるでしょうか…というくらい簡単に使えました。 ![システムからの通知例](https://camo.elchika.com/4c39dece6128517a5ee870b79ae48fd5cda12e82/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f63333936313234302d643365342d346361652d396632662d3965396366383634616562342f32383830623434302d343131612d343437332d616230312d303234313132366534643561/) 上図は実際のLINE通知の例です。洗濯物が残っている(蓋が閉じている状態が60分以上継続している)ことを検知すると「sentaku mono remains」というメッセージが来ます。さらにその30分後にもまだ洗濯物が残っていると「!!!」が加えられたメッセージが来ます。(実験のために120倍速にしている影響でタイムスタンプがおかしいですが無視してください。) ![洗濯機に設置した様子](https://camo.elchika.com/1bea3db4ac0a14cdeb37453d50106cfe568c237b/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f63333936313234302d643365342d346361652d396632662d3965396366383634616562342f39373035323136362d383464372d343831302d626361612d326339663062643236393330/) こんな風に洗濯機に設置して使います。蓋を開けたときにセンサと蓋の距離が最適になるよう、設置場所を調整してください。 ## 検知部分の仕組み 洗濯物が残されていることを直接検知するのは大変なので、洗濯機の蓋が開いているかどうかを検知することにします。洗濯中は蓋を閉じ、洗濯物を取り出した後は蓋を開けっ放しにする運用を前提とします。 ![蓋が開いているときの様子](https://camo.elchika.com/34590a67a6c6c22579be9387c50dc463555441e3/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f63333936313234302d643365342d346361652d396632662d3965396366383634616562342f65323231663132342d376163302d343765332d393863392d623237666564393834373565/) フォトリフレクタで蓋の開け閉めを検知するのがこの装置の核となる技術です。閉じたことを検知してから設定した時間(例えば1時間)が経過しても蓋が開かないとき、LINEに通知します。その後も蓋が開くまでの間、定期的にLINEに通知します。 ESP32-DevKitC-32EはWiFiにより通信可能です。自宅のWiFiに接続し、LINE通知を行います。プログラムを修正すればLINE通知以外に、Twitterでメンションしたり、宅内に立てたHTTPサーバにアクセスしたりと、様々な処理ができます。Bluetooth接続機能もあるため、スマホに直接通知を飛ばしたり、Bluetoothスピーカを鳴らすなんてこともできるのではないかと思います。 ところで、フォトリフレクタの値を定期的に読むというシンプルな処理は可能な限り少ない電力で済ませたいところです。ESP32には省電力モードがあるため、これを活用してエコな装置に仕上げます。 ## 部品リスト - [ESP32-DevKitC-32E(4MB)](https://akizukidenshi.com/catalog/g/gM-15673/) - [TPR-105F](https://akizukidenshi.com/catalog/g/gI-07266/) - 抵抗器2つ(200Ω、11kΩ) ## 回路の説明 今回作った装置の回路図を示します。マイコンとしてESP32の開発ボードを使ったため、外付け部品がとても少なく済みました。 ![洗濯物干してないぞ通知器の回路図](https://camo.elchika.com/3f98ed2009be745401bbc2d0ae3b5ac018fa2031/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f63333936313234302d643365342d346361652d396632662d3965396366383634616562342f32656134616231632d613339372d343035652d623566632d383531663063626437346536/) TPR-105Fがフォトリフレクタです。1、2番ピンに接続されたLEDに電流を流すと赤外線が放出され、反射した赤外線をフォトトランジスタが受光すると3、4番ピンの間に電流が流れるという仕組みです。3、4番ピンに電流が流れるとR2の両端に電圧が発生しますので、その電圧値をSENSOR_VPピンで読み取ります。 洗濯物が残っているかを確認するだけの装置ですから、そんなに頻繁にセンサーを読む必要はありません。1分に1回も読めば十分な精度が出るでしょう。すなわち、ほとんどの時間はセンサーはOFFで、1分おきにほんのわずかな時間(0.1msとかそのくらい)だけONになれば十分です。 そのため、フォトリフレクタに供給する電源をON/OFFできるようにしました。具体的にはIO27を使い、TPR-105FのLEDおよびフォトトランジスタへの電源供給を制御します。IO27をHigh(3.3V)にするとLEDが点灯し、フォトトランジスタが動作します。LEDは10mA程度の電流を消費しますので、この工夫による省電力効果は非常に高いです。 ちなみに、今回はESP32開発ボードのUSB端子で電源供給しましたが、乾電池4本が入る電池ボックスを買ってきてEXT_5V端子に接続すれば、コンセントに空きがなくても使えます。EXT_5Vと書いてありますが、1.5V×4=6V程度なら印加しても大丈夫だろうと思います。ESP32開発ボードの回路図を見る限り、EXT_5Vに接続されたコンデンサの耐圧は10Vだそうなので、マージンを考えて8V以上は印加しない方が良いと思います。006P電池は最初に9V以上の電圧が出るので、使わないほうが良いでしょう(ダイオードを2、3個直列につなぎ、電圧を下げる手もありそうですけど)。 ### さらなる省電力化にむけて ESP32開発ボードに電源をつなぐと赤色LEDが明るく光ります。CPUが省電力モードにある間もずっと点灯しちゃうので、かなりの電力消費量となります。どのくらいになるか、計算してみます。 このLEDが無いとすると、1日あたりの消費電力量は概算で2.56mAh程度です。 ※ 2×(30mA×5秒+190mA×0.5秒+100mA×0.5秒)+100μA×(86400-12)+10mA×(86400/60)×100u秒≒2.56mAh 単三形のアルカリ乾電池の容量を2800mAhとすると1094日、約3年は連続稼働できる計算になります。十分に長いです。 このLED単体の1日あたりの消費電力量は大体48mAh(2mA×24時間)ですから、LEDが無いときの消費電力量が誤差に見えるほどの量となってしまうのです。乾電池で動かす場合、2800/(2.56+48)≒55となり、2ヶ月持たないことになります。 ということで、ESP32開発ボードのLEDは取り外すか、ESP32マイコンを単体で買ってきてLED無しで組み立てるのが、省電力化には非常に有利ということになります。今回はそこまではやりませんでしたが。 ## ESP32の動作 ESP32にはメインコアとULPコプロセッサという2種類のプロセッサが載っていまして、それらが複雑に関連して動作します。その様子を図をもとに説明します。 :::plantuml:処理の流れ @startuml skinparam ParticipantPadding 50 header Header participant メインコア participant ULPコプロセッサ as ULP activate メインコア メインコア -> ULP: ulp_run() activate ULP deactivate メインコア ULP -> センサ: センサ値 < 150 hnote over メインコア: Deep Sleep ULP -> センサ: センサ値 ≧ 150 hnote over ULP: 150以上が60分継続 ULP -> メインコア: wake activate メインコア ULP -> ULP: ULPタイマを無効化 deactivate ULP メインコア -> メインコア: 通知を送信 メインコア -> ULP: ulp_run() activate ULP deactivate メインコア @enduml ::: メインコアはセンサ値読み取りのためのADC(アナログデジタル変換器)などの初期化をした後、ULPコプロセッサ用プログラムの準備を行い、最後にULPコプロセッサを起動してDeep Sleepモードに移行します。ULPコプロセッサがwake命令を発行するまでDeep Sleepが継続します。 ULPコプロセッサは1分に1回程度の頻度でセンサ値を読みます。センサ値をメインコアが設定したスレッショルド(この記事を書いているときは150を設定している)と比較し、スレッショルド以上の値が60分間継続したらメインコアをwakeで起こします。この間、1回でも150を下回るとカウンタがリセットされて再度60分待つモードに戻ります。なお、スレッショルド以上の値が90分継続した場合、メインコアを起こす頻度が30分に1回に短縮され、より洗濯物の干し忘れに気づきやすくなります。 ULPコプロセッサは、wake命令を発行した直後にULPウェイクアップタイマを無効にして、自身の動作を止めます。ULPコプロセッサはULPウェイクアップタイマにより起動し、事前に設定されたプログラムを実行し、次のタイムアウトを待つ、という動作を繰り返します。タイマを止めないとメインコアが起動した後も裏で動き続けてしまいますから、wakeの後にタイマを止める必要があります。…今回のプログラムではどうせ1分に1回しかULPコプロセッサは動作しませんので、ここで止める必要はないかもしれません。 メインコアはULPコプロセッサによりDeep Sleepから目覚めたことを検知すると、WiFiに接続し、LINEに通知を送信し、WiFiから切断した後、ULPコプロセッサを再始動させてからDeep Sleepに移行します。以後、この動作をずっと繰り返します。 ## ソースコードの紹介 ESP32のプログラミングはArduino IDEで行う例が多いみたいですが、私はESP-IDFを使うことにしました。 その方が細かいところを制御できそうだったのと、ULP(Ultra Low Power)コプロセッサを使うにはESP-IDFじゃないといけない気がしたからです。(もしかしたらArduino IDEでもULPコプロセッサを使えるのかもしれませんが、調査していません) まず全体のファイル構成を示します。 $HOME/esp/ esp-idf/ esp-laundry-alert/ sdkconfig CMakeLists.txt main/ CMakeLists.txt component.mk line_notify_api_token main.c ulp/ my_ulp.S esp-idfは[ESP-IDF](https://github.com/espressif/esp-idf)をgit cloneしたディレクトリです。esp-laundry-alertが今回作成したディレクトリで、洗濯物干してないぞ通知器のプログラムです。完全なソースコードは https://github.com/uchan-nos/esp-laundry-alert にあります。 CMakeLists.txtやcomponent.mkはプロジェクト構成などを記入したファイルであり、プログラムの本筋とは関係ありません。説明は省略します。 main.cとmy_ulp.Sがプログラムの本筋です。順に紹介していきましょう。 ### main.c ESP32のメインプログラムを書いたファイルです。ESP-IDFのexamples/get-started/hello_worldを改造して作りました。まず、メイン関数`app_main()`を抜粋して掲載します。 ```c:main.c void app_main(void) { esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause(); if (cause != ESP_SLEEP_WAKEUP_ULP) { printf("wake up not from ULP\n"); init(); } else { printf("wake up from Deep Sleep!\n"); ESP_ERROR_CHECK(nvs_flash_init()); ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default()); /* This helper function configures Wi-Fi or Ethernet, as selected in menuconfig. * Read "Establishing Wi-Fi or Ethernet Connection" section in * examples/protocols/README.md for more information about this function. */ ESP_ERROR_CHECK(example_connect()); send_notify(); } printf("Hello world!\n"); printf("going deep sleep...\n"); adc1_ulp_enable(); esp_sleep_enable_ulp_wakeup(); ESP_ERROR_CHECK( ulp_run(&ulp_entry - RTC_SLOW_MEM) ); esp_deep_sleep_start(); esp_restart(); } ``` `esp_sleep_get_wakeup_cause()`でスリープ状態からの復帰がULPコプロセッサによるものなのか、そうではない(=初回起動)のかを表す値を取得できます。初回起動であれば`init()`を呼び出してもろもろの初期化を行います。 ULPコプロセッサからの復帰ならばセンサ値が150以上の状態が継続していることを意味します。`example_connect()`でWiFiに接続し、`send_notify()`でLINEに通知を送ります。 `init()`による初期化、あるいは`send_notify()`による通知が完了したらDeep Sleepに移行する準備を行います。`adc1_ulp_enable()`はULPコプロセッサがADCを使えるようにするための設定です。`esp_sleep_enable_ulp_wakeup()`はメインコアがULPコプロセッサによりDeep Sleepから復帰するように設定します(この他、復帰要因としてはタイマやGPIOの信号変化などもあります)。 必要な設定ができたら、`ulp_run()`でULPコプロセッサを起動し、その後`esp_deep_sleep_start()`によってメインコアをDeep Sleepモードにします。Deep SleepモードになるとULPコプロセッサ用の低速RAM以外のデータは消えるので、Deep Sleepから復帰する際は`app_main()`の先頭から処理が始まります。したがって最後の`esp_restart()`は実行されませんが、念のため書いておきました。 次は通知処理の心臓部、`send_notify()`を説明します。リポジトリで公開しているコードから説明のためにエラー処理を取り除き、意味が変わらない範囲で変形して掲載します。 ```c:main.c #define WEB_SERVER "notify-api.line.me" #define WEB_PORT "443" #define WEB_URL "https://notify-api.line.me/api/notify" static const char *TAG = "example"; static const char *REQUEST = "POST " WEB_URL " HTTP/1.1\r\n" "Host: "WEB_SERVER"\r\n" "User-Agent: esp-idf/1.0 esp32\r\n" "Content-Type: application/x-www-form-urlencoded\r\n" "Authorization: Bearer " #include "line_notify_api_token" "\r\n"; static void send_notify(void) { char buf[512], msg[256]; ssize_t msg_len; uint16_t sensor_phase = ulp_sensor_phase & 0xffffu; if (sensor_phase < 2) { return; } else if (sensor_phase == 2) { msg_len = sprintf(msg, "message=sentaku+mono+remains"); } else { msg_len = sprintf(msg, "message=sentaku+mono+remains!!!"); } esp_tls_cfg_t cfg = { .crt_bundle_attach = esp_crt_bundle_attach, }; struct esp_tls *tls = esp_tls_conn_http_new(WEB_URL, &cfg); tls_write(tls, REQUEST, strlen(REQUEST)); ssize_t buf_len = sprintf(buf, "Content-Length: %d\r\n\r\n%s", msg_len, msg); tls_write(tls, buf, buf_len); ESP_LOGI(TAG, "Reading HTTP response..."); // レスポンスの受信 exit: esp_tls_conn_delete(tls); putchar('\n'); // JSON output doesn't have a newline at end } ``` 変数`REQUEST`はLINE Notify APIに送るHTTPリクエストのリクエストヘッダを表します。このAPIは通知データをPOSTにより送る仕様になっています。User-Agentは適当な名前でかまいません。Content-TypeはPOSTで送るデータの種類を表示します。URLエンコード方式でフォームデータ(今回は`message=sentaku+mono+remains`)を送るための設定をします。 [LINE Notify API Document](https://notify-bot.line.me/doc/ja/)にある通り、Authorizationには`Bearer <access_token>`を指定します。ここに指定するアクセストークンはLINE Notifyにログインしてマイページにある「アクセストークンの発行(開発者向け)」から発行します。 発行したトークンは秘密の文字列ですのでリポジトリにアップロードするわけにはいきません。ですので、トークン文字列だけを記載したファイルline_notify_api_tokenを用意し、これをソースコードに組み込んで使用します。line_notify_api_tokenの内容は次のような形式です。 ```txt:line_notify_api_token "アクセストークンのランダム文字列" ``` トークンの両端を`"`で囲んでいることがポイントです。これにより、main.cにインクルードした際にC言語の文字列として認識されるようになります。その結果、前後の文字列リテラルと結合し、1つの文字列リテラルとなり、それが変数`REQUEST`に設定されます。(C言語では隣り合う文字列リテラルは自動的に結合するのです。) `send_notify()`の中では、まずULPコプロセッサが設定した変数`ulp_sensor_phase`を読み取り、センサ値≧150が60分経過した段階(フェーズ=2)なのか、それ以降(フェーズ>2)なのかを判定し、それによってメッセージを変化させます。ここでメッセージの文字数が変わるため、後ほどContent-Lengthの値を動的に生成しなければならず、プログラムが複雑化する原因となっています。ここで得た文字数`msg_len`がContent-Lengthの値になります。 その後`send_notify()`はHTTPS通信のためにTLS機能を初期化します。その後はいよいよHTTPリクエストヘッダとリクエストボディを送ります。最後にLINE Notify APIからのレスポンスを読み取って表示(UARTに送信するだけ)し、コネクションを閉じたら処理終了です。`send_notify()`から`app_main()`に戻り、直後にDeep Sleepモードに移行します。 ### my_ulp.S ULPコプロセッサでは定期的にセンサを読み、適切にメインコアを起床させます。ULPコプロセッサ用のプログラムはアセンブラで書かなければいけませんので、ちょっと読みづらいですが説明してみます。 ULPコプロセッサがウェイクアップタイマで動作を開始したときに実行される`entry`ルーチンを次に示します。 ```as:my_ulp.S .text .global entry entry: // power on the photo sensor WRITE_RTC_REG(RTC_GPIO_OUT_REG, 14 + 17, 1, 1) WAIT 800 // 100us (ULP clock = 8MHz) move r1, sensor_threshold ld r1, r1, 0 adc r0, 0 /* SARADC1 */, 0 + 1 /* channel 0 */ // power off the photo sensor WRITE_RTC_REG(RTC_GPIO_OUT_REG, 14 + 17, 1, 0) sub r2, r0, r1 // r2 = r0 - sensor_threshold jump lid_closed, ov // jump if r0 < sensor_threshold // lid is open // clear sensor_phase and lid_closed_counter move r0, 0 move r1, sensor_phase st r0, r1, 0 move r1, lid_closed_counter st r0, r1, 0 halt ``` `entry`ルーチンが最初に実行されるのは、main.cの`app_main()`の中で`ulp_run(&ulp_entry - RTC_SLOW_MEM)`と書いたからです。ESP-IDFのルールでは、C言語側からアセンブラで定義した識別子(変数や関数の名前)を使うときは`ulp_`を前置した名前で参照することになっています。すなわちC言語で書いた`ulp_entry`はアセンブラの`entry`を意味します。 `entry`の実行が始まると、最初にフォトリフレクタの電源を入れます。フォトリフレクタの電源はIO27に接続してあるのでしたね。ということで、IO27に1を書けばフォトリフレクタに3.3Vが供給されます。メインコアとULPコプロセッサではIOの番号が異なります。IO27は、ULPコプロセッサからは17番と認識されます。 `WRITE_RTC_REG`というマクロでIOポートに出力します。このマクロは`WRITE_RTC_REG(レジスタ, ビット位置, ビット幅, 値)`という文法です。指定したレジスタの指定したビットにvalueを書き込みます。RTC_GPIO_OUT_REGというレジスタのビット31:14がIOポートに対応しているため、IO番号に14を足した値が対応するビット位置となります。したがって、IO27(ULPからは17番)はビット31(14+17)です。 フォトリフレクタの電源を入れた後、ちょっと待たないとセンサがうまく動作しないらしく、800サイクルだけ待つことにしました。ULPコプロセッサのクロックは8MHzだそうなので、800サイクル=100μsということになりますね。とりあえずこのくらい待てば十分ということが実験で分かりました。最小値は探求していません。 次にR1レジスタにセンサ値の閾値を読み込みます。閾値はC言語側から設定できるよう、グローバル変数`sensor_threshold`として定義しています。この値は、今のところ150です。 adc命令でADC(アナログデジタル変換器)の値を読むことができます。フォトリフレクタはSENSOR_VPピン、つまりADC1のチャンネル0に接続してありますから、それをR0レジスタへと読み込む命令は`adc r0, 0, 1`となります。adc命令の第2オペランドはADC1/2の指定、第3オペランドはチャンネルの指定となっています。チャンネル番号はなぜか1を足す必要があります。(謎の設計ですね…) センサ値から値を読んだらもうフォトリフレクタは使わないので電源を切っておきます。その後、センサ値とスレッショルドを比較して「センサ値<スレッショルド」であれば洗濯機の蓋は閉じている(=洗濯物が残っている)ということで、lid_closedにジャンプします(「lid」は英語で「蓋」)。 洗濯機の蓋が開いていればジャンプはせず、各種変数を初期化してULPプログラムを停止(halt)します。Halt命令を実行するとULPコプロセッサが停止すると同時にULPウェイクアップタイマが始動し、1分後に`entry`の実行が始まります。 ```as:my_ulp.S lid_closed: move r1, lid_closed_counter ld r0, r1, 0 jumpr lid_closed_long, 30, ge add r0, r0, 1 st r0, r1, 0 halt lid_closed_long: // clear lid_closed_counter move r0, 0 st r0, r1, 0 // sensor_phase++ move r1, sensor_phase ld r0, r1, 0 add r0, r0, 1 st r0, r1, 0 // jump after second probe jumpr lid_closed_too_long, 2, ge halt lid_closed_too_long: wake // disable ULP timer WRITE_RTC_REG(RTC_CNTL_STATE0_REG, RTC_CNTL_ULP_CP_SLP_TIMER_EN_S, 1, 0) halt ``` 蓋が閉じていることを検知したときのプログラムを示します。 `lid_closed`は蓋が30分継続して閉じていることを検知します。その方法は、変数`lid_closed_counter`を1分に1回インクリメントし、30になったら`lid_closed_long`にジャンプする、というものです。単純ですね。 `lid_closed_long`は`lid_closed_counter`を0に初期化した後、`sensor_phase`をインクリメントします。つまり`sensor_phase`は30分に1回インクリメントされますから、蓋が閉まってから60分後、90分後、120分後、…を判断するのに使えます。`sensor_phase`が2以上の値であれば`lid_closed_too_long`にジャンプします。 `lid_closed_too_long`はメインコアを起動し、ULPウェイクアップタイマとULPコプロセッサ自身を止めます。この処理で起床したメインコアはLINEへ通知を送ります。 ## ハマりどころ 製作中にいくつかハマったところがありますのでご紹介します。 ### USBシリアル変換デバイスが権限不足で開けない ESP-IDFでESP32マイコンにプログラムを書き込むにはシリアル通信を用います。 ESP32マイコンをUSBケーブルでLinuxに接続すると、/dev/ttyUSB0というようなデバイス名で見えます。 ESP-IDFのドキュメントによると、次のコマンドでプログラムを書き込むようです。 idf.py -p /dev/ttyUSB0 flash ですが、初期状態では/dev/ttyUSB0が一般ユーザからアクセスできず、書き込めません。 エラーを解決するには `sudo idf.py -p ...` のようにsudoを付ける必要があります。 プログラムの書き込みごときでsudoコマンドを使いたくないですよね… これを解決する綺麗な手段は、ユーザをdialoutグループに追加することです。 /dev/ttyUSB0の所有グループがdialoutとなっているため、書き込みをしようとするユーザをdialoutグループに所属させようということです。 次のコマンドで、現在のユーザをdialoutグループに追加できます。 sudo usermod -a -G dialout $USER dialout:x:20:uchan 設定したら、一旦ログアウトし再度ログインすると設定が有効になります。あるいはPCを再起動するのでも大丈夫です。 その後はsudo無しで書き込めるようになっているはずです。 ### ULPプログラミングのハマりどころ 別記事でまとめました。 [ESP32のULPプログラミングのはまりポイント ](https://elchika.com/article/3fbb17bd-5dec-4409-a063-d0b77871603f/)