Zellij ターミナルワークスペースと WebAssembly プラグイン

自分は Linux をセットアップして最初に行う設定が tmux 関連だったりするのですが、新しい Windows 機の WSL2 にも秘伝の .tmux.conf を入れつつ、そうだ Zellij を試してみようと思い立ち、良い感じだったので紹介です。

Zellij

A terminal workspace with batteries included

Zellij (ぜりーじゅと読むそうです)は、いわゆるターミナルマルチプレクサーに種別されるソフトウェアで、セッション管理や、ターミナルエミュレータ上での画面分割やタブ制御を行うことができます。

Windows Terminal WSL2 上で動作している Zellij。

tmux よりも新しく作られていることもあり、デフォルトで自動起動スクリプトの出力やシステムクリップボード連携設定などが備わっており、また操作系も画面下部に表示することができるので非常に導入しやすいつくりとなっています。

導入

絶賛開発中 (記事執筆時点 0.36.0) の Rust でできたソフトウェアにて、導入は cargo install を使ってソースからビルドする方法が良いと思います。cargo なのでコマンド一発です。

Zellij User Guide – Installation

The easiest way to install Zellij is through a package for your OS.

If one is not available for your OS, you can download a prebuilt binary or even try Zellij without installing.

Otherwise, you can install it with Cargo.

(cargo を含む)Rust のツールチェインが未導入の場合は rustup で導入します。

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

導入後 PATH などが設定されますので、いったんシェルを落とし上げします。

cargo install で zellij を導入。

cargo install --locked zellij
$ ls -laF ~/.cargo/bin/ | grep zel
-rwxr-xr-x  1 hiromasa hiromasa 17380208  4月 20 17:31 zellij*

なお、cargo install で導入したバイナリーのアップデートは cargo-install-update を使うことで行うことができます。

# cargo-update の導入 (初回のみ)
cargo install cargo-install-update
# アップデート確認
cargo install-update -l
    Polling registry 'https://index.crates.io/'.....

Package         Installed  Latest   Needs update
espup           v0.2.9     v0.4.0   Yes
cargo-generate  v0.18.2    v0.18.2  No
cargo-update    v13.0.1    v13.0.1  No
powerline-rs    v0.2.0     v0.2.0   No
zellij          v0.36.0    v0.36.0  No
# Needs update が Yes なものをアップデート
cargo install-update -a

フォント

Zellij は標準で powerline フォントに対応しています。お使いのターミナルエミュレータのフォントに設定しましょう。自分は HackGen を使わせていただいています。(とてもきれいです…!)

https://github.com/yuru7/HackGen

白源 (はくげん/HackGen) は、プログラミング向け英文フォント Hack と、源ノ角ゴシックの派生フォント源柔ゴシックを合成したプログラミングフォントです。

設定フォントは「源ノ角ゴシックの派生フォント源柔ゴシックに、Powerline フォントを含む Nerd Fonts を合成した」 HackGen_NF_*.zipHackGenConsoleNF-Regular.ttfHackGen Console NF)を設定すると良いと思います。

なお、Windows Terminal だと Ricty 系の powerline フォントの罫線がずれるようです。

自動起動設定

zellij 起動確認後は、シェル設定ファイルに Zellij の自動起動設定を入れておくと便利です。

Zellij User Guide – Autostart on shell creation

Autostart a new zellij shell, if not already inside one. Shell dependent, fish:

自分は .bashrc ですので次のようになります。

echo 'eval "$(zellij setup --generate-auto-start bash)"' >> ~/.bashrc

ちなみに zellij setup --generate-auto-start bash の中身は次のようになっています。tmux だと自分で書かなければいけないので小粋な感じでぐー。

if [[ -z "$ZELLIJ" ]]; then
    if [[ "$ZELLIJ_AUTO_ATTACH" == "true" ]]; then
        zellij attach -c
    else
        zellij
    fi

    if [[ "$ZELLIJ_AUTO_EXIT" == "true" ]]; then
        exit
    fi
fi

というわけで、お好みで ZELLIJ_AUTO_ATTACH を設定すると、Windows Teminal などがダウンしたときにや誤ってタブを落とした時に自動復帰できて良いかと思います。

`.bashrc`

ZELLIJ_AUTO_ATTACH=true
eval "$(zellij setup --generate-auto-start bash)"

関連して Zellij の default key binding に Ctrl + s, s がバッファのサーチに割り当てられていて、自分はうっかりロックを忘れて tty に START/STOP を送ってしまうことがあったので、以下も一緒に入れて tty の制御コード受付を disable にしています。

if [[ -t 0 && $- = *i* ]]
then
    stty -ixon
fi
ZELLIJ_AUTO_ATTACH=true
eval "$(zellij setup --generate-auto-start bash)"

設定

Zellij はシングルバイナリーで動作するようになっていますが、設定ファイルを使うこともできます。デフォルト設定を最初に出力すると良いかと思います。

mkdir -p ~/.config/zellij/themes
mkdir -p ~/.config/zellij/layouts
mkdir -p ~/.config/zellij/plugins

Zellij User Guide – Configuration

Zellij uses KDL as its configuration language.

# 設定ファイル
zellij setup --dump-config > ~/.config/zellij/config.kdl

自分のデフォルト設定から変えている定義は次の通りです。ほとんど変えてないですねw

// ペインのフレームをボーダレスに
pane_frames false
// 起動時の操作モードをインターフェース locked に
default_mode "locked"
// 配色テーマ
themes {
    tokyo-night-dark {
        fg 169 177 214
        bg 26 27 38
        black 56 62 90
        red 249 51 87
        green 158 206 106
        yellow 224 175 104
        blue 122 162 247
        magenta 187 154 247
        cyan 42 195 222
        white 192 202 245
        orange 255 158 100
    }
}
theme "tokyo-night-dark"

