やったこと
市販のAndroidタブレットとシリアルインタフェースを持ったデバイス間の通信をしてみました。
通信プロトコルは、素のシリアル通信と、modbusどちらでも成功しました。
構成イメージ
リンク先の構成図とほとんど同じような構成です。
今回、シリアル通信の知識はゼロからスタートしたので、このサイトを死ぬほど見て勉強しました。
とてもわかりやすく纏まっていて助かりました。このサイトがなかったら多分実現できてません。
ライブラリ情報
usb-serial-for-android v2.2.2(https://github.com/mik3y/usb-serial-for-android/releases)
2020/4月ぐらいの話なので、バージョン古くてごめんなさい。
説明したいこと
当時はライブラリの使用方法について、ReadMeに書いてある内容があまり充実していなかったので、記事を書きたいと思っていました。
今は多少充実しているので迷ったのですが、補足説明できたらと思います。
ただ前述したとおり、シリアル通信自体はゼロスタートだったので、イメージで語る部分も多いです。
間違えがあればご指摘いただけたら助かります。
シリアル接続について
まずはじめに、シリアル接続についての前提知識の話です。
基本的に市販のAndroidタブレットにはシリアルインタフェースが付いていません。
そのため、物理構成は、【Androidタブレット→USBケーブル→シリアルUSB変換ケーブル→デバイス】のような接続になります。
Androidフレームワークには、USB操作のAPIが提供されていますが、シリアル通信のAPIは提供されていません。
そのため、シリアル通信をUSB通信でカプセル化する(アプリケーションからはシリアル通信ではなくただのUSB操作として見える)ようなイメージで実現する必要があります。
ここでやっかいなのが、シリアル通信の内容は「シリアルUSB変換ケーブル」に依存しており、ベンダーや製品毎に内容が変わります。
そんなこと実装してられないので、ライブラリが必要になる、というわけです。
ライブラリの使い方
2021/1月時点のReadMeより抜粋して、コメント入れていきます
ちなみに、通信用のスレッドを立ち上げる、マルチスレッド前提のサンプルです。
UIスレッド止めるのはどうかと思う(というか、Androidの制約で無理?)ので、普通はマルチスレッドにするはずです。
初期処理
// Find all available drivers from attached devices.
// Android標準のAPIを使用して、USBサービスのマネージャを作成します
UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);
// 以下はusb-serial-for-androidのAPIです。使用可能なUSBシリアル通信のドライバーを取得します。
// Proberというのは後述でも出てきますが、使用するケーブルのベンダIDと製品IDのマッピングテーブルです
// availableDriversにはProverに登録されているかつ、接続されているUSBシリアル変換ケーブルがあるときに値が入ります
List<UsbSerialDriver> availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager);
if (availableDrivers.isEmpty()) {
return;
}
// Open a connection to the first available driver.
// 以下、ReadMeのサンプルでは、get(0)でとりあえず最初のドライバーを選択しています。
UsbSerialDriver driver = availableDrivers.get(0);
// 複数のケーブルが接続されているときや、使用可能なケーブルを固定したかったので、
// 私は以下のように実装していました
// for(UsbSerialDriver driver: availableDrivers) {
// // ベンダIDと製品IDが一致するものを抽出
// String vendorId = Integer.toString(driver.getDevice().getVendorId());
// String productId = Integer.toString(driver.getDevice().getProductId());
// if(ベンダIDと製品IDが意図しているものと一致していたら) {
// return driver;
// }
// }
// 以下は、Android標準のAPIです。その名の通り、デバイスをオープンします。
UsbDeviceConnection connection = manager.openDevice(driver.getDevice());
if (connection == null) {
// add UsbManager.requestPermission(driver.getDevice(), ..) handling here
// USBデバイスへのアクセス権限がなかった時のハンドリング処理をここに書きます。
// 恐らくBroadcast Receiverの仕組みを使うと思います
return;
}
// 以下はusb-serial-for-androidのAPIです。ここもget(0)していていますが、
// 複数とれるパターンがどのような状況なのかわかりません。
UsbSerialPort port = driver.getPorts().get(0); // Most devices have just one port (port 0)
// シリアルポートのオープンです
port.open(connection);
// オープン後、シリアル通信の規格に合わせたパラメータをセットします。参考リンクを「シリアル設定内容」に記載します
port.setParameters(115200, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE);
送受信処理
単純な送受信
// 後述の「イベント処理」の方を普通は使うと思うので、多分こっちは使いません
// 送信
// requestは、byte[]型で書き込みたい内容を設定します
port.write(request, WRITE_WAIT_MILLIS);
// 受信
// responseは、byte[]型で内容を読み込ませたい変数を設定します
// lenには受信したサイズが入ります
len = port.read(response, READ_WAIT_MILLIS);
これだけだと、メソッドの説明には不十分だと思うので、UsbSerialPortのJavaDocを転記しておきます。
/**
* Reads as many bytes as possible into the destination buffer.
*
* @param dest the destination byte buffer
* @param timeout the timeout for reading in milliseconds, 0 is infinite
* @return the actual number of bytes read
* @throws IOException if an error occurred during reading
*/
public int read(final byte[] dest, final int timeout) throws IOException;
/**
* Writes as many bytes as possible from the source buffer.
*
* @param src the source byte buffer
* @param timeout the timeout for writing in milliseconds, 0 is infinite
* @return the actual number of bytes written
* @throws IOException if an error occurred during writing
*/
public int write(final byte[] src, final int timeout) throws IOException;
イベント処理
// 以下は、usb-serial-for-androidが提供している送受信マネージャクラスです
// 基本的にはこちらを使った方が良いと思います。
// が、あまり良いサンプルではないので、「私の実装例」で別途補足入れます。
usbIoManager = new SerialInputOutputManager(usbSerialPort, this);
// 以下、SingleThreadExecutorを使っていますが、マルチスレッド出来るなら何でも良いです
Executors.newSingleThreadExecutor().submit(usbIoManager);
...
port.write("hello".getBytes(), WRITE_WAIT_MILLIS);
// 通信内容の受信時に呼ばれるコールバック関数です。が、メソッドはSerialInputOutputManager.Listenerの実装とする
// 必要があるので、これをそのまま書いても動きません
@Override
public void onNewData(byte[] data) {
runOnUiThread(() -> { textView.append(new String(data)); });
}
私の実装例
実際はいろいろクラス設計をしていたので、以下と同じではないのですが、要所だけ抜粋します。
あと、以下の実装は当時、usbSerialExamplesパッケージにあったサンプルコードを参考にしました。
今もサンプルコードはあるのですが、実装がかなり変わっているのようなので、
直接サンプルコードを見た方が確実かもしれません。
// どこのクラスでも良いのですが、UIスレッドへのコールバックができる仕組みが必要になります
public class SampleActivity extends AppCompatActivity {
private SerialInputOutputManager mSerialIoManager;
private SerialInputOutputManager.Listener mListener;
private Handler mHandler;
private UsbSerialPort mPort;
// 任意のメソッド。恐らくonCreate等のライフサイクルで実施することになる。
@Override
protected void onCreate(Bundle savedInstanceState) {
// 「初期処理」相当の処理や、onCreateで必要な処理は省略
// シリアルポート作成処理
mPort = driver.getPorts().get(0); // Most devices have just one port (port 0)
// UIスレッドのハンドラを生成
mHandler = new Handler(Looper.getMainLooper());
// コールバック用のリスナを生成
mListener = new Listener {
// データ受信時に呼ばれるコールバックメソッド
@Override
public void onNewData(byte[] data) {
// UIスレッドに処理をコールバック
mainLooper.post(() -> {
// やりたい処理
});
}
// 何かしらのエラーを検知したときに呼ばれるコールバックメソッド
// 例えばケーブルが抜けた、とか
@Override
public void onRunError(Exception e) {
// UIスレッドに処理をコールバック
mHandler.post(() -> {
// やりたい処理
});
}
}
this.startSerial();
}
// 任意のメソッド。恐らくonDestroy等のライフサイクルで実施することになる。
@Override
protected void onDestroy() {
// onDestroyで必要な処理は省略
if (mSerialManager != null) {
this.stopSerial();
}
}
// シリアル通信開始用のメソッド
private void startSerial() {
if (mPort != null) {
// シリアル通信マネージャと、シリアルポート、イベント受信時のコールバックを紐づける
mSerialIoManager = new SerialInputOutputManager(mPort, mListener);
// マルチスレッド出来れば何でもよい
new Thread(mSerialIoManager).start();
} else {
// 適当にエラーハンドリング
}
}
// シリアル通信停止用のメソッド
private void stopSerial() {
if (mSerialIoManager != null) {
// シリアル通信マネージャを停止
mSerialIoManager.stop();
mSerialIoManager = null;
}
if (mPort != null) {
try {
// シリアルポートをクローズする
mPort.close()
} catch (IOException e) {
// 適当にエラーハンドリング
}
}
}
ちなみに、writeもシリアル通信マネージャでできます
byte[] data = // 送信したいデータ
mSerialIoManager.writeAsync(data);
[補足]シリアル設定内容
所感
私はあまりシリアル通信のことに詳しくないですが、そんな私でも簡単に通信を実現できました。
とても便利なライブラリだと思います。
ただ、あまり情報量は多くないので、エラーハンドリングや諸々のオープンクローズ処理には多少泣かされるかもしれません...
あと、これはシリアル通信の制約な気がしますが、Androidアプリから見たデバイスは、前述したところで言う、【Androidタブレット→USBケーブル→シリアルUSB変換ケーブル→デバイス】のデバイスではなく、シリアルUSB変換ケーブルです。
ということで、本物のデバイスが誰なのかを検知する仕組みは、当該ライブラリにはないようです。
セキュリティ的な観点でそれをどうするかは、要検討だと思います。