UbuntuとAndroidをbluetooth RFCOMMで通信する

2013-01-21T00:00:00+00:00 Android Java Python

まぁAndroid公式にBluetoothChatっていうのはありますけど。一応Android側はそれのソース読んでやってみたって話。ただこれはAndroidとPC(Ubuntu)をbluetooth RFCOMMを使って通信するというところになる

一応前持って行っておきますが、以下のソースはクソですので... (Threadの扱いとかダメ)。あくまでAndroidとRFCOMMにおける通信方法の手段にしかすぎないかと

でソースですけど、AndroidManifest.xmlとかはBLUETOOTH/BLUETOOTH_ADMINのpermissionが必要なくらいなので端折る。という事でActivityだけ書く

package net.kinjouj.test;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.UUID;

import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Process;
import android.content.Intent;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.Toast;

public class SampleActivity extends Activity {

    public static final String TAG = "SampleActivity";

    private static final int REQUEST_ENABLE_BT = 2;
    private static final int MESSAGE_CLIENT_ACCEPT = 1;
    private static final int MESSAGE_RECEIVE = 2;

    private static final UUID MY_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");

    private BluetoothAdapter adapter;
    private BluetoothSocket socket;
    private ArrayAdapter<String> arrays;
    private AcceptThread mAcceptThread;
    private ConnectThread mConnectThread;

    private final Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MESSAGE_CLIENT_ACCEPT:
                    if (msg.obj != null && msg.obj instanceof BluetoothSocket) {
                        if (mConnectThread != null) {
                            mConnectThread.cancel();
                            mConnectThread = null;
                        }

                        LinearLayout layout = (LinearLayout)findViewById(R.id.layout);

                        if (layout.getVisibility() == View.INVISIBLE) {
                            layout.setVisibility(View.VISIBLE);
                        }

                        BluetoothSocket socket = (BluetoothSocket)msg.obj;

                        mConnectThread = new ConnectThread(socket);
                        mConnectThread.start();
                    }

                    break;

                case MESSAGE_RECEIVE:
                    if (msg.obj != null) {
                        String text = (String)msg.obj;

                        if (!TextUtils.isEmpty(text)) {
                            arrays.add(text);
                        }
                    }

