Bluetooth SPP(Rfcomm)双方向通信

前のポストで、Windows 8.1の新機能、Bluetooth Serial Port Profileを介して接続するデバイスとの通信方法について、基本を説明しました。

実際のデバイスとのデータ通信では、ハンドシェーク的なコマンドの送信だけでなく、単に送るだけのコマンドあり、デバイスの方から任意のタイミングで送信してくる通知もあります。例えばLego Mindstormsとの通信の場合もそうです。

.NET FrameworkのSerialPortクラスにはデータ受信を知るためのイベントハンドラが用意されていたり、Threadを駆使したマルチスレッドプログラミングでこなせました。WinRT APIでは、これらの方法は使えないので、別のパターンが必要です。

このポストでは、非同期プログラミングの仕組みと、タスク間の同期をとるManualResetEventクラスを使います。Lego Mindstorms NXTとの通信を例として説明します。

通知は任意の時点で送信されてきます。応答コマンドはPCから応答付コマンドを送信した時その応答として送信されてきます。デバイスから送られてくる送信を常に受信するループを用意します。PCから応答付コマンドを送信する場合、データ送信後に待ちにはいり、受信ループ側ではデータパケットを受信した時、通知か応答付コマンドの応答かを判断し、応答であればデータ送信後の待ちを解除する、通知であればバッファリング、もしくは、イベントを発行するなどでロジックを組めば、うまく動きます。

先ず、以下のメンバー変数、メソッドを用意します。

    private ManualResetEvent mre = new ManualResetEvent( false );
    private byte lastCommandSent;
    private List<byte[]> receivedMessages = new List<byte[]>();

    private async Task DataReceive(StreamSocket socket)
    {
        DataReader dataReader = new DataReader(socket.InputStream);
        while (btSocket != null)
        {
            var loadResult = await dataReader.LoadAsync(2);
            byte[] headerBuffer = new byte[loadResult];
            dataReader.ReadBytes(headerBuffer);
            int lsb = headerBuffer[0];
            int msb = headerBuffer[1];
            int size = lsb + msb * 256;
            byte[] readBuffer = new byte[size];
            loadResult = await dataReader.LoadAsync(size);
            dataReader.ReadBytes(readBuffer);
            if (readBuffer[0] != (byte)CommandType.Reply)
            {
                // 通知の場合
                var received = readBuffer.Copy(0, size);
                lock (this)
                {
                    // 通知データパケットをバッファリング
                    receivedMessages.Add(received);
                }
            }
            else
            {
                // 応答コマンドの場合
                if (readBuffer[1] == lastCommandSent)
                {
                    // 受信データを応答コマンドバッファに登録
                    LastResponse = readBuffer.Copy(0, size);
                }
                // 応答付コマンド送信後に待っているスレッドに通知
                mre.Set();
           }
      }
      return;
  }

さて、応答付コマンドの送信ロジックですが、以下のようにコーディングします。送信するバイト列は既にbyte[]のcommandにセットされているものとします。

    // ManualResetEventをリセット
    this.mre.Reset();

    // 送信する応答付コマンドを登録
    lastCommandSent = command[1];
    // ここでDataWriterを使ってNXTに向けてデータを送信。コードは省略
    …
    // 応答を受信するまでManualResetEventを使って待ち
    this.mre.WaitOne( millisecondsTimeout);

これで、デバイスからの応答が必要なコマンドは、応答を受信するまで待つことができます。後は、DataRecieveメソッドを別スレッドで動くようにコールすれば仕組みが成り立ちます。
DeviceInformation、RfcommServiceDevice、StreamSocketを使って接続し、StreamSocketが出来上がった後、

    btSocket = new StreamSocket();
    await btSocket.ConnectAsync(rfcommServiceDevice.ConnectionHostName, rfcommServiceDevice.ConnectionServiceName, SocketProtectionLevel.BluetoothEncryptionAllowNullAuthentication);

    Task taskReceive = Task.Run(async () => { DataReceive(btSocket); });
    taskReceive.ConfigureAwait(false);

と、Taskを使ってDataReceiveメソッドをコールします。単に非同期にコールしただけでは、同じスレッド上で動いてしまうので、ManualResetEventの同期が成り立ちません。

Bluetooth SPP(Rfcomm)だけでなく、StreamSocket、DataReader、DataWriterを使った周辺機器連携では、このパターンが使えるはずなので、お試しください。