LoginSignup
18
12

More than 3 years have passed since last update.

Bluetooth PC-PC/Android-PC間通信(RFCOMM)

Posted at

BluetoothのプロトコルRFCOMM(RFCOMM ‐ 通信用語の基礎知識)での通信を試した。

  • クライアント-サーバ
  • PC-PC
  • Android-PC

PC-PC間 通信

  • サーバ
    • Ubuntu 18.04
    • Python 3.7.4
  • クライアント
    • Windows 10
    • Python 3.6.8

導入(Ubuntu)

sudo apt install libbluetooth-dev
pip3 install pybluez

導入(Windows)

# ref: https://github.com/pybluez/pybluez/issues/279
pip3 install PyBluez-win10

ペアリング

ペアリングはOSのUIを使って手動で行った。

Ubuntu:Settings>Bluetooth
Windows:設定>デバイス>Bluetoothとその他のデバイス

Bluetoothデバイスのアドレス(MACアドレス)はbluetoothctl(Ubuntu)かデバイスマネージャ(Windows)で分かる。

簡易コード

インタフェースはsocketとほぼ同じものらしい。

サーバサイド

import bluetooth

PORT = 1
server_sock = bluetooth.BluetoothSocket(bluetooth.RFCOMM)
server_sock.bind(('', PORT))
server_sock.listen(1) # backlog: 接続待ち受け数

client_sock, client_addrport = server_sock.accept() # blocking until connection

data = client_sock.recv(1024)
print(data) # bytes
# print(data.decode('ascii'))

クライアントサイド

import bluetooth

server_addr = '**:**:**:**:**:**' # replace here
server_port = 1

sock = bluetooth.BluetoothSocket(bluetooth.RFCOMM)
sock.connect((server_addr, server_port))

sock.send(b'Hello World') # bytes

PC-Android間 通信

Bluetooth の概要 | Android デベロッパー | Android Developers

接続先を指定するときはポートではなくUUIDを使うらしい(直接ポートを使うインタフェースはなさそう)。

サーバサイド(PC)

pybluezのサンプル(サーバクライアント)でもポートを自動にしてUUIDで接続先を決めるようになっているので、Bluetooth的にもともとこれが推奨なのかな(ポートふさがってたら困るし)。

import uuid
print(uuid.uuid4()) # ef7ce24a-a1eb-45d4-9208-f896b0ae8336
import bluetooth

server_sock = bluetooth.BluetoothSocket(bluetooth.RFCOMM)
server_sock.bind(('', bluetooth.PORT_ANY))
server_sock.listen(1) # backlog: 接続待ち受け数
# server_port = server_sock.getsockname()[1]

uuid = 'ef7ce24a-a1eb-45d4-9208-f896b0ae8336'
bluetooth.advertise_service(
    server_sock, "MyServer", service_id=uuid,
    service_classes=[ uuid, bluetooth.SERIAL_PORT_CLASS ],
    profiles=[ bluetooth.SERIAL_PORT_PROFILE ],
)

client_sock, client_addrport = server_sock.accept() # blocking until connection

data = client_sock.recv(1024)
print(data) # bytes
# print(data.decode('ascii'))

適当なUUIDを生成して、サーバ側のコードを修正した。

***site-packages/bluetooth/bluez.py", line 275, in advertise_service
    raise BluetoothError (*e.args)
bluetooth.btcommon.BluetoothError: [Errno 2] No such file or directory

pybluezのラップしてるコマンドラインインタフェースが非推奨になってるらしい。仕方ないので互換モードを有効にして、Serial Port Profileを追加する。

# vim /etc/systemd/system/dbus-org.bluez.service

# ExecStart=/usr/lib/bluetooth/bluetoothd
ExecStart=/usr/lib/bluetooth/bluetoothd -C

# sdptool add SP
# systemctl daemon-reload
# systemctl restart bluetooth
***site-packages/bluetooth/bluez.py", line 275, in advertise_service
    raise BluetoothError (*e.args)
bluetooth.btcommon.BluetoothError: [Errno 13] Permission denied

実行にroot権限が必要になってしまったみたいなので、rootのPython環境(3.6.9)にpybluezを入れて再実行したところ動いた。

クライアントサイド(Android/Java)

try-catchを分割したので長くなったが、createRfcommSocketToServiceRecordBluetoothSocketのインスタンスを取得すればjava.net.Socketとだいたい同じ。

Bluetoothの権限が必要なので、AndroidManifest.xmlに以下を追加。

<uses-permission android:name="android.permission.BLUETOOTH" />

以下のようなコードを実機で動かして(ペアリング済みの)PCと通信できることが確認できた。実際には、adapter.getBondedDevices()でペアリング済みのデバイスリスト(List<BluetoothDevice>)が手に入るのでそこから宛先を選択させる実装をするか、未ペアリングのアドレスにconnectしようとすれば自動でペアリング用のOSのUIが開くらしいので、アドレスをサーバ側に(テキストとかQRコードとかで)表示して入力させるか、みたいな感じなのかな。

final String SERVER_DEVICE_ADDRESS = "**:**:**:**:**:**";
final String SERVER_SERVICE_UUID = "ef7ce24a-a1eb-45d4-9208-f896b0ae8336";

final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter == null) {
    Log.d(TAG, "No bluetooth support.");
    return;
}

if (! adapter.isEnabled()) {
    Log.d(TAG, "Bluetooth is disabled.");

    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableBtIntent, REQUEST_CODE_ENABLE_BT);

    return; // onActivityResult省略
}

new Thread(new Runnable() {
  @Override
  public void run() {
      BluetoothDevice server = adapter.getRemoteDevice(SERVER_DEVICE_ADDRESS);
      final BluetoothSocket socket;
      try {
          socket = server.createRfcommSocketToServiceRecord(UUID.fromString(SERVER_SERVICE_UUID));
      } catch (IOException e) {
          e.printStackTrace();
          return;
      }

      try {
          socket.connect();
      } catch (IOException e) {
          e.printStackTrace();
          Log.d(TAG, "Unable to connect.");

          try {
              socket.close();
          } catch (IOException ex) {
              ex.printStackTrace();
              Log.d(TAG, "Unable to close socket.");
              return;
          }

          return;
      }

      final OutputStream os;
      try {
          os = socket.getOutputStream();
      } catch (IOException e) {
          e.printStackTrace();
          Log.d(TAG, "Unable to create output stream.");

          try {
              socket.close();
          } catch (IOException ex) {
              ex.printStackTrace();
              Log.d(TAG, "Unable to close socket.");
              return;
          }
          return;
      }

      try {
          os.write("Hello World from Android\n".getBytes(Charset.forName("ascii")));
      } catch (IOException e) {
          e.printStackTrace();
          Log.d(TAG, "Unable to write to output stream.");
      }

      try {
          os.close();
      } catch (IOException e) {
          e.printStackTrace();
          Log.d(TAG, "Unable to close output stream.");
      }

      try {
          socket.close();
      } catch (IOException e) {
          e.printStackTrace();
          Log.d(TAG, "Unable to close socket.");
      }

  }
}).run();
18
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
12