kkbnart's diary

遊びは終わりだ

PC・Android間のBluetooth通信

PCに繋いだセンサ情報をandroidに送信するプログラムを作る機会があったので,そのまとめ.
システムの要件としては以下の様になります.

  • Android側は元々Bluetoothでデータをやり取りする部分があり,そのデータをセンサ情報で置き換えるため通信にはBluetoothを使用
  • PCはVAIOで,元からBluetoothを搭載しているので,それを使用.
  • センサのライブラリがC++のコードで用意されていたので,PC側はC++を使用
  • PC側のソケット通信には最もメジャーであろうSocketライブラリを使用
  • サーバーはPC側で立て,Androidから接続

やったことある人には簡単じゃないかと思われるかもしれませんが,自分は通信まわりのプログラムがとても苦手で,今回も案の定大苦戦を強いられました・・・
全体のおおまかな流れとしては,以下の様になります.

  1. Androidからの接続を受け付けるためのSocketを開く
  2. Androidから接続し,通信用のSocketを開く
  3. 通信用のソケットを通し,センサデータを送信する

通信用のSocketを開くまで

下準備

まずBluetoothのペアリングを行います.この部分に関してはBluetoothドングルを用いた場合など,個別のケースによるので割愛.
次に通信用のCOMポートの設定を行います(これはひょっとしたら必要ないかもしれませんが).今回用いたVAIOPCの場合,Android側でUUIDを指定してソケットを開くとBluetoothの設定から接続先のCOMポートの追加ができました.USBでつなぐ場合は自動的に割り振られると思います(確認はしてないけど).

socket部分のコード

ここまでで下準備は終了.ここからはコードに移ります.
Android側のプログラムに関しては,公式のsampleのBluetoothChatの通信部分を用いました.一部変更点としては,UUIDとしてSPP(SerialPortServiceClass_UUID)を使用し,BluetoothChatService.javaのMY_UUIDを変更します.通信のUUIDに関して詳しく知りたい方はここを参考に.

変更前

// Unique UUID for this application
private static final UUID MY_UUID = UUID.fromString("fa87c0d0-afac-11de-8a39-0800200c9a66");

変更後

// Unique UUID for this application
private static final UUID MY_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");

次にPC側です.この部分はSPP通信用Bluetooth Socket Serverを参考にしました.コードは重要な部分のみ抜粋.
接続用のsocketを開き,androidからの接続を待って通信用のsocketを開く部分のコード.

    SOCKET sockRecv;
    SOCKADDR_BTH sockAddrBth = {0};

    sockRecv = socket(AF_BTH, SOCK_STREAM, BTHPROTO_RFCOMM);
    if (INVALID_SOCKET == sockRecv) {
        return false;
    }

    sockAddrBth.addressFamily = AF_BTH; // For Bluetooth
    sockAddrBth.port = BT_PORT_ANY;
    if (SOCKET_ERROR == bind(sockRecv, (SOCKADDR *) &sockAddrBth, sizeof(sockAddrBth)))
    {
        closesocket(sockRecv);
        return false;
    }

    int size = sizeof(sockAddrBth);
    getsockname(sockRecv, (SOCKADDR *) &sockAddrBth, &size);

    CSADDR_INFO info = {0};
    info.LocalAddr.lpSockaddr = (LPSOCKADDR) &sockAddrBth;
    info.LocalAddr.iSockaddrLength = sizeof(sockAddrBth);
    info.iSocketType = SOCK_STREAM;
    info.iProtocol = BTHPROTO_RFCOMM;

    WSAQUERYSET set = {0};
    set.dwSize = sizeof(WSAQUERYSET);                              // Must be set to sizeof(WSAQUERYSET)
    set.dwOutputFlags = 0;                                         // Not used
    set.lpszServiceInstanceName = "Server";                        // Recommended.
    set.lpServiceClassId = (LPGUID)&SerialPortServiceClass_UUID;   // !ここのUUID(00001101-0000-1000-8000-00805F9B34FB)はAndroid側とマッチしている必要がある!
    set.lpVersion = NULL;                                          // Not used.
    set.lpszComment = NULL;                                        // Optional.
    set.dwNameSpace = NS_BTH;                                      // Must be NS_BTH.
    set.lpNSProviderId = NULL;                                     // Not required.
    set.lpszContext = NULL;                                        // Not used.
    set.dwNumberOfProtocols = 0;                                   // Not used.
    set.lpafpProtocols = NULL;                                     // Not used.
    set.lpszQueryString = NULL;                                    // not used.
    set.dwNumberOfCsAddrs = 1;                                     // Must be 1.
    set.lpcsaBuffer = &info;                                       // Pointer to a CSADDR_INFO.
    set.lpBlob = NULL;                                             // Optional.

    if (WSASetService(&set, RNRSERVICE_REGISTER, 0) != 0) {
        return false;
    }

    listen(sockRecv, 0);

    SOCKADDR_BTH sab2;
    int ilen = sizeof(sab2);
    // 通信用のsocketを開く.このsocketで通信を行う.
    acceptedSocket = accept(sockRecv, (SOCKADDR *)&sab2, &ilen);
    // 接続用のsocketは必要ないので閉じる
    closesocket(sockRecv);

上記コードのacceptedSocketは保持しておき,通信にも用います.

Androidにデータを送信

ここまでくればあと少し・・・のはずが,ここでまさかのどん詰まり.数時間悩みました・・・

Socketを通してデータを送信するのは簡単で,sendの関数一つの呼び出しで可能です.何故詰まったかと言うと,Android側でデコードしたデータが全然違うという問題が解決しなかったためです.
ちなみに,sendは,send(SOCKET s, const char* buf, int len, int flags)の形式を取り,bufの部分で配列などを変換して送信します.今回はintの配列を送信するため,int * -> char * の変換をしました.

Android側のデコードは他でも使っていて恐らく間違っていない(というか正しかった)と考え,エンコードの際にどうすればよいのかを延々と調べては試すという繰り返しでした・・・
このやり方に関する記事は結構あるのですが,単純に(char *) (intの配列)という風に型変換すればいいと書いてあるものが多く,試しましたがダメ.さらにint * -> byte * -> char * の変換等も色々試しましたが,一向に解決せず.途方に暮れていたところで一筋の光が.

stackoverflowのこの記事によると,htonを噛ませてネットワークバイトオーダーに変換する必要があるとのこと.
最終的には以下のコードで送信できました.arrayが送信する配列,intは送信するデータ数(配列の長さと等価)です.

void btSend(int* array, int len) {
        int* netArray;
        netArray = (int*)malloc(sizeof(int)*len);
        for (int i = 0; i < len; i++) {
            netArray[i] = htonl(array[i]);
        }
        send(acceptedSocket,  (const char *)netArray, sizeof(int)*len, 0);
}

分かってれば一時間もかからずできたものに結局2日もかかってしまい,本当に通信周りの知識をつけないとまずいと感じました・・・