テーマは次のドキュメントからいただいてきたものです。

Zellij User Guide – Themes

Themes can be specified either in the configuration file under the themes section, or directly in a separate file.

レイアウト

Zellij の肝のひとつですが、レイアウトファイルを使うことによりペインに好きなパーツを配置することができます。

# デフォルト画面レイアウトファイル
zellij setup --dump-layout default > ~/.config/zellij/layouts/default.kdl

pain の一番目に後述する自分の WebAssembly プラグインを追加していますが、ファイルの内容は次のようになります。

layout {
    pane size=1 borderless=true {
        plugin location="file:/home/hiromasa/.config/zellij/plugins/zellij-datetime.wasm"
    }
    pane size=1 borderless=true {
        plugin location="zellij:tab-bar"
    }
    pane
    pane size=2 borderless=true {
        plugin location="zellij:status-bar"
    }
}

この定義が起動時の画面レイアウトと対応する形のつくりになっています。タブバーや下部のステータスバーがデフォルトプラグインとして実装されているのが分かります。

レイアウトファイルの記述方法は以下のドキュメントが参考になります。

また Zellij 起動時の引数でレイアウトファイルを指定できるため、作業によってワークスペースを切り替えるようなことも可能です。

Zellij User Guide – Layouts

Layouts are text files that define an arrangement of Zellij panes and tabs.

You can read more about creating a layout

default_tab_template を使うと複数のタブで同じ設定を共有できます。

layout {
    tab name="t420s#1" focus=true
    tab name="t420s#2"
    default_tab_template {
        pane size=1 borderless=true {
            plugin location="file:/home/hiromasa/.config/zellij/plugins/zellij-datetime.wasm"
        }
        pane size=1 borderless=true {
            plugin location="zellij:tab-bar"
        }
        children
        pane size=2 borderless=true {
            plugin location="zellij:status-bar"
        }
    }
}

例えばですが、シングルボードコンピュータへの ssh 接続を行い、各種リソースを表示するようなレイアウトを作成した例 です。(後述していますがこの例で使っている RISC-V Linux な SBC では直接 Zellij が動作しないため、ホスト側から ssh で接続しています。RPi 4A など Arm 機ではリモート側で Zellij を起動できるでしょう)

layout {
    tab name="LPi4A" focus=true {
        pane split_direction="vertical" {
            pane size="60%" close_on_exit=true {
                command "sshpass"
                args "-p" "licheepi" "ssh" "sipeed@172.16.15.201"
            }
            pane size="40%" {
                pane size="25%" close_on_exit=true {
                    command "sshpass"
                    args "-p" "licheepi" "ssh" "sipeed@172.16.15.201" "-t" "watch" "-n5" "df" "-k"
                }
                pane size="16%" close_on_exit=true {
                    command "sshpass"
                    args "-p" "licheepi" "ssh" "sipeed@172.16.15.201" "-t" "watch" "sudo" "cat" "/sys/devices/system/cpu/cpu*/cpufreq/cpuinfo_cur_freq"
                }
                pane size="16%" close_on_exit=true {
                    command "sshpass"
                    args "-p" "licheepi" "ssh" "sipeed@172.16.15.201" "-t" "watch" "sensors"
                }
                pane size="43%" close_on_exit=true {
                    command "sshpass"
                    args "-p" "licheepi" "ssh" "sipeed@172.16.15.201" "-t" "htop"
                }
            }
        }
    }
    tab name="Host"
    default_tab_template {
        pane size=1 borderless=true {
            plugin location="zellij:tab-bar"
        }
        children
        pane size=2 borderless=true {
            plugin location="zellij:status-bar"
        }
    }
}

Ubuntu/Alactirry 上の動作:

Windows Terminal/WSL2 Ubuntu で動作させている様子(WSL2 Ubuntu 上の zellij-server が接続を維持してくれるのでターミナルを落としてしまっても復帰できる例):

キーバインドから特定のコマンドをペインで起動

例えば F12 押下で Python REPL を起動したいなんて時は config.kbl で次のようにします。

