疑似USBメモリを介したIoT機器からの情報取得

## はじめに

Raspberry PIやArduino、ESP32など、有線・無線LANに接続し、プログラムにより処理内容を変更できる、安価なIoTデバイスが普及してきました。
これらデバイスを使って、工場などでの機器稼働監視や、作業員へのリアルタイム作業指示など、ビジネス向けの応用が多数出ています。

ある機器を稼働監視する方法として、定常的に計測した観測値をサーバ側に集約し、週・月・年単位での長期間の時系列変化を見る方法があります。経年劣化による障害の可能性を予測したり、使用頻度に応じた定期保守部品の交換時期を算定したりできます。

ここでは、筆者がとある機器Aの稼働情報を収集する仕掛けを開発したときに、より性能の良い手法を試行錯誤で求めた具体例を紹介します。
手法を幾つか発案、各手法を試作し動作検証、性能測定してどの方式が良いか決めた手順を説明します。

## やりたいこと

今回やりたいことは、「機器Aから稼働情報が出力されたら、なるべく早くインターネット上にある収集サーバに送付したい」です。

以下が、機器Aの特徴です。

- 機器Aはインターネットに接続できない。USBメモリに稼働情報ファイルを出力する。
- 一日に数回程度、稼働情報を出力する
- 機器Aが稼働情報をファイル出力すると、5秒後にはファイル出力が完了する

## システム構成と動作概略
ARMベースのLinux機器Bを用意し、機器Aの稼働情報をインターネット上の収集サーバに転送します。以下の設定を施します。

- 機器Aと機器BをUSBケーブルで接続する
- 機器Bのディスク領域(/dev/mmcblk2p9)をVFATでフォーマットする
- LinuxのMass Storage Gadgetの機能を使い、当該領域を機器AにUSBメモリとして疑似的に見せる

参考：https://www.kernel.org/doc/html/latest/usb/mass-storage.html
稼働時は、以下のように動作します。

- 機器Aが疑似USBメモリにファイルを書くと、機器Bのディスク領域に書き込まれる。
- 機器Bが書き込みを検知したら、稼働情報ファイルを取得する。
- 機器Bは、取得した稼働情報をインターネット経由で収集サーバに送付する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695115/d22ed0d8-1f27-a704-423f-9bd2c0223564.png)



## ファイル書込検知

さて、LinuxのMass Storage Gadgetの機能を使って、疑似USBメモリを作る所までは設計できました。
次の課題は、機器Bがどうやって、機器Aが 1)新規ファイルを書き込んだ事を検知しするか です。書き途中のファイルをコピーすると情報不整合になるので、ファイルを 2)書き終えた事を検知 する必要もあります。USBメモリ上の全ファイルを毎回コピーすると時間と消費電力が無駄なので、新規ファイルだけをコピーする 3)差分コピー も必要です。

ここでは3通りのやり方を考えました。A) Read only mount, B) mtools polling, C) inotify APIです。各方式の概略を以下の表に示し、詳細を説明します。

|手順 \ 方式| A) Read only mount | B) mtools polling | C) inotify API |
|:-:|:-:|:-:|:-:|
|概略|mount/unmount繰り返し|mdirコマンド定期実行|inotifytoolsで書込検知|
|1. 新規ファイル検知| 5秒ポーリング、ls比較 | 5秒ポーリング、mdir比較 | inotifywait |
|2. 書終え検知| 5秒待つ | 5秒待つ | inotifywatch |
|3. 差分コピー| comm と cp | comm と mcopy | comm と mcopy |

### A) Read only mount

USBメモリ用のディスク領域を、Read onlyでmountする、unmountするを繰り返します。

- readonlyにするのは、機器Aと機器Bの両方から書き込むとファイルシステムが壊れるので、これを防ぐため。
- 毎回umountするのは、mountしたままでは file一覧がメモリ上にキャッシュされたままになり、機器Aがファイル追記したことを機器Bが検知できないため。

これを実現するシェルスクリプトを以下に示します。mountする度に、以前とファイル一覧に差があるか比較し、新規ファイルを/dest/にコピーします。

```sh
#!/bin/bash
touch old.lst
while true; do
  mount -o ro /dev/mmcblk2p9 /mnt
  ls /mnt | sort > new.lst
  diff old.lst new.lst > /dev/null # 1.
  if [ $? -ne 0 ]; then
    sleep 5 # 2.
    comm -13 old.lst new.lst | xargs -i cp {} /dest/ # 3.
  fi
  umount /mnt
  cp new.lst old.lst
  sleep 5 # 1.
done
```

