概要
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を自動アタッチにするとポートがわからなくなるので自動アタッチできなかったのが不便でした。
識別できるツールを作ってみた
上記のツールになります。
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とかローカル開発環境だけの方が無難だと思います。


コメント