keybinds {
    normal {
        // uncomment this and adjust key if using copy_on_select=false
        // bind "Alt c" { Copy; }
        bind "F12" {
            Run "python" "-i" "/home/hiromasa/.local/share/micropython/imports.py" {
                cwd "/home/hiromasa"
                direction "Down"
            }
        }
    }

ワンボタンで計算機起動的。とても便利です。

ペイン内の操作シンク

クラスタ構成の同一構成複数サーバとかで同じコマンドを入力したい時につかうあれも OK です。tab 操作から sync を選択することで、ペイン全てに同じ操作ができます。

ペインとタブ間の Break 操作

Zellij 0.38 よりペインをタブに移動させたり、その逆ができるようになりました。便利です。

WebAssembly プラグイン拡張

Zellij プラグインは WebAssembly/WASI で作成することができます。勉強がてら 日時 date-time を表示するプラグインを Rust でかいてみました。

https://github.com/h1romas4/zellij-datetime

This plugin adds a date and time pane to [Zellij](https://zellij.dev/), a terminal multiplexer.

amd64 Ubuntu/Alacritty で動作している様子。

なお、システムインターフェースは WASI API の範囲となります。とりあえずまだ TIMEZONE がとれない気がした(たぶん)ので +9 JST 固定でビルドしました

(2023-06-21 追記)定義ファイルとマウスクリックでタイムゾーンを選択できるように改良しました。

WASI API 超えをしたい場合のアイディアメモ:(ホスト側で情報を取得するシェルスクリプトを動かして、結果をファイルかソケットでプロキシしていると思います)

https://github.com/yvt/zellij-cpulamp

Minimal CPU activity indicator plugin for Zellij

ファイルシステムへのセキュリティーポリシーなどがどうなっているかなど継続調査。WebAssembly ランタイムは Wasmer (執筆時点 2.3 系) が使われています。

https://zellij.dev/documentation/plugin-api-file-system.html

Plugin API – Reading from the Filesystem

(2023-06-19 追記) Zellij 0.37 からプラグインから Worker スレッドが作成できるようになりました…!

https://zellij.dev/documentation/plugin-api-workers.html

Plugin workers are a way to get around the fact that wasm/wasi threads are not stable yet. If a plugin has a potentially long operation to perform, it can declare a worker on startup and send and receive messages from it.

Wasm を使っているため .wasm ファイルは amd64 や Arm といった CPU の違いを気にせず同一バイナリーが使えますので便利です。 .wasm を配置してレイアウトファイルで読み込ませれば動作を開始します。(Wasmer は 3.2 から RISC-V 対応なので残念ながら現時点では RISC-V 機では動作しません)

今回は Rust を使いましたが、言語を選ばずプラグインがかけるのも良いです。

なお、まだ Rust と Zig 言語以外にはバインディングのリリースはなく、そのほかの言語では、引数をもらうのに WASI の stdin から JSON をパースする処理をかく必要がありそうです。Rust のバインディングも内部的にはこの構成になっています。この辺は、WebAssembly の Component Model (Interface Types) の仕様ができれば改善されることでしょう。

(2023-06-19 追記)Zellij は開発中のためプラグインの ABI (API) zellij-tile が現在のところまだ確定していません。このため Zellij の update が合った際は、Rust wasm32-wasi なプラグインは該当バージョンに合わせて dependencies を修正して .wasm ビルドしなおす必要があります。(これは近い将来に修正されると思われます)

(2023-08-28 追記)Zellij 0.38 で Wasm plugin API が protocol buffer 化され、下位互換が保たれるようになりました。API チェンジがない限り Zellij のバージョンが上がってもそれよりも下のバージョンでコンパイルされたプラグインが利用可能です。

https://zellij.dev/documentation/plugin-upgrading.html

Upgrading a Plugin

Since Zellij plugins using zellij-tile rely on shared data structures, currently one would need to compile a plugin against the corresponding zellij-tile package of the zellij version it is installed on.

The Zellij maintainers are aware this is an inconvenient and undesired scenario and are working on a more robust solution.

zellij-datetime リポジトリはその時点の最新版でリリースしていきます。もし筆者が Zellij のアップデートに気がついていなかったら、以下の部分のバージョンを Zellij コアのそれと合わせるように修正ビルドして .wasm を取得してみてください。

$ cat Cargo.toml | grep -A 2 dependencies
[dependencies]
zellij-tile = "^0.37"
zellij-tile-utils = "^0.37"

size=1 のペインを縦並びにするハック

デフォルトのペインの罫線表示を pane_frames false (非表示)とした場合、size=1 のペインを vertical で配置すると余計なボーダーが表示されてしまうのを、pane_frames trueとした上で、次のように個別に pane borderless=true として回避。

layout {
    tab name="thinkpad-t420s" focus=true {
        pane borderless=true
    }
    default_tab_template {
        pane size=1 split_direction="vertical" {
            pane size="75%" borderless=true {
                plugin location="zellij:tab-bar"
            }
            pane size="25%" borderless=true {
                plugin location="file:/home/hiromasa/.config/zellij/plugins/zellij-datetime.wasm" {
                    // Test multiple time zone definitions and default time zones.
                    timezone1 "PDT/-9"
                    timezone2 "UTC/0"
                    timezone3 "CEST/+2"
                    timezone4 "JST/+9"
                    default_timezone "JST"
                    // Test color settings.
                    background_color "#0080a0"
                    foreground_color "#ffffff"
                    pane_color "#383e5a"
                }
            }
        }
        children
        pane size=2 borderless=true {
            plugin location="zellij:status-bar" {
                supermode true
            }
        }
    }
}

次のスクショの 1行目のように、タブバーと時計を横並びに…などできる。(ただしウインドウサイズを変えるとボーダーがでてきたり、現在のところ少しバギー)

その他

もし Zellij が panic で起動しない場合はログを確認するのと、よく分からなければ次のテンポラリーファイルを削除して再起動してみます。(マルチユーザ使用の場合は気を付けて)

$ rm -Rf ~/.cache/zellij/*
$ rm -Rf /tmp/zellij*

Zellij 起動中に新しい Zellij を cargo install-update するとセッションがアップグレードできずに起動しない場合があります。 rm -Rf ~/.cache/zellij/* で修正できるはずです。

https://github.com/zellij-org/zellij/issues/2553

zellij does not start anymore #2553

ERROR  |zellij_utils::errors::not| 2023-06-17 18:54:54.876 [async-std/runti] [/home/amtoine/.local/share/cargo/registry/src/github.com-1ecc6299db9ec823/zellij-utils-0.36.0/src/errors.rs:585]: Panic occured:
             thread: async-std/runtime
             location: At /home/amtoine/.local/share/cargo/registry/src/github.com-1ecc6299db9ec823/rkyv-0.7.42/src/impls/core/mod.rs:267:17
             message: assertion failed: !result.is_null() 

おわりに

今回 Zellij のプラグインをかいていたら、昔よく VZ Editor のマクロをかいていたことを思い出しました。。Zellij 拡張も テトリスやスネークゲームなどもつくれそうな雰囲気です。

これだけ拡張性が高いと心強いですね。良ければお試しください。

関連

M5Stack RCA Module の I2S PCM5102A と Rust ESP32 xtensa-esp32-espidf ビルド

M5Stack RCA Module に搭載されている I2S (PCM5102APWR) を、Rust で生成した PCM 波形で発音させたメモです。

波形の生成は libymfm.wasm として WebAssembly 向けに作成している Rust 製のシーケンサーとサウンドチップエミュレーションをそのまま esp-idf に持ってきて xtensa-esp32-espidf ビルドしています。また、libymfm.wasm は C++ でつくられた FM 音源エミュレータの ymfm もリンクしています。

ESP32 Xtensa の Rust は初挑戦でしたが、確認した範囲で問題なくすんなり動作しました。ESP32 Xtensa Rust ツールチェインの準備やビルドは大変な印象がありましたが、昨今は esp-rs の各プロジェクトを組み合わせることで簡単に設定できるようになっています。

ちなみに ESP32-S3 開発ボードでも軽く動かしてみましたが問題なさそうです。ESP-S3 では PSRAM 80MHz Octa 設定にしているのが効いたか、波形生成は 1.4 倍程度高速でした。(Rust モジュールの細かなメモリ配置については未調査で課題としています)

ということで、この記事には以下の内容が含まれます。

  • I2S PCM5102APWR のイニシャライズ方法
  • ESP32 Xtensa Rust のビルド
実は M5Stack のスタックするモジュールを初めて買ったので、ボトムがないと寂しいことになると知らなかったのは内緒です。でも手軽に接続できてかっこいいです!

ソースコードは以下のリポジトリから見ることができます。詳しくはソースを見ていただくのが早いかもしれません。

https://github.com/h1romas4/m5stack-chipstream

This is a test to port C++’s ymfm and Rust’s vgmplay to ESP32(Xtensa).

Rust でつくられた VGM パーサーと SEGAPCM エミュレーションで I2S を発音させてるデモ:(残念ながら今回のビルドでは、ymfm の FM 音源エミュレーションは ESP32 で処理速度が間に合いませんでした

I2S PCM5102APWR のイニシャライズ

M5Stack Core2 に接続した RCA Module の PCM5102A の i2s_config と pin_config は以下のように設定すると良いようです。

// i2s_driver_install
i2s_config_t i2s_config = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
    .sample_rate = sample_rate,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
    .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
    .communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_STAND_I2S),
    .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count = dma_buf_count,
    .dma_buf_len = dma_buf_len,
    .use_apll = false,
    .tx_desc_auto_clear = true,
    .fixed_mclk = I2S_PIN_NO_CHANGE
};
ESP_ERROR_CHECK(i2s_driver_install(I2S_NUM_1, &i2s_config, 0, NULL));

// i2s_set_pin
i2s_pin_config_t i2s_pin_config = {
    .mck_io_num = GPIO_NUM_0,
    .bck_io_num = GPIO_NUM_19,
    .ws_io_num = GPIO_NUM_0,
    .data_out_num = GPIO_NUM_2,
    .data_in_num = I2S_PIN_NO_CHANGE
};
ESP_ERROR_CHECK(i2s_set_pin(I2S_NUM_1, &i2s_pin_config));

communication_format は必ず I2S_COMM_FORMAT_STAND_I2S を設定のこと。

うっかり I2S_COMM_FORMAT_STAND_MSB を設定するとなんとなく発音するものの、送信する PCM の先頭 1bit が無視されるような動きになってややはまり。(-32768, 32767 のフルスイング矩形波が正しく発音しなくて気が付きました…)

bits_per_sampleI2S_BITS_PER_SAMPLE_16BITchannel_formatI2S_CHANNEL_FMT_RIGHT_LEFT することで、int16_t のステレオ PCM 形式になります。

DMA バッファは、波形生成側のプリレンダのバッファリングに合わせて dma_buf_len を 1024 で dma_buf_count を 32個設定して動作させています。一度に扱うサンプル数としては int16_t ステレオにて 256 です。

今回のオーディオ系の実装ですが、波形生成を ESP32 の core 0 で、I2S への PCM の送信を core 1 と FreeRTOS タスクを分割して実行しています。なお、i2s_write にはほとんど時間がかからないようですので、波形生成は core 1 に持ってきても影響ないかもしれません。

タスク間の通信には、esp-idf の FreeRTOS Additions になっている RingBuffer の Byte buffers を介して PCM データの送受信を行っています。

FreeRTOS AdditionsRing Buffers

Byte buffers do not store data as separate items. All data is stored as a sequence of bytes, and any number of bytes can be sent or retrieved each time. Use byte buffers when separate items do not need to be maintained (e.g. a byte stream).

ESP32 Xtensa Rust のビルド方法

Xtensa の Rust ツールチェインは espup で導入するのが簡単でした。Windows の場合は WSL2 の Ubuntu 22.04 を使うと便利かもです。(sysroot は esp-idf 4.4.3 を使っています)

詳しくはリポジトリに GitHub Actions のビルドを入れていますの参考にしてください。

https://github.com/esp-rs/espup

Tool for installing and maintaining Espressif Rust ecosystem.

espup で Rust を導入すると ~/.rustup/toolchains/esp に Xtensa の Rust が導入されます。また .espressif/tools/xtensa-esp32-elf-clang に clang が入ります。

$ ls -laF ~/.rustup/toolchains/esp/bin/
合計 58120
drwxr-xr-x 2 hiromasa hiromasa     4096  2月 19 15:56 ./
drwxr-xr-x 7 hiromasa hiromasa     4096  2月 19 15:56 ../
-rwxr-xr-x 1 hiromasa hiromasa 21897920  2月 19 15:56 cargo*
-rwxr-xr-x 1 hiromasa hiromasa   954888  2月 19 15:56 cargo-clippy*
-rwxr-xr-x 1 hiromasa hiromasa  1796976  2月 19 15:56 cargo-fmt*
-rwxr-xr-x 1 hiromasa hiromasa 11781088  2月 19 15:56 clippy-driver*
-rwxr-xr-x 1 hiromasa hiromasa      759  2月 19 15:56 rust-gdb*
-rwxr-xr-x 1 hiromasa hiromasa     1933  2月 19 15:56 rust-gdbgui*
-rwxr-xr-x 1 hiromasa hiromasa     1072  2月 19 15:56 rust-lldb*
-rwxr-xr-x 1 hiromasa hiromasa    17264  2月 19 15:56 rustc*
-rwxr-xr-x 1 hiromasa hiromasa 11124040  2月 19 15:56 rustdoc*
-rwxr-xr-x 1 hiromasa hiromasa 11906968  2月 19 15:56 rustfmt*
$ ls -laF ~/.espressif/tools/xtensa-esp32-elf-clang/esp-15.0.0-20221201-x86_64-unknown-linux-gnu/esp-clang
合計 44
drwxr-xr-x 11 hiromasa hiromasa 4096  2月 19 15:57 ./
drwxr-xr-x  3 hiromasa hiromasa 4096  2月 19 15:57 ../
drwxr-xr-x  2 hiromasa hiromasa 4096  2月 19 15:57 bin/
drwxr-xr-x  4 hiromasa hiromasa 4096  2月 19 15:57 include/
drwxr-xr-x  8 hiromasa hiromasa 4096  2月 19 15:57 lib/
drwxr-xr-x  2 hiromasa hiromasa 4096  2月 19 15:57 libexec/
drwxr-xr-x  5 hiromasa hiromasa 4096  2月 19 15:57 riscv32-esp-elf/
drwxr-xr-x  9 hiromasa hiromasa 4096  2月 19 15:57 share/
drwxr-xr-x  5 hiromasa hiromasa 4096  2月 19 15:57 xtensa-esp32-elf/
drwxr-xr-x  5 hiromasa hiromasa 4096  2月 19 15:57 xtensa-esp32s2-elf/
drwxr-xr-x  5 hiromasa hiromasa 4096  2月 19 15:57 xtensa-esp32s3-elf/

ちなみに espup でツールチェインを入れると、esp-idf とは別のディレクトリ名に同バージョンの gcc が入るようです。(?)

さて、今回は Rust は波形生成をするライブラリの形で、C/C++ の Arduino Loop をメインとしたかったので、”esp-idf first” という構成としています。components 配下に Rust のプロジェクトをおいて、いつも通り idf.py build すると Rust ごとビルドしてくれます。

cargo generate で以下のテンプレートを cmake 指定してビルドスクリプトやディレクトリストラクチャーをつくっています。(引数を cargo にすると Rust 中心の構成になります)

https://github.com/esp-rs/esp-idf-template

cargo generate https://github.com/esp-rs/esp-idf-template cmake

esp-idf first のビルドでは components の下にいつも通りコンポーネントを配置し、CMakeLists.txt から external project として Rust を追加してビルドする動作するようにつくってくれます。

ビルドさえできてしまえば、あとは std 環境の Rust で、今回は外部ライブラリとしてバイナリーパーサ nom や JSON シリアライズ serde などを入れていますが、そのまま動作しました。

一点、Rust から返した *const i16 (16bit) の RAW ポインターを C 側から読むとメモリーの状態によって 1 byte (?) ズレるような動作がありました。いったん C からポインターを渡して Rust 側から書き込むことで修正していますが、Xtensa 特有なのか自分のミスなのかまだ詳しく原因を調査できていません。(Rust 構造体内の配列ポインターなのですがアライメント関連?)

というわけで、ESP32 で Rust std が呼べる。とても便利す。

Rust の特定モジュールを IRAM に載せたり、ヒープアロケータの SRAM/PSRAM コントロール(できる?)など未調査の部分もありますので、引き続きやってみたいと思います。

関連

Windows WSL2 の Ubuntu 22.04 上から USB-UART 経由で M5Stack に書き込みする

Windows WSL2 の 1.0 版以降のカーネルでは、usbipd を経由することで Windows 側の USB 機器を IP 経由で参照することができます。

この動きを利用して、USB に接続した マイコン(M5Stack/USB-UART等々) に WSL2 Ubuntu 上のツールチェインから書き込みする手順を記載してみます。数コマンド叩くだけでセットアップは完了します。便利。

書き込み処理(libusb) は Linux 側で動作しますので、Windows 側にUSB-UART ドライバーなどのセットアップは不要です。

本記事では WSL2 を使った書き込み実用例として以下の手順を記載しています。

  • esp-idf の idf.py コマンドを使った M5Stack Core2 への書き込み
  • PlatformIO を使った書き込み例
  • openocd を使った M5Stamp C3 のデバッグ構成
  • LicheePi 4A への fastboot コマンドを使ったファームウェア書き込み

環境

Windows WSL2 (Windows 10 の場合 1.0 以降)

> wsl --version
WSL バージョン: 1.0.3.0
カーネル バージョン: 5.15.79.1
WSLg バージョン: 1.0.47
MSRDC バージョン: 1.2.3575
Direct3D バージョン: 1.606.4
DXCore バージョン: 10.0.25131.1002-220531-1700.rs-onecore-base2-hyp
Windowsバージョン: 10.0.22621.1105

WSL2 Ubuntu 22.04

$ uname -a
Linux minis-um690 5.15.79.1-microsoft-standard-WSL2 #1 SMP Wed Nov 23 01:01:46 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux

Windows に usbipd-win を導入

基本的に以下の Microsoft のガイドに従うと OK ですが、一部コマンドがカーネルバージョンに依存しているので、usbipd-win の手順も参照しています。

USB デバイスを接続する

このガイドでは、USB/IP オープンソース プロジェクト usbipd-win を使用して、WSL 2 で実行されている Linux ディストリビューションに USB デバイスを接続するために必要な手順について説明します。

まずは Windows 側に usbipd-win を導入します。 PowerShell 窓より以下のコマンドにて。

> winget install --interactive --exact dorssel.usbipd-win

UPDATE – 2023-12 usbipd 4.0 リリースに伴い以下の Ubuntu 側へのクライアントツールのインストールは不要になりました。

次に WSL2 Ubuntu 22.04 に apt で usbipd を導入します。

$ sudo apt update
$ sudo apt upgrade
$ sudo apt install linux-tools-virtual hwdata

usbipd を登録。

$ sudo update-alternatives --install /usr/local/bin/usbip usbip `ls /usr/lib/linux-tools/*/usbip | tail -n1` 20

さて、この流れでついでに Ubuntu の udev を構成して、マイコンの USB 接続をユーザ権限から見れるようにしておきます。 PlatformIO の udev 定義を使うとほとんどのマイコンで使えて便利です。

$ curl -fsSL https://raw.githubusercontent.com/platformio/platformio-core/develop/platformio/assets/system/99-platformio-udev.rules | sudo tee /etc/udev/rules.d/99-platformio-udev.rules

これで WSL2 側から接続された USB を認識させる準備は完了です。手順上、いったん状態をクリアするため Windows ごと再起動します。

USB 機器のバインド

Windows 起動後、先に Ubuntu の usbipd を起動するために、WSL2 Ubuntu 窓を上げておきます。

また、WSL2 では udev サービスが自動起動しませんので、WSL2 起動後に以下のコマンドで起動します。(wsl.conf などで自動起動設定するのもいいかもしれません)

$ sudo service udev restart

次に Windows の PowerShell 窓を「管理者で起動」し、使いたい USB 機器を usbipdにアタッチします。

usbipd wsl list コマンドで USB 機器を表示。

4.0 系:

> usbipd list

3 系:

> usbipd wsl list
BUSID  VID:PID    DEVICE                                                        STATE
3-1    17ef:6047  Lenovo USB Interface Device(HID), USB 入力デバイス            Not attached
3-3    0e8d:0608  RZ608 Bluetooth(R) Adapter                                    Not attached
7-1    0499:170d  AG06/AG03, Line (AG06/AG03)                                   Not attached
9-1    10c4:ea60  CP2104 USB to UART Bridge Controller                          Not attached

ここでは、CP2104 USB to UART が M5Stack なので 9-1 をアタッチ。なお、初回のアタッチの操作のみ管理者権限が必要で次回からはユーザ権限でいけるようです。

4.0 系:

> usbipd attach --wsl --busid 9-1

3 系:

> usbipd wsl attach --busid 9-1

3 系:

この操作で以下のエラーが出力された場合は、再度 Ubuntu 側で sudo update-alternatives --install を実行してください。(Ubuntu カーネル(tools の)バージョンが上がった場合に発生します)

❯ usbipd.exe wsl attach --busid 9-3
usbipd: error: WSL 'usbip' client not correctly installed. See https://github.com/dorssel/usbipd-win/wiki/WSL-support for the latest instructions.

linux-tools がアップデートされた例(ここでは Kernel 5.15.0-70 から 5.15.0.-71)された後の update-alternatives 設定ログ:

$ sudo update-alternatives --install /usr/local/bin/usbip usbip `ls /usr/lib/linux-tools/*/usbip | tail -n1` 20
update-alternatives: 警告: /usr/lib/linux-tools/5.15.0-70-generic/usbip の alternative (リンクグループ usbip のパート) が存在しません 。alternatives のリストから削除しています
update-alternatives: 警告: /etc/alternatives/usbip is dangling; it will be updated with best choice
update-alternatives: /usr/local/bin/usbip (usbip) を提供するために自動モードで /usr/lib/linux-tools/5.15.0-71-generic/usbip を使います

ちなみにユーザ権限がついたあとは、Ubuntu 側 WSL2 窓から次のコマンドでも操作可能です。(.exe を付けて明示的に Windows 側のコマンドを呼びます)

4.0 系:

$ usbipd.exe attach --wsl --busid 9-1

3 系:

$ usbipd.exe wsl attach --busid 9-1

うまくいけば、upsipd wsl list コマンドのデバイス部分が Ubuntu になります。

4.0 系:

> usbipd list

3 系:

> usbipd wsl list
BUSID  VID:PID    DEVICE                                                        STATE
9-1    10c4:ea60  CP2104 USB to UART Bridge Controller                          Attached - Ubuntu-22.04

できたら、WSL2 Ubuntu で lsusb してみて機器(CP210)が見えればOKです。

$ lsusb
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 004: ID 10c4:ea60 Silicon Labs CP210x UART Bridge
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

PlatformIO からお借りした udev 定義が効いていれば、USB-UART では /dev/ttyUSB0 あたりにユーザ権限の書き込み付きでブロックデバイスが見えます。rw-rw-rw なら成功です。

$ ls -laF /dev/ttyUSB*
crw-rw-rw- 1 root dialout 188, 0  1月 28 17:17 /dev/ttyUSB0

マイコンが暴走して USB を抜き差しした後などで、usbipd.exe で再アタッチするも udev が効かないケースがあるようです。その場合は面倒なので手動で chmod するのもありかと思います。

$ sudo chmod 666 /dev/ttyUSB0

esp-idf の idf.py コマンドを使った M5Stack Core2 への書き込み

Ubuntu 22.04 上に esp-idf を構成して idf.py コマンドで書き込む例です。なお、esp-idf ツールチェインが必要とする Ubuntu の依存関係はこちらに記載されています。

$ get_idf
$ echo ${IDF_PATH}
/home/hiromasa/devel/toolchain/esp-idf
$ idf.py --version
ESP-IDF v4.4.3

esp-idf (esp-tools) は自動的にシリアルポートを探してくれますので、以下のいつもの idf.py flash monitor コマンドで書き込みからログ入力まで動作します。

$ idf.py build flash monitor
Executing action: all (aliases: build)
Running ninja in directory /home/hiromasa/devel/esp32/m5stack-core2-wasm3-as/build
Executing "ninja all"...
[1/4] cd /home/hiromasa/devel/esp32/m5stack-core2-wasm3-as/b...asa/devel/esp32/m5stack-core2-wasm3-as/build/hello_world.bin
hello_world.bin binary size 0x124cb0 bytes. Smallest app partition is 0x200000 bytes. 0xdb350 bytes (43%) free.
[2/4] Performing build step for 'bootloader'
[1/1] cd /home/hiromasa/devel/esp32/m5stack-core2-wasm3-as/build/bootloader/esp-idf/esptool_py && /home/hiromasa/.espressif/python_env/idf4.4_py3.10_env/bin/python /home/hiromasa/devel/toolchain/esp-idf/components/partition_table/check_sizes.py --offset 0x8000 bootloader 0x1000 /home/hiromasa/devel/esp32/m5stack-core2-wasm3-as/build/bootloader/bootloader.bin
Bootloader binary size 0x6330 bytes. 0xcd0 bytes (11%) free.
Executing action: flash
Serial port /dev/ttyUSB0
Connecting....
Detecting chip type... Unsupported detection protocol, switching and trying again...
Connecting.....
Detecting chip type... ESP32
...以下略...

本環境設定と合わせて、プロジェクトディレクトリを VSCode の WSL Remote 拡張で開けば、ソースコードの編集からマイコンへの書き込みまで Linux 周りのツールで揃えることができるので便利なのではないかと思います。

WSL2 からのマイコンへの書き込みの操作は以上です。usbipd 経由で接続された USB が WSL2 側から認識できれば、他は特殊な操作は不要で書き込みすることができます。


VS Code の WSL Remote 拡張は、先に Windows 側の VS Code で拡張を導入した上で一度終了。 WSL2 側のターミナルでプロジェクトディレクトリにカレントディレクトリを cd で移し、

$ code .

とすると、外見は Windows で中身は Linux の VS Code が起動します。初回起動、及びアップデートがあった場合のみ、Linux 版の VS Code backend (vscode-server) をダウンロードするため少しだけ時間がかかります。

起動したら、その他で Linux 側に必要な拡張がある場合(PlatformIO 等)は、拡張を入れる画面から「WSL2 に入れる」的なボタンを押してあげてください。


PlatformIO を使った書き込み例

Longan Nano (GD32V) + VSCode + PlatformIO の例です。

Longan Nano dfu 用の udev 構成:

$ cat /etc/udev/rules.d/70-ttyusb.rules #ファイル追加
# Longan Nano
SUBSYSTEM=="usb", ATTRS{idVendor}=="28e9", ATTRS{idProduct}=="0189", MODE="0666"
SUBSYSTEM=="usb_device", ATTRS{idVendor}=="28e9", ATTRS{idProduct}=="0189", MODE="0666"
$ sudo service udev restart # udev restart

Longan Nano を BOOT + RESET 起動で dfu モードに入れて attach。

> usbipd wsl list
BUSID  VID:PID    DEVICE                                                        STATE
3-1    17ef:6047  Lenovo USB Interface Device(HID), USB 入力デバイス            Not attached
3-3    0e8d:0608  RZ608 Bluetooth(R) Adapter                                    Not attached
7-1    0499:170d  AG06/AG03, Line (AG06/AG03)                                   Not attached
9-1    28e9:0189                                                                Not attached

先に管理者権限 PowerShell でアタッチした後、リセットするとアタッチが切れるので、再アタッチ。 WSL2 Ubuntu 側からも Windows の usbipd.exe を呼べる動作を活用してアップロード操作前にアタッチコマンドを実行できるようにタスクを組むと便利。

$ usbipd.exe wsl attach --busid 9-1
$ lsusb
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 004: ID 28e9:0189 GDMicroelectronics GD32 DFU Bootloader (Longan Nano)
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

あとは WSL2 側に入れた VSCode と PlatformIO 拡張から、いつも通り Upload を起動すれば書き込みできます。

openocd デバッグを行う場合

M5Stamp C3 の USB JTAG デバッグを行う設定例です。

M5Stamp C3 は 「M5Stamp C3 用の開発向けボードを製作」のように USB JTAG を引き出すことができます。また、M5Stamp C3U では付属の USB ポートでいけると思います。

openocd の依存となる libusb の導入(esp-idf の依存で導入されているかもですが念の為。どちらかでだけでもいいかものと、-dev を入れているのは癖なので外しても OK です)

$ sudo apt install libusb-dev libusb-1.0-0-dev

openocd の依存となる libpython2.7 導入

$ sudo apt install libpython2.7

USB ポートに M5Stamp C3 の JTAG USB 側(?) を引き出して接続、Windows 側でアタッチ後 Linux で USB 認識確認

$ lsusb | grep JTAG
Bus 001 Device 007: ID 303a:1001 Espressif USB JTAG/serial debug unit

udev 構成追加( 303a の 追加)

$ cat /etc/udev/rules.d/99-platformio-udev.rules | grep 303a
# 以下を追加
ATTRS{idVendor}=="303a", ATTRS{idProduct}=="1001", MODE="0666", ENV{ID_MM_DEVICE_IGNORE}="1", ENV{ID_MM_PORT_IGNORE}="1"

udev restart

$ sudo service udev restart

接続確認

いったん USB を抜き差しして、Windows 側で再アタッチなどして、接続確認します。

$ ls -laF /dev/ttyACM*
crw-rw-rw- 1 root dialout 166, 0  1月 28 19:46 /dev/ttyACM0

openocd 起動

$ which openocd
/home/hiromasa/.espressif/tools/openocd-esp32/v0.11.0-esp32-20221026/openocd-esp32/bin/openocd
$ openocd -f board/esp32c3-builtin.cfg
Open On-Chip Debugger  v0.11.0-esp32-20220706 (2022-07-06-15:48)
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
Info : only one transport option; autoselect 'jtag'
Info : esp_usb_jtag: VID set to 0x303a and PID to 0x1001
Info : esp_usb_jtag: capabilities descriptor set to 0x2000
Warn : Transport "jtag" was already selected
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : esp_usb_jtag: serial (34:B4:72:12:94:14)
Info : esp_usb_jtag: Device found. Base speed 40000KHz, div range 1 to 255
Info : clock speed 40000 kHz
Info : JTAG tap: esp32c3.cpu tap/device found: 0x00005c25 (mfg: 0x612 (Espressif Systems), part: 0x0005, ver: 0x0)
Info : datacount=2 progbufsize=16
Info : Examined RISC-V core; found 1 harts
Info :  hart 0: XLEN=32, misa=0x40101104
Info : starting gdb server for esp32c3 on 3333
Info : Listening on port 3333 for gdb connections

VSCode などから接続(設定例

おそらく openocd 接続後初回の Debug Launch は(マイコンの状態により)失敗するので、2回 Debug Launch するとブレイクすると思います。(WSL2 ではない Ubuntu でも再現するので自分の setupCommandsが悪いのかもしれません)

Lichee Pi 4A の fastboot による Flash

Lichee Pi 4A の boot ボタンを押しながら PC の USB-C に接続。

> usbipd.exe wsl list
BUSID  VID:PID    DEVICE                                                        STATE
6-1    2345:7654  USB download gadget                                           

初回の場合は PowerShell 管理者で実行。

> usbipd.exe wsl attach --busid 6-1

WSL2 側で認識確認。

$ sudo dmesg
[   31.713030] usb 1-1: new high-speed USB device number 2 using vhci_hcd
[   31.873057] usb 1-1: SetAddress Request (2) to port 0
[   31.920340] usb 1-1: New USB device found, idVendor=2345, idProduct=7654, bcdDevice= 2.23
[   31.921254] usb 1-1: New USB device strings: Mfr=1, Product=2, SerialNumber=0
[   31.921580] usb 1-1: Product: USB download gadget
[   31.921790] usb 1-1: Manufacturer: T-HEAD
[   49.129386] hv_balloon: Max. dynamic memory size: 14204 MB

あとは fastboot コマンドで書き込めば OK です。

$ pwd
/home/hiromasa/devel/riscv/lip4a/images/0511_gnome
$ ls -laF
合計 4259020
drwxrwxr-x 2 hiromasa hiromasa       4096  5月 12 12:04 ./
drwxr-xr-x 3 hiromasa hiromasa       4096  6月  2 17:24 ../
-rw-rw-r-- 1 hiromasa hiromasa   62914560  5月 12 11:35 boot-20230510-230240.ext4
-rwxrwxrwx 1 hiromasa hiromasa        368  5月 12 11:29 burn_gnome_gpu.sh*
-rwxrwxrwx 1 hiromasa hiromasa    2230360  5月  5 23:47 fastboot*
-rw-rw-r-- 1 hiromasa hiromasa       2022  5月 12 11:38 image_intro.txt
-rw-r--r-- 1 hiromasa hiromasa      68443  5月 12 11:36 light-lpi4a_1.85GHz.dtb
-rw-r--r-- 1 hiromasa hiromasa      68464  5月 12 11:36 light-lpi4a_2Ghz.dtb
-rw-rw-r-- 1 hiromasa hiromasa        259  5月 12 11:21 md5sum.txt
-rw-rw-r-- 1 hiromasa hiromasa 4294967296  5月 12 11:32 rootfs-20230511-183752-gnome.ext4
-rw-rw-r-- 1 hiromasa hiromasa     957728  5月 12 11:21 u-boot-with-spl-lpi4a-20230510.bin
$ file fastboot
fastboot: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, not stripped

なのですが、一度 uboot を書く前のファームウェア書き込み後に一度リセットがかかり <waiting device..> となりますので、ここのタイミングで再度 PowerShell 側で usbipd.exe wsl attach --busid 6-1 すれば完走できます。

$ ./burn_gnome_gpu.sh
Sending 'ram' (935 KB)                             OKAY [  0.248s]
Writing 'ram'                                      OKAY [  0.002s]
Finished. Total time: 0.256s
Rebooting                                          OKAY [  0.001s]
Finished. Total time: 0.202s
< waiting for any device > # ←ここで再度 wsl attach する
Sending 'uboot' (935 KB)                           OKAY [  0.058s]
Writing 'uboot'                                    OKAY [  0.031s]
Finished. Total time: 0.113s
Sending 'boot' (61440 KB)                          OKAY [  2.568s]
Writing 'boot'                                     OKAY [  1.535s]
Finished. Total time: 4.241s
Invalid sparse file format at header magic
Sending sparse 'root' 1/33 (113316 KB)             OKAY [  4.680s]
Writing 'root'                                     OKAY [  2.791s]
Sending sparse 'root' 2/33 (110992 KB)             OKAY [  4.660s]
Writing 'root'                                     OKAY [  2.568s]
...(snip)...
Writing 'root'                                     OKAY [  2.891s]
Sending sparse 'root' 32/33 (114684 KB)            OKAY [  4.709s]
Writing 'root'                                     OKAY [  2.617s]
Sending sparse 'root' 33/33 (9492 KB)              OKAY [  0.417s]
Writing 'root'                                     OKAY [  0.236s]
Finished. Total time: 264.331s

関連