                    break;
            }
        }
    };

    @Override
    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        setContentView(R.layout.main);

        ((Button)findViewById(R.id.btn)).setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) {
                String text = ((EditText)findViewById(R.id.text)).getText().toString();

                if (TextUtils.isEmpty(text)) {
                    return;
                }

                try {
                    if (socket == null) {
                        Set<BluetoothDevice> devices = adapter.getBondedDevices();

                        if (devices.size() <= 0) {
                            return;
                        }

                        BluetoothDevice device = devices.iterator().next();
                        socket = device.createRfcommSocketToServiceRecord(MY_UUID);
                        socket.connect();
                    }

                    socket.getOutputStream().write(text.getBytes());
                } catch (IOException e) {
                    e.printStackTrace();

                    Toast.makeText(
                        SampleActivity.this,
                        String.format("connect unavailable: %s", socket.getRemoteDevice().getName()),
                        Toast.LENGTH_LONG
                    ).show();
                } finally {
                    if (socket != null) {
                        try {
                            socket.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        });

        arrays = new ArrayAdapter<String>(this, R.layout.messages);

        ((ListView)findViewById(R.id.listView)).setAdapter(arrays);
    }

    @Override
    public void onStart() {
        super.onStart();

        adapter = BluetoothAdapter.getDefaultAdapter();

        if (adapter == null) {
            finish();

            return;
        }

        if (!adapter.isEnabled()) {
            Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            startActivityForResult(intent, REQUEST_ENABLE_BT);
        } else {
            onBonding();
        }
    }

    @Override
    public void onStop() {
        super.onStop();

        if (socket != null) {
            try {
                socket.close();
                socket = null;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        if (mConnectThread != null) {
            mConnectThread.cancel();
            mConnectThread = null;
        }

        if (mAcceptThread != null) {
            mAcceptThread.cancel();
            mAcceptThread = null;
        }

        Process.killProcess(Process.myPid());
    }

    @Override
    protected void onActivityResult(int request, int result, Intent intent) {
        if (request == REQUEST_ENABLE_BT && result == RESULT_OK) {
            onBonding();
        }
    }

    private void onBonding() {
        Set<BluetoothDevice> devices = adapter.getBondedDevices();

        if (devices.size() <= 0) {
            return;
        }

        if (mAcceptThread != null) {
            mAcceptThread.cancel();
            mAcceptThread = null;
        }

        mAcceptThread = new AcceptThread(adapter);
        mAcceptThread.start();
    }

    private class AcceptThread extends Thread {

        private BluetoothServerSocket server;

        public AcceptThread(BluetoothAdapter adapter) {
            super("accept");

            try {
                server = adapter.listenUsingRfcommWithServiceRecord("RFCOMM Service", SampleActivity.MY_UUID);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void run() {
            boolean isRunnable = true;

            while (isRunnable) {
                try {
                    BluetoothSocket socket = server.accept();

                    if (socket != null) {
                        managedConnection(socket);
                    }
                } catch (IOException e) {
                    Log.v(TAG, "disconnect", e);

                    isRunnable = false;

                    cancel();
                }
            }
        }

        private void managedConnection(final BluetoothSocket socket) {
            handler.post(new Thread() {
                @Override
                public void run() {
                    BluetoothDevice device = socket.getRemoteDevice();

                    Toast.makeText(
                        SampleActivity.this,
                        String.format("Connect: %s:%s" ,device.getName(), device.getAddress()),
                        Toast.LENGTH_LONG
                    ).show();
                }
            });

            handler.obtainMessage(SampleActivity.MESSAGE_CLIENT_ACCEPT, socket).sendToTarget();
        }

        public void cancel() {
            try {
                server.close();
                server = null;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private class ConnectThread extends Thread {

        private BluetoothSocket socket;

        public ConnectThread(BluetoothSocket socket) {
            super("connect");

            this.socket = socket;
        }

        @Override
        public void run() {
            InputStream is = null;

            try {
                is = socket.getInputStream();

                try {
                    BufferedReader br = new BufferedReader(new InputStreamReader(is));
                    String str = null;

                    while ((str = br.readLine()) != null) {
                        handler.obtainMessage(SampleActivity.MESSAGE_RECEIVE, str).sendToTarget();
                    }
                } catch (IOException  e) {
                    Log.v(TAG, "disconnect", e);

                    cancel();
                } finally {
                    if (is != null) {
                        try {
                            is.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if(is != null) {
                    try {
                        is.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

        public void cancel() {
            if(socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

簡単に言うと起動するとAcceptThreadが走って、そこでBluetooth RFCOMMをリッスンする。でPC側のクライアントを使って接続するとConnectedThreadが走って、クライアントから受け取ったデータを取得してそれをListViewのアダプターにバインドする。まぁそんな感じ

でPCからデータを送るクライアントは以前書いたネタにも書いてあるけど、python-bluezを使う

from bluetooth import find_service,RFCOMM,BluetoothSocket,BluetoothError

host = None
port = 0

for service in find_service():
    if service["protocol"] == "RFCOMM" and service["name"] == "RFCOMM Service":
        host = service["host"]
        port = service["port"]

if host is not None:
    sock = BluetoothSocket(RFCOMM)
    sock.connect((host, port))

    print "Connected: %s:%d" % (host, port)

    while True:
        data = raw_input("message: ")

        if data is not None:
            try:
                sock.send("%sn" % data)
            except:
                break;

まぁこれを実行してデータを送ったりするとスマフォ側にデータが表示される。でその動画を撮影しました

{% youtube 9FjwF3B0rqo %}

というような感じ。で問題はここからで逆にスマフォ側からPC側にデータをどうやって送るのかって話なんですが。上記のJavaソース上だとcreateRfcommSocketToServiceRecordのメソッドを使ってPC側デバイスのRFCOMMと接続してごにょごにょしているのですが、そもそもPC側のbluetoothにRFCOMMがバインドされてるかって所なんですが、基本的にはRFCOMMは自分でやらないとバインドされないはずなので

sudo sdptool browse local

をやったあとに。Serial Port通信なRFCOMMがあるかどうかを確認しなきゃならん。多分、デフォルトでは無いはずなので

sudo sdptool add --channel=15 SP

で登録しておく。でもっかい見ると

Service Name: Serial Port
Service Description: COM Port
Service Provider: BlueZ
Service RecHandle: 0x10009
Service Class ID List:
  "Serial Port" (0x1101)
Protocol Descriptor List:
  "L2CAP" (0x0100)
  "RFCOMM" (0x0003)
    Channel: 15

な感じで出てるかと。あとはこっちもサーバープログラムを作って、スマフォから送られてきたデータを取得して表示したりするなりだけ。だが、上記のJavaソースだと一度PC側からクライアント接続しないと送信なフォームが出ないようになってるので、それと事前にPC側のRFCOMMレシーバーなプログラムを起動しておかないとスマフォ側からデータ送信出来ない。理由としてスマフォからPC側のサーバーへのコネクションは一度処理した一回のみだけ接続処理を行うので

でそのRFCOMMサーバープログラムも書く。ここもPython

from bluetooth import BluetoothSocket,RFCOMM
import notify2

port = 15

server = BluetoothSocket(RFCOMM)
server.bind(("", port))
server.listen(1)
conn, addr = server.accept()

notify2.init("bluetooth notify")

while True:
    data = conn.recv(4096)

    n = notify2.Notification("Bluetooth Message", data)
    n.show()

っていう感じ。今回のだと

  • PC側のRFCOMMサーバーを起動する (その前にPC側でRFCOMMなSerial Portあるか確認)
  • スマフォアプリ側を起動
  • PC側クライアントからスマフォのRFCOMMへ接続するプログラムを実行 (ここで双方的なコネクションが立つ)
  • PC側からデータを送る (スマフォアプリ側の画面に出る)
  • スマフォ側からデータを送る (PC側にinoitfyなポップアップが出る)

webrat FuelPHPをやってみる (26) - MongoDBを使う -