### B) mtools polling
mtoolsを使い、新規ファイル追加を検知して、新規ファイルをコピーします。
- mtoolsは、主にフロッピーディスクや光磁気ディスク(MO, Magneto-Optical Disk)に読み書きする際に使われたツール。
- mtoolsのmdirコマンドを使うと、USBメモリ内のファイル一覧を取得できる。
- mdirコマンドを定期実行し結果の差分を見ることで、新規ファイル追加を検知する。
- 新規ファイルをmcopyコマンドでコピーする。

以下が、これを実現するシェルスクリプトです。

```sh
#!/bin/bash
touch old.lst
while true; do
  mdir -b -i /dev/mmcblk2p9 | sort > new.lst
  diff old.lst new.lst > /dev/null # 2.
  if [ $? -ne 0 ]; then
    sleep 5 # 書き終わるまで5秒待つ
    comm -13 old.lst new.lst | xargs -i mcopy -i /dev/mmcblk2p9 {} /dest/ # 3.
  fi
  cp new.lst old.lst
  sleep 5 # 1.
done
```

### C) inotify API
Linuxのinotify API を使い、ファイルが更新されたことをイベントで検知します。
- inotifywaitコマンドを実行しておく。
- USBメモリのディスクイメージへの書き込み(modify)を検知すると、inotifywaitコマンドが終了する。
- inofitywatchコマンドで書き込み完了を検知する。具体的には、直近5秒間の書き込みが無くなるまで待つ。
- 新規追加されたファイルをmcopyコマンドでコピーする。

実行にはinotify-toolsが必要です。debian/ubuntuの場合は、事前にapt install inotify-tools を実行してください。

```sh
#!/bin/bash
touch old.lst
while true; do
  inotifywait -e modify /dev/mmcblk2p9 # 1.
  while inotifywatch -t 5 -e modify /dev/mmcblk2p9 \
    | grep -v 'No events occurred.' > /dev/null; do # 2.
    :
  done

  mdir -b -i /dev/mmcblk2p9 | sort > new.lst
  comm -13 old.lst new.lst | xargs -i mcopy -i /dev/mmcblk2p9 {} /dest/ # 3.
  cp new.lst old.lst
done
```

## 性能比較

これら3方式の処理性能を比較し、どの方式が良いか決めましょう。以下の条件で性能測定しました

- Hardware: ARM Cortex A53, 4core
- Kernel: Linux 4.4.194 ARM64
- 疑似USB memory上に30000個のファイルを作成 (FAT32で1ディレクトリ上に置ける最大ファイル数は32767のため)
- A～Cの方式を実行し、vmstatコマンドで装置2の負荷を測定
- 性能は5秒間の平均で測定。
- 定常時の負荷を測定するため、最初の5回分の測定結果は捨て、次の10回分の結果の平均を計測。

```
$ vmstat 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 0  0      0 1249656  81340 461084    0    0     5     2   72   52  0  1 99  0  0
 1  0      0 1249392  81340 461084    0    0  1671    11 1001 1088  1  2 96  0  0
 0  0      0 1249376  81340 461092    0    0  1671    10  910  968  1  2 96  0  0
```

結果は以下の通りです。数値が低いほど良い性能です。

|性能種別\方式| A | B | C |
|:-:|:-:|:-:|:-:|
|us: CPUユーザ時間(%) | 1  | 1 |  0 |
|sy: CPUシステム時間(%) | 2  | 0  | 0  |
|bi: Disk読込量(blocks/秒) |1671|320 | 0  |


3方式とも、CPU使用率は低いです。
Disk読込量に注目すると、方式Cが0と最低です。方式A,Bが5秒毎に高負荷な処理を実行するのに対して、方式Cではwriteシステムコールが起きるまで何もしないで待つため、最低の負荷で済みます。
消費電力的にも他処理への影響を避けるためにも、IO帯域使用量は低い方が望ましいです。
以上の結果から、inotify APIを使う方式Cを採用する事にしました。

## 最後に
性能比較をする際に、奇妙な現象を確認しました。
機器A側でUSBメモリ上のファイルを読むと、inotifyのMODIFYイベントが出ます。ファイルを読んでいるだけなのに、変更イベントが出るのは奇妙です。
[次回](https://qiita.com/diizuka-biz/items/dbefefc92d026d1cc0a3)、この奇妙な現象の原因を解析した過程を紹介します。

### 商標
- Raspberry Piは、 RASPBERRY PI FOUNDATIONの英国およびその他の国における登録商標または商標です。
- Arduinoは、Arduino AGのイタリアまたは他の国における商標登録または他の国における商標登録または商標です。
- ESP32は、Espressif Systems (Shanghai) Co., Ltd.の中国または他の国における商標登録または商標です。
- Armは、Arm Limitedの英国およびその他の国における登録商標または商標です。
- Linuxは、Linus Torvalds氏の日本およびその他の国における登録商標または商標です。
- 記載の会社名、製品名、サービス名等はそれぞれの会社の商標または登録商標です。