LinuxでCH340などのtty名を固定する

概要

Linuxの場合シリアルポートは/dev/ttyUSB0などのように接続した順番で認識されます。WSLとusbipを組み合わせて利用する場合にはアタッチした順番によってシリアルポート名が変更されてしまいます。そこで認識したトリガーを使ってMACアドレスを読み込んで別名でシリアルポートを定義するツールを作ってみました。

通常の動き

$ ls -la /dev|grep dialout
crw-rw----  1 root dialout 166,   0 Aug  7 01:18 ttyACM0
crw-rw----  1 root dialout 166,   1 Aug  7 01:18 ttyACM1
crw-rw----  1 root dialout 166,   2 Aug  6 23:57 ttyACM2
crw-rw----  1 root dialout 166,   3 Aug  6 23:57 ttyACM3
crw-rw----  1 root dialout   4,  64 Jul 31 10:29 ttyS0
crw-rw----  1 root dialout   4,  65 Jul 31 10:29 ttyS1
crw-rw----  1 root dialout   4,  66 Jul 31 10:29 ttyS2
crw-rw----  1 root dialout   4,  67 Jul 31 10:29 ttyS3
crw-rw----  1 root dialout   4,  68 Jul 31 10:29 ttyS4
crw-rw----  1 root dialout   4,  69 Jul 31 10:29 ttyS5
crw-rw----  1 root dialout   4,  70 Jul 31 10:29 ttyS6
crw-rw----  1 root dialout   4,  71 Jul 31 10:29 ttyS7
crw-rw----  1 root dialout 188,   0 Aug  7 00:14 ttyUSB0
crw-rw----  1 root dialout 188,   1 Aug  7 00:02 ttyUSB1

上記のように通常は/dev/配下にttyから始まる名前でシリアルポートが定義されています。ttyS0は内蔵のシリアルでWSL配下なので使えないと思います。ttyUSB*がUSBシリアル変換のCH340とかで専用ドライバーが必要なもの、ttyACM*はCDCの標準ドライバーで接続するCH343とかになります。

$ ls -l /dev/serial/by-id/ | cut -c 40-

usb-1a86_USB_Serial-if00-port0 -> ../../ttyUSB1
usb-1a86_USB_Single_Serial_58FA041019-if00 -> ../../ttyACM3
usb-1a86_USB_Single_Serial_5B5E057951-if00 -> ../../ttyACM0
usb-1a86_USB_Single_Serial_5B5E058417-if00 -> ../../ttyACM1
usb-1a86_USB_Single_Serial_5B5F091220-if00 -> ../../ttyACM2

そして通常識別をするときには/dev/serial/by-id/を確認します。この場合5個のシリアルポートでttyUSB0がありません。これはCH340はチップに識別情報がないので区別できず、あとから認識したttyUSB1に上書きされています。CH343は識別情報があるので58FA041019などのようなIC別の情報で名前が作られています。

$ ls -l /dev/serial/by-path|cut -c 40-

platform-vhci_hcd.0-usb-0:1:1.0 -> ../../ttyACM0
platform-vhci_hcd.0-usb-0:2:1.0 -> ../../ttyACM1
platform-vhci_hcd.0-usb-0:3:1.0 -> ../../ttyACM2
platform-vhci_hcd.0-usb-0:4:1.0 -> ../../ttyACM3
platform-vhci_hcd.0-usb-0:5:1.0-port0 -> ../../ttyUSB0
platform-vhci_hcd.0-usb-0:6:1.0-port0 -> ../../ttyUSB1
platform-vhci_hcd.0-usbv2-0:1:1.0 -> ../../ttyACM0
platform-vhci_hcd.0-usbv2-0:2:1.0 -> ../../ttyACM1
platform-vhci_hcd.0-usbv2-0:3:1.0 -> ../../ttyACM2
platform-vhci_hcd.0-usbv2-0:4:1.0 -> ../../ttyACM3
platform-vhci_hcd.0-usbv2-0:5:1.0-port0 -> ../../ttyUSB0
platform-vhci_hcd.0-usbv2-0:6:1.0-port0 -> ../../ttyUSB1

そんなときに利用するのが/dev/serial/by-pathです。こちらはUSBハブのポート番号で区別が付きます。こちらだと6個のシリアルポートが全部見えています。

WSLとusbipを使うとどうなるか?

>usbipd list
Connected:
BUSID  VID:PID    DEVICE                                                        STATE
1-14   8087:0033  インテル(R) ワイヤレス Bluetooth(R)                           Not shared
13-4   0d3d:0001  USB 入力デバイス                                              Not shared
14-3   045e:0039  Microsoft USB IntelliMouse Optical                            Not shared
17-1   1a86:55d3  USB-Enhanced-SERIAL CH343 (COM6)                              Attached
17-2   1a86:55d3  USB-Enhanced-SERIAL CH343 (COM7)                              Attached
17-3   1a86:55d3  USB-Enhanced-SERIAL CH343 (COM5)                              Attached
17-4   1a86:55d3  USB-Enhanced-SERIAL CH343 (COM9)                              Attached
18-1   1a86:7523  USB-SERIAL CH340 (COM14)                                      Attached
18-2   1a86:7523  USB-SERIAL CH340 (COM15)                                      Attached
18-4   04b4:8613  Unknown Device #1                                             Not shared

