Uart通信デバイスのUSB抜き差しを動的に検知したい



ソースコードは、こちらに置いておきます。

https://github.com/imammy/Udev-Sample


組み込み開発では一般的なI/FであるUART。
Linux環境でもUART使うことってよくあると思います。



色々調べると、いろんなブログ記事とかで以下のようなサンプルをよく見かけますね。
<details><summary>UARTのあるあるサンプル</summary>

```c:Uart_Smaple.c

#define SERIAL_PORT "/dev/ttyHOGE0"

int main()
{
    int fd = open(SERIAL_PORT, O_RDWR);
    if(fd < 0) {
        perror("open");
        printf("%d\n", errno);
        return -1;
    }

    ioctl(fd, TCGETS, &s_old_tio);      // 現在のポートの設定を退避しておく

    struct termios tio;
    tio.c_cflag += CREAD;               // 受信有効
    tio.c_cflag += CLOCAL;              // ローカルライン（モデム制御なし）
    tio.c_cflag += CS8;                 // データビット:8bit
    tio.c_cflag += 0;                   // ストップビット:1bit
    tio.c_cflag += 0;                   // パリティ:None


    cfsetispeed( &tio, baudRate );
    cfsetospeed( &tio, baudRate );

    tio.c_cc[VMIN] = 0;
    tio.c_cc[VTIME] = 0;
    tio.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);

    cfmakeraw(&tio);                    // RAWモード
    tcsetattr( fd, TCSANOW, &tio );     // デバイスに設定を行う
    ioctl(fd, TCSETS, &tio);            // ポートの設定を有効にする

    int len;
    // 受信処理ループ
    while(1) {
        len = read(fd, buf, 1);
    }
}
```
</details>

こちらのコードでは、システムコールのopen()の引数に`/dev/ttyHOGE0`を指定してますが、
事前に、USBの抜き差しの前後の`ls -l /dev/tty*`を見ておけば、
デバイスファイルの一覧に追加されるものが見つかるので、そこから分かると思います。

なので、あらかじめUSBが刺さっていることを確認し、カーネルがUSBデバイスを認識している状態で、
この通信プログラムを起動すれば問題はないはず。
パパッと、機器の動作を確認したい程度であれば、これで良いでしょう。

---
でも、ちょっとかっこよく使い回したいなと思いました。

:::note warn
- いちいち挿抜を確認しておかないといけない
- 自分の環境では`/dev/ttyHOGE0`だったかもしれないが、他の環境では変わることがある（他にシリアルの制御端末が刺さっていたりとか。）
:::

以下のことが出来たらいいですよね。

:::note info
- USBの挿抜を常時監視したい
- 刺さったデバイスが何者か判断して、適切なアプリを起動したい
:::

できるのかな？って事で、色々調べてみました、という話です。

## デバイスが何者か判断するには？
lsusbを使うことで確認する事ができます。

https://qiita.com/take5249/items/6aaad432584a5e80bf34

Vender ID、Product IDはそれぞれユニークな値になるので、控えておきましょう。


## USBの挿抜を監視するには？
### udevという仕組みを利用します
https://www.wdic.org/w/TECH/udev

> udev（userspace device management）とは，
> カーネルがパソコンへの接続を検出したデバイスに対して，動的に「デバイス・ファイル」を作成して割り当てるための仕組みです。
> Linuxは，システムに存在するあらゆるリソースをファイルとして扱うという特徴を持っています。

### udevにどんなイベントが来るのか見てみる
udevに問い合わせ・操作を行うための`udevadm`と呼ばれるコマンドラインツールがあります。
イベントの内容をリアルタイムに出力できるので、ホットプラグ時のイベントを確認できます。

udevadm monitor で実際にどんなデバイスイベントが来るのか見てみましょう。
<details><summary>udevadm monitorのログ</summary>

~~~
$ udevadm monitor
monitor will print the received events for:
UDEV - the event which udev sends out after rule processing
KERNEL - the kernel uevent

KERNEL[2372.548218] add      /devices/pci0000:00/0000:00:14.0/usb3/3-1 (usb)
KERNEL[2372.557778] add      /devices/pci0000:00/0000:00:14.0/usb3/3-1/3-1:1.0 (usb)
KERNEL[2372.560649] add      /devices/pci0000:00/0000:00:14.0/usb3/3-1/3-1:1.0/bluetooth/hci1/rfkill3 (rfkill)
KERNEL[2372.560716] bind     /devices/pci0000:00/0000:00:14.0/usb3/3-1/3-1:1.0 (usb)
KERNEL[2372.560755] add      /devices/pci0000:00/0000:00:14.0/usb3/3-1/3-1:1.1 (usb)
KERNEL[2372.560800] bind     /devices/pci0000:00/0000:00:14.0/usb3/3-1/3-1:1.1 (usb)
KERNEL[2372.560832] bind     /devices/pci0000:00/0000:00:14.0/usb3/3-1 (usb)
UDEV  [2372.564140] add      /devices/pci0000:00/0000:00:14.0/usb3/3-1 (usb)
UDEV  [2372.574849] add      /devices/pci0000:00/0000:00:14.0/usb3/3-1/3-1:1.0 (usb)
UDEV  [2372.583214] add      /devices/pci0000:00/0000:00:14.0/usb3/3-1/3-1:1.1 (usb)
UDEV  [2372.597508] bind     /devices/pci0000:00/0000:00:14.0/usb3/3-1/3-1:1.1 (usb)
UDEV  [2372.603877] add      /devices/pci0000:00/0000:00:14.0/usb3/3-1/3-1:1.0/bluetooth/hci1/rfkill3 (rfkill)
UDEV  [2372.610403] bind     /devices/pci0000:00/0000:00:14.0/usb3/3-1/3-1:1.0 (usb)
UDEV  [2372.613654] bind     /devices/pci0000:00/0000:00:14.0/usb3/3-1 (usb)
~~~
</details>

