2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-08-07

※本記事はUARTを例にしていますが、別にそれに限った話ではないです。

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

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

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

UARTのあるあるサンプル
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);
    }
}

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

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


ただ、こちらのサンプルですと、以下のようなイケてない点があります

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

かっこよく使い回すためには、以下のことが出来たらいいですね。

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

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

デバイスが何者か判断するには?

lsusbを使うことで確認する事ができます。

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

USBの挿抜を監視するには?

udevという仕組みを利用します

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

udevにどんなイベントが来るのか見てみる

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

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

udevadm monitorのログ
$ 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)

ログの見方について

  • [2372.548218]はタイムスタンプ
  • add:「デバイスの追加」
  • bind:「ドライバとデバイスの紐付け」
  • remove:「デバイスの取り外し」

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

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

udev監視サンプル
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()

サンプルの説明

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

これで、ちょっといけた感じでシリアルデバイスを操作できるようになりました、、、!
udevの存在は初めて知りましたが、Linuxがいろんな仕組みの上で成り立っている事を、
改めて知ることが出来たので良かったです!

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?