Windows上での実行です。BUSIDがWindows上のUSBハブのポート配置になります。Attachedになっているのが6個なので正しいです。ただLinux上のBUSIDと一致していませんね。

そうです、usbipを使ってUSBをアタッチするとLinux側は認識した順にBUSIDを割り振ります。すると本当のUSBハブのポート配置とは関係無い順番になります。

Windows上でもBUSIDは再起動とかUSBハブを抜き出しすると番号が変わったりします。そして、usbipは自動アタッチもできるのですが、アタッチされる順番はランダムになります。その結果CH340のデバイスはどれがどれだかわからなくなります。

上記はWindows上でusbipを管理するツールなのですが、複数のCH340を自動アタッチにするとポートがわからなくなるので自動アタッチできなかったのが不便でした。

識別できるツールを作ってみた

GitHub – tanakamasayuki/board-identify
Contribute to tanakamasayuki/board-identify development by creating an account on GitHub.

上記のツールになります。

ACTION=="add", SUBSYSTEM=="tty", KERNEL=="ttyUSB*", TAG+="systemd", ENV{SYSTEMD_WANTS}+="board-identify@%k.service"
ACTION=="add", SUBSYSTEM=="tty", KERNEL=="ttyACM*", TAG+="systemd", ENV{SYSTEMD_WANTS}+="board-identify@%k.service"

ACTION=="remove", SUBSYSTEM=="tty", KERNEL=="ttyUSB*", TAG+="systemd", RUN+="/usr/bin/systemctl --no-block start board-identify-cleanup.service"
ACTION=="remove", SUBSYSTEM=="tty", KERNEL=="ttyACM*", TAG+="systemd", RUN+="/usr/bin/systemctl --no-block start board-identify-cleanup.service"

仕組みとしてはudev/90-board-identify.rulesを入れて、ttyが増えたタイミングと減ったタイミングで独自のスクリプトを起動させます。対象はUSBシリアル変換の”ttyUSB*”と、CDCの”ttyACM*”になります。

    def identify(self, port: Path) -> Identification | None:
        try:
            completed = subprocess.run(
                [
                    sys.executable,
                    "-m",
                    "esptool",
                    "--port",
                    str(port),
                    "--baud",
                    "115200",
                    "read-mac",
                ],
                capture_output=True,
                text=True,
                timeout=15,
                check=False,
            )

Arduino純正ボードとかはUSBのVIDやPIDで特定できるのですが、わからない場合には環境別に特定できるまでコマンドを実行します。ESP32の場合にはesptoolでread-macをします。

$ uv run -m esptool --port /dev/ttyUSB0 --baud 115200 read-mac
esptool v5.3.1
Connected to ESP32 on /dev/ttyUSB0:
Chip type:          ESP32-D0WD-V3 (revision v3.1)
Features:           Wi-Fi, BT, Dual Core + LP Core, 240MHz, Vref calibration in eFuse, Coding Scheme None
Crystal frequency:  40MHz
MAC:                ec:e3:34:70:3a:84

Stub flasher running.

MAC:                ec:e3:34:70:3a:84

Hard resetting via RTS pin...

上記のコマンドを実行するとこれが取れました。普通のESP32ですね。

$ tree /run/board-identify/
/run/board-identify/
├── by-id
│   ├── esp32-d0wd-v3-0070070e9b0c -> /dev/ttyUSB1
│   ├── esp32-d0wd-v3-ece334703a84 -> /dev/ttyUSB0
│   ├── esp32-p4-e8f60ae0aa24 -> /dev/ttyACM2
│   ├── esp32-s3-d0cf1358fd94 -> /dev/ttyACM1
│   ├── esp32-s3-d0cf1359101c -> /dev/ttyACM0
│   └── esp32-s3-e4b063b4a81c -> /dev/ttyACM3
└── state
    ├── ttyACM0.json
    ├── ttyACM1.json
    ├── ttyACM2.json
    ├── ttyACM3.json
    ├── ttyUSB0.json
    └── ttyUSB1.json

これらの情報をもとに/run/board-identify/配下に特定できるシリアルポートを作ってくれます。

/run/board-identify/by-id/esp32-d0wd-v3-ece334703a84

先程のESP32はCH340搭載で/dev/ttyUSB0で認識していますが、上記の固定名を使えば認識順が入れ替わって/dev/ttyUSB1で認識されていても、同じ名前でアクセスすることができます。

また、デバイスが取り外されたときにはシンボリックリンク先がない識別名はすべて削除されるような動きとなっています。

まとめ

WSL上でpytestを実行しているのですが、KVMを入れている環境でBUSIDが頻繁に変わります。自動アタッチが使えなかったので手でぽちぽちアタッチを繰り返していたので面倒でした。

あとpytestを実行しているとたまにボードがハングアップしたりしてusbipのアタッチが外れたりするんですよね。このツールを使えば自動アタッチで復旧できるかもと思って作ってみました。

やっていることは単純なのですが、本番系のLinuxとかだとちょっと怖いのでWSLとかローカル開発環境だけの方が無難だと思います。

コメント