LinuxでUSB機器を配線したまま接続のON/OFFを行う

# これはなに？

LinuxでUSBのON/OFFを行います。
検証環境

- Ubuntu 21.10
  - Kernel 5.16.1 (自前ビルド)

# どうして必要なの？

フロントポートのUSBなら物理的に抜き差しも容易ですが裏側のポートは面倒。
たまに認識しなくなった際にまず再接続を行いたい。

# 方法

端的に言うと以下のコマンドラインで可能。

接続する

```sh
echo $BUS_ID | sudo tee /sys/bus/usb/drivers/usb/bind
```

切断する

```sh
echo $BUS_ID | sudo tee /sys/bus/usb/drivers/usb/unbind
```

## BUS IDの確認方法

まず接続デバイスを確認します。
僕の環境では以下のようになっています。

```sh
lsusb
```

```text
Bus 008 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 007 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 006 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 005 Device 003: ID 08bb:2904 Texas Instruments PCM2904 Audio Codec
Bus 005 Device 002: ID 0582:00a3 Roland Corp. EDIROL UA-4FX
Bus 005 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 004 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 003 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 002 Device 003: ID 1bcf:2c99 Sunplus Innovation Technology Inc. MiraBox Video Capture
Bus 002 Device 002: ID 8564:4000 Transcend Information, Inc. microSD/SD/CF UHS-II Card Reader [RDF8, RDF9]
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 005: ID 1462:7c94 Micro Star International MYSTIC LIGHT 
Bus 001 Device 003: ID 05e3:0608 Genesys Logic, Inc. Hub
Bus 001 Device 006: ID 046d:c069 Logitech, Inc. M-U0007 [Corded Mouse M500]
Bus 001 Device 004: ID 258a:000c HAILUCK CO.,LTD USB KEYBOARD
Bus 001 Device 002: ID 05e3:0608 Genesys Logic, Inc. Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
```

USB機器にはvendorIDとproductIDが振られており、 `lsusb` を実行した際も十六進法で `XXXX:XXXX` という表記で機器名の左に表示されています。

このvendorIDとproductIDを控えておき、 `dmesg` のログからどう認識されたか確認します。
今回は `08bb:2904` を見てみましょう。
ちなみにこれはベースエフェクターのB2.1uです。

```text
(Ubuntu) [sakura@x2 ~]$ sudo dmesg | grep 08bb | grep 2904
[    1.389766] usb 5-2: New USB device found, idVendor=08bb, idProduct=2904, bcdDevice= 1.00
```

`usb 5-2: 〜` となっており、この `5-2` の部分がBUS_IDとなります。

## 試してみる

まず現状を確認してみます。
B2.1uはサウンドデバイスとして認識されているので `/proc/asound/` を覗きます。

```text
(Ubuntu) [sakura@x2 ~]$ ll /proc/asound/B21U 
lrwxrwxrwx 1 root root 5  1月 18 13:03 /proc/asound/B21U -> card5
```

デバイスが認識されているようです。

それでは外してみましょう。

```text
(Ubuntu) [sakura@x2 ~]$ echo 5-2 | sudo tee /sys/bus/usb/drivers/usb/unbind
5-2
(Ubuntu) [sakura@x2 ~]$ ll /proc/asound/B21U                               
ls: '/proc/asound/B21U' にアクセスできません: そのようなファイルやディレクトリはありません
```

無事に切断されました。
B2.1uのLEDも消えております。

このまま再度繋いでみましょう。

```text
(Ubuntu) [sakura@x2 ~]$ echo 5-2 | sudo tee /sys/bus/usb/drivers/usb/bind  
5-2
(Ubuntu) [sakura@x2 ~]$ ll /proc/asound/B21U                             
lrwxrwxrwx 1 root root 5  1月 18 13:08 /proc/asound/B21U -> card5
```

再び認識されました。
これでベースの演奏を録音できます。

### スクリプトにまとめる