### ログの見方について
:::note
- `[2372.548218]`はタイムスタンプ
- `add`：「デバイスの追加」
- `bind`：「ドライバとデバイスの紐付け」
- `remove`：「デバイスの取り外し」
:::

どうやら、addとremoveをユーザープログラムから監視できれば、やりたいことが出来そうですね。
pythonにpyudevという便利なモジュールがあるのでこれを使用してみました。
udevの情報をpythonで取得できるようにしたモジュールになります。

https://pyudev.readthedocs.io/en/latest/


こちらのモジュールを使って、イベントを監視するプログラムを作ってみました。
<details><summary>udev監視サンプル</summary>

```python:udev_event.py
# -*- coding: utf-8 -*-

import pyudev
import socket
import serial
import serial.tools.list_ports

VENDER_ID = '1a2b'
PRODUCT_ID = '3c4d'
UNIX_DOMAIN_SOCKET_PATH = "/tmp/udev-event"

def main():
    # デバイスの接続が検知されたら、socketで通知したい。まずは接続
    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    s.connect(UNIX_DOMAIN_SOCKET_PATH)

    # まず、この監視プログラムが起動した時点で必要なデバイスが、接続されているか確認する
    ports = list(serial.tools.list_ports.comports())
    for p in ports:
        print('Device File Name : ' + p.device)
        print('Vendor ID : ' + str(format(p.vid, '04x')))
        print('Product ID : ' + str(format(p.pid, '04x')))
        print('Serial Number : ' + str(p.serial_number))
        print('Location : ' + p.location)
        print('MAnufacturer : ' + p.manufacturer)
        print('Product Name : ' + p.product)
        print('Interface : ' + str(p.interface))
        print('---')
        # ベンダーID、プロダクトIDが一致していたらソケットで通知
        if(str(format(p.vid, '04x')) == VENDER_ID and str(format(p.pid, '04x')) == PRODUCT_ID):
                s.send('Add Device!!,' + p.device)
                print('---')


    # 以降は、udevをモニタリングして、デバイス挿抜を監視する
    context = pyudev.Context()
    monitor = pyudev.Monitor.from_netlink(context)
    monitor.filter_by(subsystem='tty')
    monitor.start()

    for device in iter(monitor.poll, None):
        print(device.action)
        print('MODEL  : {0} -> {1}'.format(device.get('ID_MODEL_ID'), device.get('ID_MODEL_FROM_DATABASE')))
        print('VENDOR : {0} -> {1}'.format(device.get('ID_VENDOR_ID'), device.get('ID_VENDOR_FROM_DATABASE')))
        print('SERIAL : {0} -> {1}'.format(device.get('ID_SERIAL_SHORT'), device.get('ID_SERIAL')))
        print('SPECIAL FILE (DEVICE NAME) : {0} ({1})'.format(device.get('DEVNAME'), device.get('DEVTYPE')))
        print('---')

        if(device.get('ID_VENDOR_ID') == VENDER_ID and device.get('ID_MODEL_ID') == PRODUCT_ID):
            if(device.action == 'add'):
                s.send('Add Device!!,' + device.get('DEVNAME'))
                print('---')
            if(device.action == 'remove'):
                s.send('Remove Device!!,' + device.get('DEVNAME'))
                print('---')


if __name__ == "__main__":
    main()
```

</details>

### サンプルの説明
:::note
1. udevでaddとremoveは動的に検知できます。が、監視用のサンプルを起動した時点ですでにデバイスが刺さっていたことを考慮しなければいけません。
なので、serial.toolsという便利なモジュールがあったので、このプログラム起動時点で、OSが認識しているデバイスがあるかを調べるようにしてみました。
2. `device.action == 'add'、'remove'`で、挿抜イベントかチェックします
3. UARTを使うプログラム側へ判明したデバイスファイルを渡すのに、色々とやり方はあると思いますが、
今回はUNIXドメインソケットを選択しました。（ここは、よしなにやってみてください。）
4. Vender ID・Product IDは、lsusbコマンド等で調べたものを入れて頂ければ、大丈夫なはずです。
:::

### まとめ
これで、ちょっといけた感じでシリアルデバイスを操作できるようになりました。
udevの存在は初めて知りましたが、改めてLinuxがいろんな仕組みの上で支えられ成り立っていることを認識できました。