前書き#
Bluetooth 技術の開発を行う前に、まず Bluetooth モジュールの分類について理解しておくことができます:
現在、Bluetooth モジュールの開発は、従来の Bluetooth と低消費電力 Bluetooth の開発に分かれています。現在市場で最も人気のある IoT 技術の多くは、低消費電力 Bluetooth モジュールを選択しており、高効率で低消費電力ですが、欠点は小データ量のデータ転送のみをサポートしていることです。Android 4.3(API 18)以降に導入されました。一方、大量のデータ転送(音声や動画など)の開発には、従来の Bluetooth モジュールを使用する必要があります。開発前に Bluetooth モジュールの種類を区別することが重要です。なぜなら、2 種類の Bluetooth モジュールの開発は異なるためです。本記事では、2 種類の Bluetooth モジュールの開発手順についても紹介します。
一般的な開発手順#
Bluetooth モジュールの開発手順は以下の通りです:
2 種類の Bluetooth モジュールの開発手順はほぼ同じですが、Bluetooth 接続と Bluetooth 通信には違いがあります。
関連 API の紹介#
一、一般 API#
1.BluetoothAdapter#
ローカル Bluetooth アダプタで、Bluetooth がオンになっているかどうか、Bluetooth デバイスを検索するなどの基本操作に使用されます。
2.BluetoothDevice#
Bluetooth デバイスオブジェクトで、デバイス名、MAC アドレスなどの Bluetooth デバイスの属性を含みます。
二、従来の Bluetooth(BT)API#
1.BluetoothSocket#
Bluetooth ソケットのインターフェース(TCP ソケットに似ています。ソケットの概念については、コンピュータネットワークに関する関連内容を参照してください)。このクラスのオブジェクトは、アプリケーション内でのデータ転送の接続点として機能します。
2.BluetoothServerSocket#
サーバーソケットを表し、将来のリクエストをリスニングします(TCP ServerSocket に似ています)。2 つの Bluetooth デバイスが接続できるようにするために、1 つのデバイスはこのクラスを使用してサーバーソケットを開く必要があります。リモート Bluetooth デバイスがこのサーバーデバイスにリクエストを送信すると、接続が受け入れられた場合、BluetoothServerSocket は接続された BluetoothSocket クラスのオブジェクトを返します。
3.BluetoothClass#
Bluetooth デバイスの主要な特徴を説明します。BluetoothClass のクラスオブジェクトは、読み取り専用の Bluetooth デバイスの属性セットです。このクラスのオブジェクトは、BluetoothProfile のすべての内容やデバイスがサポートするすべてのサービス情報を信頼性を持って説明することはできませんが、このクラスのオブジェクトはデバイスのタイプを示すのに役立ちます。
4.BluetoothProfile#
Bluetooth 仕様を表し、Bluetooth 仕様は 2 つの Bluetooth デバイス間の通信の標準です。
三、低消費電力 Bluetooth(BLE)API#
1.BluetoothGatt#
Bluetooth 一般属性プロトコルで、BLE 通信の基本ルールを定義し、BluetoothProfile の実装クラスです。Gatt は Generic Attribute Profile の略で、デバイスの接続、サービスの検索などの操作に使用されます。
2.BluetoothGattCallback#
Bluetooth デバイスが接続に成功した後、いくつかの操作の結果をコールバックするために使用され、接続が成功した後にのみコールバックされます。
3.BluetoothGattService#
Bluetooth デバイスが提供するサービスで、Bluetooth デバイスの特徴の集合です。
4.BluetoothGattCharacteristic#
Bluetooth デバイスの特徴で、GATT サービスを構成する基本データ単位です。
5.BluetoothGattDescriptor#
Bluetooth デバイスの特徴記述子で、特徴の追加説明です。
1.Bluetooth 権限の追加#
(1)android.permission.BLUETOOTH#
開発したアプリケーションデバイスで Bluetooth 機能を使用するためには、Bluetooth 権限 "BLUETOOTH" を宣言する必要があります。Bluetooth 通信を行う際(接続リクエスト、接続の受け入れ、データの交換など)にこの権限が必要です。
(2)android.permission.BLUETOOTH_ADMIN#
アプリケーションが Bluetooth デバイスの検索をインスタンス化したり、Bluetooth の設定を操作する必要がある場合は、BLUETOOTH_ADMIN 権限を宣言する必要があります。ほとんどのアプリは、この権限を使用してローカル Bluetooth デバイスを検索します。この権限の他の機能は、アプリが電源管理アプリであり、Bluetooth の設定を変更する必要がない限り使用すべきではありません。
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
ここで注意が必要なのは、Google は Android 6.0 以降、ユーザーのデータセキュリティをより良く保護するために、ハードウェアのユニーク識別子にアクセスする必要があるすべての場所で位置権限を申請する必要があり、周囲の Bluetooth デバイスを検索するには、携帯電話が位置サービスを提供する必要があります。そうでなければ、Bluetooth スキャンを呼び出しても何も結果が得られません。
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
AndroidManifest.xml に位置権限を追加し、位置権限は Dangerous レベルの権限であるため、コード内で動的に申請する必要があることを忘れないでください。
2.Bluetooth がサポートされているかどうか#
//Bluetoothアダプタを取得する。アダプタがnullの場合、現在の携帯電話はBluetoothをサポートしていません。
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
if (bluetoothAdapter == null) {
Toast.makeText(this, "現在の携帯電話デバイスはBluetoothをサポートしていません", Toast.LENGTH_SHORT).show()
}
3.Bluetooth がオンになっているかどうか#
(1)Bluetooth がオンになっているかどうかを判断する#
//携帯電話デバイスがBluetoothをサポートしている場合、Bluetoothがオンになっているかどうかを判断します。
if (bluetoothAdapter!!.isEnabled) {
Toast.makeText(this, "携帯電話のBluetoothはオンになっています", Toast.LENGTH_SHORT).show()
//Bluetoothの検索操作を行うことができます。
searchBtDevice()
} else {
//Bluetoothがオフになっているため、Bluetoothをオンにします。2番目の方法でBluetoothをオンにすることをお勧めします。
//最初の方法:携帯電話のBluetoothを直接オンにし、何の提示もありません。通常はこの方法は使用しません。
// bluetoothAdapter.enable(); //BLUETOOTH_ADMIN権限
//2番目の方法:ユーザーにBluetoothをオンにするように友好的に提示します。
val enableBtIntent = Intent(ACTION_REQUEST_ENABLE)
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
}
(2)ユーザーに Bluetooth をオンにするように友好的に提示する#
//ユーザーにBluetoothをオンにするように友好的に提示します。
val enableBtIntent = Intent(ACTION_REQUEST_ENABLE)
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
(3)ユーザーが Bluetooth をオンにする操作のコールバックをリスニングする#
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_ENABLE_BT) {
if (resultCode == Activity.RESULT_OK) {
// Bluetoothがオンになりました
//Bluetoothの検索を開始できます。Bluetoothリストを取得します。
searchBtDevice()
} else {
// Bluetoothがオフになっています。一般的な操作は、ユーザーにBluetoothをオンにするかどうかを確認するダイアログを表示することです。
DialogUtil.getInstance().showConfirmDialog(this, "ヒント", "Bluetoothがオンになっていません。Bluetoothをオンにしますか?", "オンにする",
"キャンセル", object : DialogUtil.DialogConfirmListener<Any> {
override fun cancel() {
//ユーザーがキャンセルを選択した場合、現在のページを閉じるか、Bluetoothがオフになっていることを通知できます。
finish()
}
override fun confirm(data: Any?) {
//ユーザーにBluetoothをオンにするように友好的に提示し続けます。
val enableBtIntent = Intent(ACTION_REQUEST_ENABLE)
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
}
})
}
}
}
4.Bluetooth をスキャンする#
Bluetooth スキャンには 2 つの方法があります。1 つは従来の Bluetooth 開発モードに適した Bluetooth アダプタ bluetoothAdapter.startDiscovery () を呼び出して Bluetooth を検索し、Bluetooth ブロードキャストレシーバーを登録してスキャンした Bluetooth 情報を取得する方法です。この検索方法は、従来の Bluetooth でも低消費電力 Bluetooth でもすべてスキャンできます。もう 1 つの方法は、低消費電力 Bluetooth の検索スキャンに特化しており、低消費電力 Bluetooth モードの Bluetooth 情報のみがスキャンされます。
(1)従来の Bluetooth 検索#
① Bluetooth ブロードキャストレシーバーを新規作成#
public class BroadcastReceiver extends BroadcastReceiver {
private static final String TAG = "BluetoothReceiver";
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
LogUtil.logNormalMsg("----Bluetoothブロードキャストレシーバー----------action----------" + action);
//検索を開始
if (TextUtils.equals(action, BluetoothAdapter.ACTION_DISCOVERY_STARTED)) {
if (onDeviceSearchListener != null) {
onDeviceSearchListener.onDiscoveryStart(); //検索開始のコールバック
}
findConnectedBluetooth();
} else if (TextUtils.equals(action, BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) {
//検索完了
if (onDeviceSearchListener != null) {
//検索完了のコールバック
onDeviceSearchListener.onDiscoveryStop();
}
} else if (TextUtils.equals(action, BluetoothDevice.ACTION_FOUND)) {
//3.0でデバイスを検索
//Bluetoothデバイス
BluetoothDevice bluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
//信号強度
int rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE);
LogUtil.logNormalMsg("TAG", "デバイスをスキャンしました:" + bluetoothDevice.getName() + "-->" + bluetoothDevice.getAddress());
if (onDeviceSearchListener != null) {
//3.0でデバイスを検索した際のコールバック
onDeviceSearchListener.onDeviceFound(bluetoothDevice, rssi);
}
} else if (TextUtils.equals(action, BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
//Bluetoothが切断されました
//Bluetoothデバイス
BluetoothDevice bluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
LogUtil.logNormalMsg("--------------Bluetoothが切断されました---------------");
if (onDeviceSearchListener != null) {
//現在のBluetooth接続が切断されたかどうかを判断します。
if (bluetoothDevice.getAddress().equals(Content.bluetoothMac)){
onDeviceSearchListener.onDeviceDisconnect();
}
}
} else if (TextUtils.equals(action, BluetoothDevice.ACTION_ACL_CONNECTED)) {
//Bluetoothが接続されました
LogUtil.logNormalMsg("--------------Bluetoothが接続されました---------------");
if (onDeviceSearchListener != null) {
onDeviceSearchListener.onDeviceConnect();
}
} else if (TextUtils.equals(action, BluetoothDevice.ACTION_PAIRING_REQUEST)) {
//従来のBluetoothのペアリングリクエスト。ペアリングダイアログをインターセプトするために使用できます。
}
}
/**
* Bluetoothデバイス検索リスナー
* 1、検索開始
* 2、検索完了
* 3、デバイスを検索
*/
public interface OnDeviceSearchListener {
void onDiscoveryStart(); //検索開始
void onDiscoveryStop(); //検索完了
void onDeviceFound(BluetoothDevice bluetoothDevice, int rssi); //デバイスを検索
//デバイスが切断されました
void onDeviceDisconnect();
//デバイスが接続されました
void onDeviceConnect();
}
private OnDeviceSearchListener onDeviceSearchListener;
public void setOnDeviceSearchListener(OnDeviceSearchListener onDeviceSearchListener) {
this.onDeviceSearchListener = onDeviceSearchListener;
}
}
② Bluetooth ブロードキャストレシーバーを動的に登録#
/**
* Bluetoothブロードキャストを登録
*/
private fun initBtBroadcast() {
//ブロードキャストを登録
btBroadcastReceiver = BtBroadcastReceiver()
//ここで現在のActivityがBluetoothデバイス検索リスナーOnDeviceSearchListenerを実装する必要があります。
btBroadcastReceiver!!.setOnDeviceSearchListener(this)
val intentFilter = IntentFilter()
//優先度を上げる
intentFilter.priority = 1001
//アクションを追加
intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED) //スキャン開始
intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)//スキャン終了
intentFilter.addAction(BluetoothDevice.ACTION_FOUND)//デバイスを検索
intentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)//切断
intentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED)//接続
intentFilter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST)//Bluetoothペアリングリクエスト
registerReceiver(btBroadcastReceiver, intentFilter)
}
② Bluetooth をスキャン開始#
/**
* Bluetoothを検索開始
*/
private fun searchBtDevice() {
if (bluetoothAdapter!!.isDiscovering) {
//現在デバイスを検索中...現在の検索を停止
bluetoothAdapter!!.cancelDiscovery()
}
//検索を開始
bluetoothAdapter!!.startDiscovery()
}
スキャンを開始すると、Bluetooth ブロードキャストレシーバー内で携帯電話がスキャンした Bluetooth をリスニングでき、その後 Bluetooth デバイス検索リスナー OnDeviceSearchListener メソッドのコールバックで Bluetooth デバイス情報を取得できます。
override fun onDeviceFound(device: BluetoothDevice?, rssi: Int) {
//Bluetooth情報がスキャンされるたびにこのメソッドがコールバックされます。ここでBluetoothデバイス情報と信号強度を取得できます。
}
ここまで来ると、開発者はスキャンした Bluetooth デバイス情報リストを表示し、リストをクリックして Bluetooth 接続操作を行うことができます。
(2)低消費電力 Bluetooth 検索#
startLeScan () メソッドを使用してスキャンを開始し、スキャン結果は BluetoothAdapter.LeScanCallback または ScanCallback で直接コールバック処理されます。従来の Bluetooth モードでは、スキャンした Bluetooth 情報を取得するためにブロードキャストを登録する必要はありません。ここで注意が必要なのは、異なるバージョンで startScan メソッドのロジックが異なることです。現在、ほとんどの Android バージョンは 5.0 以上であり、基本的には後者のスキャン方法を選択すれば問題ありません。
//1. Android 4.3以上、Android 5.0未満
mBluetoothAdapter.startLeScan(BluetoothAdapter.LeScanCallback LeScanCallback);
//2. Android 5.0以上、スキャン結果はmScanCallbackで処理されます
mBluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner();
mBluetoothLeScanner.startScan(ScanCallback mScanCallback);
コールバック処理:
public abstract class ScanCallback {
/**
* BLE広告が見つかったときのコールバック。
*
* @param callbackType このコールバックがトリガーされた方法を決定します。{@link
* ScanSettings#CALLBACK_TYPE_ALL_MATCHES}、{@link ScanSettings#CALLBACK_TYPE_FIRST_MATCH}または
* {@link ScanSettings#CALLBACK_TYPE_MATCH_LOST}のいずれかです。
* @param result Bluetooth LEスキャン結果。
*/
public void onScanResult(int callbackType, ScanResult result) {
//ここでBluetoothスキャン結果を処理できます。
BluetoothDevice bluetoothDevice = result.getDevice();
}
/**
* バッチ結果が配信されたときのコールバック。
*
* @param results 以前にスキャンされたスキャン結果のリスト。
*/
public void onBatchScanResults(List<ScanResult> results) {
}
/**
* スキャンを開始できなかったときのコールバック。
*
* @param errorCode スキャン失敗のエラーコード(SCANN_FAILED_*のいずれか)。
*/
public void onScanFailed(int errorCode) {
}
}
低消費電力 Bluetooth の技術開発に関わる場合は、スキャン Bluetooth の 2 番目の方法を選択すれば簡単です。逆に、最初の方法を選択します。
5.Bluetooth 接続と切断#
(1)従来の Bluetooth(BT)#
アプリと従来の Bluetooth デバイスの接続は、通常 BluetoothDevice オブジェクトの createRfcommSocketToServiceRecord メソッドを使用して BluetoothSocket を取得し、ソケットプログラミングに似ています。connect () メソッドを呼び出して接続を開始し、接続が成功した後、BluetoothSocket の getInputStream () と getOutputStream () メソッドを使用して入力ストリームと出力ストリームを取得し、Bluetooth と通信します。createRfcommSocketToServiceRecord メソッドには UUID パラメータを渡す必要があります。通常、接続する Bluetooth デバイスのパラメータから知ることができます。UUID を取得できなくても問題ありません。リフレクションメカニズムを使用して BluetoothSocket オブジェクトを取得し、接続を行うことができます。ここで注意が必要なのは、Bluetooth 接続は時間がかかるプロセスであり、メインスレッドで実行できないことです。
① createRfcommSocketToServiceRecord で BluetoothSocket を取得#
//1、BluetoothSocketを取得
try {
//安全なBluetooth接続を確立し、ペアリングダイアログが表示されます。
mSocket = mDevice.createRfcommSocketToServiceRecord(UUID.fromString(uuid));
} catch (IOException e) {
e.printStackTrace();
LogUtil.logNormalMsg(TAG, "BluetoothSocketを取得中に異常が発生しました!" + e.getMessage());
}
② リフレクションで BluetoothSocket を取得#
//1、BluetoothSocketを取得
try {
//リフレクションを使用してソケットを取得
mSocket = (BluetoothSocket) mDevice.getClass().
getMethod("createRfcommSocket", new Class[]{int.class})
.invoke(mDevice, 1);
} catch (IOException e) {
e.printStackTrace();
LogUtil.logNormalMsg(TAG, "BluetoothSocketを取得中に異常が発生しました!" + e.getMessage());
}
③ Bluetooth を接続する#
BluetoothSocket を取得した後、Bluetooth デバイスに接続できます。connect () 接続メソッドを呼び出すと、通常、携帯電話に Bluetooth ペアリングダイアログが表示されます。一部の Bluetooth デバイスでは、ペアリングコードが設定されていないため、ダイアログが表示されません。ここでは、ペアリングコードがある Bluetooth モジュールにのみ該当します。手動でペアリングコードを入力すると、接続が成功します。一般的に Bluetooth 開発デバイスのペアリングコードは 0000 または 1234 です。ユーザーエクスペリエンスを考慮すると、通常はユーザーにペアリングコードを手動で入力させないようにします。この操作は冗長で面倒に見えるため、アプリ開発時には通常、接続時に Bluetooth のペアリングコードを静かに設定することを考慮します。ユーザーが気づかないうちに接続とペアリング操作を行います。この操作を行うには、開発者が Bluetooth ペアリングの Action(BluetoothDevice.ACTION_PAIRING_REQUEST)をインターセプトし、コード内でペアリング処理を行う必要があります。BroadcastReceiver Bluetooth ブロードキャストレシーバーに Bluetooth ペアリングリクエストのインターセプト処理を追加します。
mSocket.connect();
④ ペアリングコードを静かに設定する#
if (TextUtils.equals(action, BluetoothDevice.ACTION_PAIRING_REQUEST)) {
BluetoothDevice btDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
//Bluetoothペアリング
Log.i(TAG, "##### ペアリングリクエストです ######");
Bundle extras = intent.getExtras();
Log.i(TAG, "-->" + extras.toString());
Object device = extras.get("android.bluetooth.device.extra.DEVICE");
Object pairKey = BaseData.bluetoothPassword;
Log.i(TAG, "device-->" + String.valueOf(device));
Log.i(TAG, "pairkey-->" + String.valueOf(pairKey));
try {
//ペアリングブロードキャストを中断します。ブロードキャストを中断しないと、一瞬のペアリングダイアログが表示されます。
abortBroadcast();
//setPinメソッドを呼び出してペアリングを行います...
setPin(btDevice.getClass(), btDevice, String.valueOf(pairKey));
} catch (Exception e) {
e.printStackTrace();
}
}
動的に Bluetooth ブロードキャストレシーバーを登録する際に、Bluetooth ペアリングリクエストの Action を追加することを忘れないでください。
//BluetoothペアリングリクエストAction
intentFilter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST)
ペアリングコードを設定する
/**
* Bluetoothペアリングコードを設定
* @param btClass btClass
* @param btDevice BluetoothDevice
* @param str str
* @return boolean
*/
public boolean setPin(Class btClass, BluetoothDevice btDevice, String str) {
boolean flag = false;
try {
Class[] arrayOfClass = new Class[1];
arrayOfClass[0] = byte[].class;
Method removeBondMethod = btClass.getDeclaredMethod("setPin", arrayOfClass);
Object[] arrayOfObject = new Object[1];
arrayOfObject[0] = str.getBytes();
flag = (Boolean) removeBondMethod.invoke(btDevice, arrayOfObject);
LogUtil.logNormalMsg(TAG, "setPinの結果: " + flag);
} catch (Exception e) {
e.printStackTrace();
}
if (flag) {
LogUtil.logNormalMsg("-------Bluetoothペアリングコードの設定に成功しました");
} else {
LogUtil.logNormalMsg("-------Bluetoothペアリングコードの設定に失敗しました");
}
return flag;
}
ここで、私自身の Bluetooth 接続コードを再掲します:
public class ConnectThread {
private static final String TAG = "ConnectThread";
private final BluetoothAdapter mBluetoothAdapter;
private BluetoothSocket mmSocket;
private final BluetoothDevice mDevice;
public ConnectThread(BluetoothAdapter bluetoothAdapter, BluetoothDevice bluetoothDevice, String uuid) {
this.mBluetoothAdapter = bluetoothAdapter;
this.mDevice = bluetoothDevice;
//一時変数を使用し、後でmmSocketに割り当てます。
//mmSocketは静的です。
BluetoothSocket tmp = null;
if (mmSocket != null) {
LogUtil.logNormalMsg(TAG, "ConnectThread-->mmSocket != null、先に解放します");
try {
mmSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
LogUtil.logNormalMsg(TAG, "ConnectThread-->mmSocket != null、すでに解放されました");
//1、BluetoothSocketを取得
try {
//安全なBluetooth接続を確立し、ペアリングダイアログが表示されます。
tmp = mDevice.createRfcommSocketToServiceRecord(UUID.fromString(uuid));
} catch (IOException e) {
e.printStackTrace();
LogUtil.logNormalMsg(TAG, "ConnectThread-->BluetoothSocketを取得中に異常が発生しました!" + e.getMessage());
}
mmSocket = tmp;
if (mmSocket != null) {
LogUtil.logNormalMsg(TAG, "ConnectThread-->BluetoothSocketを取得しました");
}
}
public void connectBluetooth() {
connect();
}
private void connect() {
//接続前にデバイスの発見をキャンセルします。そうしないと、接続試行の速度が大幅に低下し、接続失敗の可能性が高まります。
if (mBluetoothAdapter == null) {
LogUtil.logNormalMsg(TAG, "ConnectThread:run-->mBluetoothAdapter == null");
return;
}
//デバイスの発見をキャンセル
if (mBluetoothAdapter.isDiscovering()) {
mBluetoothAdapter.cancelDiscovery();
}
if (mmSocket == null) {
LogUtil.logNormalMsg(TAG, "ConnectThread:run-->mmSocket == null");
return;
}
boolean firstFlag = false;
//2、ソケットを介してデバイスに接続
try {
LogUtil.logNormalMsg(TAG, "ConnectThread:run-->接続を試みています...");
if (onBluetoothConnectListener != null) {
onBluetoothConnectListener.onStartConn(); //接続開始のコールバック
}
mmSocket.connect();
firstFlag = true;
} catch (Exception e) {
LogUtil.logNormalMsg(TAG, "ConnectThread:run-->最初の接続中に異常が発生しました!" + e.getMessage());
boolean flag = false;
try {
LogUtil.logNormalMsg(TAG, "ConnectThread:run-->リフレクションを介してソケット接続を再試行しています");
mmSocket = (BluetoothSocket) mDevice.getClass().
getMethod("createRfcommSocket", new Class[]{int.class})
.invoke(mDevice, 1);
mmSocket.connect();
flag = true;
Log.e(TAG, "mmSocket.isConnected():" + mmSocket.isConnected());
} catch (Exception e2) {
LogUtil.logNormalMsg("mmSocket接続失敗:" + e.getMessage());
e2.printStackTrace();
LogUtil.logNormalMsg(TAG, "ConnectThread:run-->接続中に異常が発生しました2!" + e2.getMessage());
if (onBluetoothConnectListener != null) {
onBluetoothConnectListener.onConnFailure("接続中に異常が発生しました:" + e.getMessage());
}
//リリース
cancel();
} finally {
if (flag) {
if (onBluetoothConnectListener != null) {
//接続成功のコールバック
onBluetoothConnectListener.onConnSuccess(mmSocket);
LogUtil.logNormalMsg(TAG, "ConnectThread:run-->接続成功");
}
} else {
if (onBluetoothConnectListener != null) {
//接続失敗のコールバック
onBluetoothConnectListener.onConnFailure("");
}
}
}
} finally {
if (firstFlag) {
if (onBluetoothConnectListener != null) {
//接続成功のコールバック
onBluetoothConnectListener.onConnSuccess(mmSocket);
LogUtil.logNormalMsg(TAG, "ConnectThread:run-->接続成功");
}
}
}
}
/**
* リリース
*/
public void cancel() {
try {
if (mmSocket != null && mmSocket.isConnected()) {
LogUtil.logNormalMsg(TAG, "ConnectThread:cancel-->mmSocket.isConnected() = " + mmSocket.isConnected());
mmSocket.close();
mmSocket = null;
return;
}
if (mmSocket != null) {
mmSocket.close();
mmSocket = null;
}
LogUtil.logNormalMsg(TAG, "ConnectThread:cancel-->接続されたソケットを閉じてリソースを解放しました");
} catch (IOException e) {
LogUtil.logNormalMsg(TAG, "ConnectThread:cancel-->接続されたソケットを閉じてリソースを解放中に異常が発生しました!" + e.getMessage());
}
}
private OnBluetoothConnectListener onBluetoothConnectListener;
public void setOnBluetoothConnectListener(OnBluetoothConnectListener onBluetoothConnectListener) {
this.onBluetoothConnectListener = onBluetoothConnectListener;
}
//接続状態リスナー
public interface OnBluetoothConnectListener {
void onStartConn(); //接続開始
void onConnSuccess(BluetoothSocket bluetoothSocket); //接続成功
void onConnFailure(String errorMsg); //接続失敗
}
}
Bluetooth 接続が成功した後、BluetoothSocket の入力出力ストリームを介して Bluetooth デバイスと通信できます。
注意が必要なのは、静かに Bluetooth ペアリングコードを設定するには、Bluetooth デバイスメーカーと固定のペアリングコードを協議する必要があることです。そうでないと、Bluetooth ペアリングコードが常に変わると、アプリが Bluetooth に接続できなくなり、エラーメッセージが表示されません。
⑤ 接続を切断する#
従来の Bluetooth 接続を切断することは、BluetoothSocket と入力出力ストリームを閉じることです。ここで注意が必要なのは、入力出力ストリームを閉じてから BluetoothSocket を閉じる必要があることです。
try {
if(mInStream != null){
mInStream.close(); //入力ストリームを閉じる
}
if(mOutStream != null){
mOutStream.close(); //出力ストリームを閉じる
}
if(mSocket != null){
mSocket.close(); //ソケットを閉じる
}
mInStream = null;
mOutStream = null;
mmSocket = null;
LogUtil.logNormalMsg(TAG,"接続を正常に切断しました");
} catch (Exception e) {
e.printStackTrace();
//どの部分でもエラーが発生した場合、ソケット接続を強制的に閉じます。
mInStream = null;
mOutStream = null;
mmSocket = null;
LogUtil.logNormalMsg(TAG, "接続を切断中に異常が発生しました!" + e.getMessage());
}
(2)低消費電力 Bluetooth(BLE)#
低消費電力 Bluetooth の接続は少し複雑です:
接続前#
Bluetooth 接続を行う前に、GATT について理解しておく必要があります。これにより、低消費電力 Bluetooth 開発技術の理解が深まります。
GATT 階層構造#
この階層構造の最上位はプロファイルです。プロファイルは、ユースケースに必要な 1 つ以上のサービスで構成されています。サービスは、他のサービスの特徴または参照で構成されています。各特徴は値を含み、オプションの情報を含むことができます。サービス、特徴、および特徴の構成要素(すなわち、値と記述子)は、プロファイルデータを含み、すべてサーバーの属性に保存されます。
各メンバーの役割#
1、Profile
Profile 仕様は、プロファイルデータを交換するためのアーキテクチャを規定します。このアーキテクチャは、サービスや特徴など、プロファイルで使用される基本要素を定義します。この階層の最上位はプロファイルです。プロファイルは、ユースケースを実現するために必要な 1 つ以上のサービスで構成されています。サービスは、特徴または他のサービスに関する参照で構成されています。各特徴は値を含み、オプションの情報を含むことがあります。サービス、特徴、および特徴の構成要素(すなわち、特徴値と特徴記述子)は、プロファイルデータを構成し、すべてサーバーの属性に保存されます。そうです、上記は引用です。簡単に言えば、プロトコル仕様であり、すべての Bluetooth 互換性はこの仕様に依存しています。標準の Profile を使用すれば、過度に気にする必要はありません。
2、Service
サービスは異なるデータ処理を担当します。たとえば、バッテリー残量や心拍数などがあり、具体的なデータのやり取りには関与せず、機能分割の役割を担います。各サービスには UUID があり、区別されます。
3、Characteristic
特徴は、データのやり取りに直接関与するメンバーの 1 つです。特徴にも UUID があり、UUID を介して特徴を取得し、特徴内の値を変更して通信を実現します。この通信モードはオブザーバーパターンに似ており、値が変更されると、その特徴をリスニングしているオブザーバーに通知され、データが変更されたことを知らせます。オブザーバーがデータを取得することで、通信が完了します。具体的な実装は後述します。
4、Descriptor
特徴値の説明であり、特徴値の定義された属性を定義するために使用されます。たとえば、記述子は、読み取り可能な説明、特徴値の受け入れ範囲、または特徴値の特定の測定単位を指定できます。このものはあまり使用されません。もし中心デバイスがデータを受信できない場合は、Descriptor が通知可能に設定されていないかどうかを確認してください。
5、Advertising
ブロードキャストは、周辺デバイスが自分自身を発見可能にする手段であり、ブロードキャストを発信しなければ、BLE スキャンで見つけることはできません。一度デバイスが接続されると、ブロードキャストは停止します。
低消費電力 Bluetooth は BluetoothGatt を介して接続および管理され、BluetoothDevice の connectGatt () メソッドを使用して BluetoothGatt オブジェクトを取得できます。この際、3 つのパラメータ(context コンテキスト、autoConnect 自動接続、callback 接続コールバック)を渡す必要があります。ここで重要なのは、3 番目のパラメータ BluetoothGattCallback です。開発者は BluetoothGattCallback コールバック内でサービスを発見し、通信パイプラインを構成する必要があり、同時に下位機(Bluetooth モジュール)からの返信メッセージをリスニングできます。ここで注意が必要なのは、GATT 内のサービス Service、特徴 Characteristic、および特徴値記述 Descriptor を取得するには UUID を指定する必要があり、これらの UUID は通常 Bluetooth モジュールメーカーから取得されます。コードを介してすべての UUID を遍歴して正しい UUID を実験的に見つけることができます。後で説明します。
connectGatt () メソッドのソースコード:
/**
* このデバイスがホストするGATTサーバーに接続します。呼び出し元はGATTクライアントとして機能します。
* コールバックは、接続状態やその他のGATTクライアント操作の結果を呼び出し元に配信するために使用されます。
* メソッドはBluetoothGattインスタンスを返します。BluetoothGattを使用してGATTクライアント操作を実行できます。
*
* @param callback 非同期コールバックを受け取るGATTコールバックハンドラー。
* @param autoConnect リモートデバイスに直接接続するか(false)、リモートデバイスが利用可能になると自動的に接続するか(true)。
* @throws IllegalArgumentException コールバックがnullの場合
*/
public BluetoothGatt connectGatt(Context context, boolean autoConnect,
BluetoothGattCallback callback) {
return (connectGatt(context, autoConnect, callback, TRANSPORT_AUTO));
}
① カスタムコールバックを設定する#
/**
* 低消費電力Bluetooth接続コールバック
* Bluetoothデータを受信
*/
inner class MyBluetoothGattCallback : BluetoothGattCallback() {
override fun onConnectionStateChange(
gatt: BluetoothGatt,
status: Int,
newState: Int
) {
super.onConnectionStateChange(gatt, status, newState)
Content.isConnectBluetoothNow = false
//接続成功、サービスの発見を開始
when {
newState == BluetoothAdapter.STATE_CONNECTED -> {
//ここでgattはすでに接続されているので、gattのサービスを発見することができます。
mGatt?.discoverServices()
}
newState == BluetoothAdapter.STATE_DISCONNECTED -> {
//接続が切断されました
}
status == 133 -> {
//接続失敗
}
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
super.onServicesDiscovered(gatt, status)
if (status == BluetoothGatt.GATT_SUCCESS) {
//ここでgattのすべてのサービスが発見されました。gatt.servicesを介して取得できます。
//gattに関連するすべてのUUIDがわかっている場合、通信パイプラインを構成できます。
//書き込み用の特徴値を取得
writeCharact = this.bluetoothGattService?.getCharacteristic(writeUUID)
//通知リスニング用の特徴値を取得。これは読み取りリスニングに相当し、リスニングを開始すると、下位機からのメッセージをリアルタイムでリスニングできます。
notifyCharact = this.bluetoothGattService?.getCharacteristic(notifyUUID)
//リスニングを開始し、下位機からのメッセージをリアルタイムでリスニングします。
//1. 特徴値通知を設定
mGatt?.setCharacteristicNotification(notifyCharact , true)
//2. 記述子を取得
val descriptor: BluetoothGattDescriptor = notifyCharact ?.getDescriptor(descriptorUUID)!!
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
//3. 記述子を設定。ここでの設定結果はonDescriptorWriteメソッドでコールバックされます。
mGatt?.writeDescriptor(descriptor)
//これで低消費電力Bluetoothの接続と構成が完了しました。
}
}
override fun onCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
super.onCharacteristicRead(gatt, characteristic, status)
}
override fun onCharacteristicWrite(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
super.onCharacteristicWrite(gatt, characteristic, status)
val value = characteristic.value
val data = Util.bytesToAscii(value)
//データ送信成功後にこのメソッドがコールバックされます。
LogUtil.logNormalMsg("onCharacteristicWrite", "送信成功:$data")
}
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
super.onCharacteristicChanged(gatt, characteristic)
// valueはデバイスから送信されたデータで、データプロトコルに従って解析します。
//データを受信して処理します。
handBluetoothData(characteristic.value)
}
override fun onDescriptorRead(
gatt: BluetoothGatt,
descriptor: BluetoothGattDescriptor,
status: Int
) {
super.onDescriptorRead(gatt, descriptor, status)
}
override fun onDescriptorWrite(
gatt: BluetoothGatt,
descriptor: BluetoothGattDescriptor,
status: Int
) {
super.onDescriptorWrite(gatt, descriptor, status)
if (status == BluetoothGatt.GATT_SUCCESS) {
LogUtil.logNormalMsg("LockBluetoothService", "Descriptorの設定に成功しました。データを送信できます。")
//一部のBluetoothモジュールは、接続成功後にMTUを設定する必要があります。開発に応じて構成します。
mGatt?.requestMtu(103)
}
}
}
② Bluetooth 接続を開始する#
Bluetooth 接続を開始した後、connectGatt メソッドを使用して BluetoothGatt オブジェクトを取得し、接続結果は前のステップでカスタムした BluetoothGattCallback 内でコールバック処理されます。
mGatt = bluetoothDevice1!!.connectGatt(appContext, false, bluetoothGattCallback)
③ GATT サービスを発見する#
この操作は接続成功後に実行する必要があり、サービスを発見した後に Bluetooth 通信を構成できます。
mGatt?.discoverServices()
④ 通信を構成する#
BluetoothGattService サービスと読み書き用の BluetoothGattCharacteristic 特徴値を取得し、Bluetooth 通信を構成します。
//書き込み用の特徴値を取得
writeCharact = this.bluetoothGattService?.getCharacteristic(writeUUID)
//通知リスニング用の特徴値を取得。これは読み取りリスニングに相当し、リスニングを開始すると、下位機からのメッセージをリアルタイムでリスニングできます。
notifyCharact = this.bluetoothGattService?.getCharacteristic(notifyUUID)
//リスニングを開始し、下位機からのメッセージをリアルタイムでリスニングします。
//1. 特徴値通知を設定
mGatt?.setCharacteristicNotification(notifyCharact , true)
//2. 記述子を取得
val descriptor: BluetoothGattDescriptor = notifyCharact ?.getDescriptor(descriptorUUID)!!
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
//3. 記述子を設定。ここでの設定結果はonDescriptorWriteメソッドでコールバックされます。
mGatt?.writeDescriptor(descriptor)
//これで低消費電力Bluetoothの接続と構成が完了しました。通信が可能です。
一般的に Bluetooth 開発メーカーは関連する UUID を通知します。Bluetooth モジュールの各 UUID がわからない場合は、Gatt のすべてのサービスを遍歴して正しい UUID を見つけることができます。
/**
* 低消費電力Bluetooth UUIDを検索
*/
private fun findUUID(gatt: BluetoothGatt) {
LogUtil.logNormalMsg("gatt.getServices().size():" + gatt.services.size)
if (gatt.services != null && gatt.services.size > 0) {
//すべてのサービスを遍歴
for (i in gatt.services.indices) {
val service = gatt.services[i]
LogUtil.logNormalMsg("---------------------------------------------")
//現在のサービスのUUIDを出力
LogUtil.logNormalMsg("service.getUuid=" + service.uuid)
//現在のサービスに特徴値があるかどうかを判断します。
if (service.characteristics != null && service.characteristics.size > 0) { //現在のすべてのサービスの特徴値を遍歴
for (j in service.characteristics.indices) {
val characteristic =
service.characteristics[j]
//現在の特徴値のUUIDを出力
LogUtil.logNormalMsg("characteristic.getUuid=" + characteristic.uuid)
//特徴値の属性を取得
val charaProp = characteristic.properties
//読み取りをサポートしているかどうか
if (charaProp or BluetoothGattCharacteristic.PROPERTY_READ > 0) {
LogUtil.logNormalMsg("-------type:PROPERTY_READ")
}
//書き込みをサポートしているかどうか
if (charaProp or BluetoothGattCharacteristic.PROPERTY_WRITE > 0) {
LogUtil.logNormalMsg("-------type:PROPERTY_WRITE")
}
//リスニングをサポートしているかどうか
if (charaProp or BluetoothGattCharacteristic.PROPERTY_NOTIFY > 0) {
LogUtil.logNormalMsg("-------type:PROPERTY_NOTIFY")
}
if (characteristic.descriptors != null && characteristic.descriptors.size > 0) {
for (descriptor in characteristic.descriptors) {
LogUtil.logNormalMsg("descriptor.getUuid()=" + descriptor.uuid)
}
}
}
}
}
}
}
ここで注意が必要なのは、すべてのサービスを網羅的に探す必要があることです。一部のサービスには特徴がなく、一部の特徴は読み書きをサポートしておらず、一部の特徴には記述子がないため、読み取りリスニングを開始できません。
⑤ 接続を切断する#
mGatt?.let {
//BluetoothGatt接続を切断
it.disconnect()
//BluetoothGattを閉じる
it.close()
}
6. 通信#
(1)従来の Bluetooth 通信#
5 で述べた接続ロジックに基づいて、BluetoothSocket を取得します。これはソケットプログラミングに似ており、BluetoothSocket の入力出力ストリームを介して Bluetooth モジュールとデータ通信を行います。ここでは、通信操作を別スレッドで行うことをお勧めします。InputStream 入力ストリーム内にデータが読み取れるかどうかをリアルタイムでリスニングする必要があります。
① 入力出力ストリームを取得#
//InputStreamとOutputStreamを取得
try {
mInStream = socket.getInputStream();
mOutStream = socket.getOutputStream();
} catch (IOException e) {
e.printStackTrace();
LogUtil.logNormalMsg(TAG,"InputStreamとOutputStreamを取得中に異常が発生しました!");
}
② データを送信#
//データを送信
public boolean write(byte[] bytes){
try {
if(mOutStream == null){
LogUtil.logNormalMsg(TAG, "mmOutStream == null");
return false;
}
//データを送信
mOutStream.write(bytes);
mOutStream.flush();
Log.d(TAG, "書き込み成功:"+ bytes2HexString(bytes, bytes.length));
return true;
} catch (IOException e) {
LogUtil.logNormalMsg("データ送信中にエラーが発生しました:"+e.getMessage());
e.printStackTrace();
return false;
}
}
③ データを読み取る#
//スレッドのrunメソッド
@Override
public void run(){
//最大バッファサイズ、ストリームを保存
byte[] buffer = new byte[1024 * 2]; //バッファはストリームのために保存します
//ストリームのread()メソッドから読み取ったバイト数
int bytes = 0; //read()から返されたバイト数
//例外が発生するまで入力ストリームを継続的にリスニング
while(!isStop){
try {
if(mInStream == null){
LogUtil.logNormalMsg(TAG,"ConnectedThread:run-->入力ストリームmmInStream == null");
break;
}
//まずデータがあるかどうかを判断し、データがあれば読み取ります。
if(mInStream.available() != 0){
//(mmInStream)入力ストリームから(内容を読み取る)一定数のバイトを読み取り、それらをバッファ配列に保存します。bytesは実際に読み取ったバイト数です。
bytes = mInStream.read(buffer);
LogUtil.logNormalMsg(TAG,"読み取ったデータの長さ:"+bytes);
//実際に読み取ったデータ内容を保存
byte[] b = Arrays.copyOf(buffer,bytes);
//Bluetoothデータを処理します。
handBluetoothData(b);
}
Thread.sleep(150);
} catch (Exception e) {
LogUtil.logNormalMsg(TAG,"メッセージ受信中に異常が発生しました!" + e.getMessage());
//ストリームとソケットを閉じます。
boolean isClose = cancel();
if(isClose){
LogUtil.logNormalMsg(TAG,"メッセージ受信中に異常が発生し、接続を正常に切断しました!");
}
break;
}
}
//ストリームとソケットを閉じます。
boolean isClose = cancel();
if(isClose){
Log.d(TAG,"メッセージ受信が終了し、接続を切断しました!");
}
}
(2)低消費電力 Bluetooth 通信#
低消費電力 Bluetooth 通信は比較的簡単で、書き込む特徴値 Characteristic を介してデータを送信できます。一方、データを読み取る際は BluetoothGattCallback の onCharacteristicChanged コールバックメソッド内で、Bluetooth デバイスから送信されたデータをリスニングします。
① データを送信#
fun sendMsgToBluetooth(msg: ByteArray): Boolean {
if (writeCharact != null) {
//特徴値のvalueを設定
writeCharact?.value = msg
//特徴値の書き込みタイプを設定。ここでは必要に応じてタイプを選択します。詳細はソースコードを参照してください。
writeCharact?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
//gattを介してデータを書き込みます。
return mGatt!!.writeCharacteristic(writeCharact)
}
return false
}
② データを読み取る#
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
super.onCharacteristicChanged(gatt, characteristic)
//受信したデータを処理します。
handBluetoothData(characteristic.value)
}
7. 補足#
(1)多くのデバッグ中に、従来の Bluetooth モード開発では、Bluetooth デバイスをスキャンできない 2 つの状況が発生することがわかりました:
- アプリ内で Bluetooth 接続を切断し、再度 Bluetooth をスキャンするか、アプリを再起動して再度スキャンする。
- アプリを再起動する際、Bluetooth デバイスがすでに携帯電話アプリとペアリング接続されている。
このような状況が発生した場合は、慌てずに、携帯電話に接続されている Bluetooth デバイスを見つけてスキャンした Bluetooth リストに追加し、再度 Bluetooth 接続をクリックすれば解決できます。
すでに携帯電話に接続されている Bluetooth を検索:
/**
* すでに接続されているBluetoothを検索
*/
private void findConnectedBluetooth() {
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
Class<BluetoothAdapter> bluetoothAdapterClass = BluetoothAdapter.class;//BluetoothAdapterのClassオブジェクトを取得
try {//接続状態を取得するメソッド
Method method = bluetoothAdapterClass.getDeclaredMethod("getConnectionState", (Class[]) null);
//権限を開く
method.setAccessible(true);
int state = (int) method.invoke(adapter, (Object[]) null);
if(state == BluetoothAdapter.STATE_CONNECTED){
LogUtil.logNormalMsg("BLUETOOTH","BluetoothAdapter.STATE_CONNECTED");
Set<BluetoothDevice> devices = adapter.getBondedDevices();
LogUtil.logNormalMsg("BLUETOOTH","devices:"+devices.size());
for(BluetoothDevice device : devices){
Method isConnectedMethod = BluetoothDevice.class.getDeclaredMethod("isConnected", (Class[]) null);
method.setAccessible(true);
boolean isConnected = (boolean) isConnectedMethod.invoke(device, (Object[]) null);
if(isConnected){
//接続されているBluetoothデバイスを見つけ、次の処理を行います。
if (device != null) {
onDeviceSearchListener.onDeviceFound(device, 50);
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
(2)低消費電力 Bluetooth GATT のデフォルト MTU は、送信可能なデータ量が 23 バイト(byte)です。GATT の opcode が 1 バイト、GATT の handle が 2 バイトを除くと、残りの 20 バイトが GATT に留保されます。GATT はデフォルトで最大 512 バイトのデータ通信をサポートしており、MTU のサイズを設定することで変更できます。通常、Bluetooth 接続成功後に設定します。ここでは onDescriptorWrite コールバックメソッド内で設定することをお勧めします:
override fun onDescriptorWrite(
gatt: BluetoothGatt,
descriptor: BluetoothGattDescriptor,
status: Int
) {
super.onDescriptorWrite(gatt, descriptor, status)
if (status == BluetoothGatt.GATT_SUCCESS) {
//Bluetooth接続構成通信成功。MTUを設定できます。
mGatt?.requestMtu(103)
}
}
MTU 設定の結果は onMtuChanged で得られます。
override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) {
super.onMtuChanged(gatt, mtu, status)
if (status == BluetoothGatt.GATT_SUCCESS) {
//MTU設定成功。現在サポートされているMTUサイズを返します。
this.supportedMTU = mtu
}
}
参考#
Android BLE Bluetooth 開発の詳細
Android の観点から見た従来の Bluetooth と低消費電力(BLE)Bluetooth の違い
Android の従来の Bluetooth 接続
Android Bluetooth BLE 開発の詳細