```dart:usbdevctl.dart
import 'dart:io';

int main(List<String> arguments) {
  // 引数なければlsusbの結果を表示する
  if (arguments.length < 1) {
    usage();
    return 1;
  }

  final action = arguments[0];
  if (action == 'help') {
    help();
  } else if (action == 'lsusb') {
    printLsUsb();
  } else if (action == 'bind' || action == 'unbind') {
    if (arguments.length < 2) {
      help();
      return 1;
    }

    final subArgs = arguments.sublist(1);
    if (action == 'bind') {
      bind(subArgs);
    } else if (action == 'unbind') {
      unbind(subArgs);
    }
  }
  return 0;
}

void usage() {
  print('check the help. type the following');
  print('  usbdevctl help');
  print('');
  printLsUsb();
}

void help() {
  print('usage: usbdevctl COMMAND [ARGS1 ...]');
  print('');
  print('suppored command list');
  print(' help: this message');
  print(' lsusb: show results `lsusb`');
  print(' bind: connects the device with the vendorID:productID specified in the arguments');
  print('  usbdevctl bind XXXX:xxxx [YYYY:yyyy]');
  print(' unbind: disconnects the device with the vendorID:productID specified in the arguments');
  print('  usbdevctl unbind XXXX:xxxx [YYYY:yyyy]');
}

void printLsUsb() async {
  print((await Process.run('lsusb', [])).stdout);
}

void bind(List<String> ids) async {
  final busIds = await retrieveUsbBusId(ids);
  if (busIds == null) {
    print('device not found');
    return;
  }

  for (var busId in busIds) {
    bindCommand('bind', busId);
  }
}

void unbind(List<String> ids) async {
  final busIds = await retrieveUsbBusId(ids);
  if (busIds == null) {
    print('device not found');
    return;
  }

  for (var busId in busIds) {
    bindCommand('unbind', busId);
  }
}

void bindCommand(String type, String busId) async {
  // echo "$busId" | sudo tee /sys/bus/usb/drivers/usb/unbind
  final echo = await Process.start('echo', [busId]);
  final sudoTee = await Process.start('sudo', ['tee', '/sys/bus/usb/drivers/usb/${type}']);
  echo.stdout.pipe(sudoTee.stdin);
}

Future<List<String>?> retrieveUsbBusId(List<String> ids) async {
  var result = <String>[];
  for (var id in ids) {
    final vid_pid = id.split(':');
    if (vid_pid.length != 2) {
      print('invalid id string: ${id}');
      continue;
    }
    final vid = vid_pid[0];
    final pid = vid_pid[1];
    final busId = await retrieveUsbBusIdFromVidAndPid(vid, pid);
    if (busId == null) {
      print('perhaps a dvice that is not plugged in: ${id}');
      continue;
    }
    if (result.contains(busId)) {
      continue;
    }
    result.add(busId);
  }

  if (result.length < 1) {
    return null;
  }
  return result;
}

Future<String?> retrieveUsbBusIdFromVidAndPid(String vid, String pid) async {
  final baseDir = '/sys/devices/';

  final vidResult = await Process.run('grep', ['-lrs', vid, baseDir]);
  if (vidResult.stdout == "") {
    return null;
  }

  final exp = new RegExp(r'^.*/usb[0-9]+/([^/]+)/id(Vendor|Product)$');

  // vendorIDにひっかかったBUS ID一覧を作る
  var vidMatchBusId = <String>[];
  for (var vidValue in vidResult.stdout.split('\n')) {
    final match = exp.firstMatch(vidValue);
    if (match == null) {
      continue;
    }
    final vidBusId = match.group(1);
    if (vidBusId == null || vidMatchBusId.contains(vidBusId)) {
      continue;
    }
    vidMatchBusId.add(vidBusId);
  }

  final pidResult = await Process.run('grep', ["-lrs", pid, baseDir]);
  if (pidResult.stdout == "") {
    return null;
  }

  // productIDでひっかかったBUS IDがvendorIDでひっかかったBUS IDにもあれば指定したvendorID:productIDのデバイスが接続されているBUS IDなので返す
  // 同じvendorID:productIDのデバイスを複数繋ぐ可能性はあるが現状そのような状況にならないので考えないでおく
  for (var pidValue in pidResult.stdout.split('\n')) {
    if (!exp.hasMatch(pidValue)) {
      continue;
    }
    final match = exp.firstMatch(pidValue);
    if (match == null) {
      continue;
    }
    final pidBusId = match.group(1);
    if (pidBusId == null) {
      continue;
    }
    if (vidMatchBusId.contains(pidBusId)) {
      return pidBusId;
    }
  }

  return null;
}
```

大体サブプロセスでシェルコマンド実行してるだけやんけ！

使い方

```
(Ubuntu) [sakura@x2 ~]$ usbdevctl unbind 08bb:2904
(Ubuntu) [sakura@x2 ~]$ ll /proc/asound/B21U      
ls: '/proc/asound/B21U' にアクセスできません: そのようなファイルやディレクトリはありません
(Ubuntu) [sakura@x2 ~]$ usbdevctl bind 08bb:2904 
(Ubuntu) [sakura@x2 ~]$ ll /proc/asound/B21U      
lrwxrwxrwx 1 root root 5  1月 18 13:36 /proc/asound/B21U -> card5
```

usbdevctlだけ打つとlsusbの結果を返すのでとりあえずvendorIDとproductIDは拾いやすいはず。

今後の検討課題

- リスト表示から番号選択してON/OFFするようなプログラムにしたい

---

それでは良いLinuxライフを。