BluetoothGatt — BLE接続上でGATT(Generic Attribute Profile)クライアント操作のためのAPIを提供するAndroidクラスです。BluetoothGattはリモートGATTサーバー(周辺BLEデバイス)への接続をカプセル化し、サービス検出、特性の読み書き、通知と表示の購読など、すべてのプロファイル操作を管理します。BluetoothGattインスタンスはBluetoothDevice.connectGatt()とBluetoothGattCallbackを介して取得されます。Android Developers、2026年によると、BluetoothGattは双方向BLE通信の中心的なクラスであり、BLE 4.0からBLE 5.4までのGATT操作をサポートしています。
重要ポイント
BluetoothGatt — Androidデバイス(セントラル)とBLE周辺機器(サーバー)間のGATT接続を表すプロキシオブジェクトです。各BluetoothGattインスタンスは1つのアクティブなBLE接続に対応します。すべてのGATTプロファイル操作(検出、読み取り、書き込み、通知)はこれを介して実行されます。BluetoothGattは直接作成されるのではなく、BluetoothDevice.connectGatt()メソッドによって返されます。
BluetoothGattの作成には4つのパラメータが必要です。Context — アプリケーションコンテキスト(ActivityまたはApplication)。autoConnect — falseの場合、Androidはすぐに直接接続を開始します。trueの場合、Androidはデバイスが検出されたときに自動的に接続します(バックグラウンド接続に便利)。BluetoothGattCallback — すべてのGATTイベントの必須コールバック。transport — BluetoothDevice.TRANSPORT_LE(BLE)またはTRANSPORT_BREDR(Classic)。BLEデバイスでは常にTRANSPORT_LEを使用してください。
BluetoothGattのライフサイクルは5つの状態で構成されます。DISCONNECTED — 初期状態。CONNECTING — connectGatt呼び出し後、確認前。CONNECTED — STATE_CONNECTEDでonConnectionStateChange後。接続後、GATT階層を取得するためにdiscoverServices()が呼び出されます。作業終了後 — disconnect()とclose()でシステムリソースを解放します。close()を呼び出さないと、アプリケーションがAndroidのBLE接続制限(通常4〜8)を使い果たす可能性があります。
// BluetoothGatt接続を作成中
import android.bluetooth.*
class GattConnector(private val context: Context) {
private var bluetoothGatt: BluetoothGatt? = null
fun connect(device: BluetoothDevice): BluetoothGatt? {
// 既存の接続があれば閉じる
close()
bluetoothGatt = device.connectGatt(
context,
false, // autoConnect = false ( )
object : BluetoothGattCallback() {
override fun onConnectionStateChange(
gatt: BluetoothGatt, status: Int, newState: Int
) {
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
// 2. 接続確立 → 検出
gatt.discoverServices()
}
BluetoothProfile.STATE_DISCONNECTED -> {
// 3. 接続が失われました
close()
}
}
}
override fun onServicesDiscovered(
gatt: BluetoothGatt, status: Int
) {
if (status == BluetoothGatt.GATT_SUCCESS) {
// 4. GATT階層を受信しました
onGattReady(gatt)
}
}
},
BluetoothDevice.TRANSPORT_LE
)
return bluetoothGatt
}
private fun onGattReady(gatt: BluetoothGatt) {
// GATTは読み取り/書き込み操作の準備ができています
}
fun close() {
bluetoothGatt?.disconnect()
bluetoothGatt?.close()
bluetoothGatt = null
}
}
GattConnectorクラスは正しいBluetoothGatt作成を示しています。autoConnect=falseパラメータは直接接続(スキャンされたデバイス用)を意味します。STATE_CONNECTEDでのonConnectionStateChange時に、すぐにdiscoverServices()が呼び出されます。onServicesDiscoveredはGATTの準備ができたことを通知します。close()はdisconnect()とclose()を順次呼び出します — close()なしではシステムリソースが解放されず、BLE接続リークが発生します。
discoverServices() — 接続後に呼び出される最初のGATTメソッドです。BLE周辺機器上のすべてのサービスの非同期検索を開始します。結果はonServicesDiscovered()にステータスコードとともに届きます:GATT_SUCCESS(0)— 成功、133 — GATT_ERROR、8 — GATT_CONNECTION_TIMEOUT。検出成功後、BluetoothGattはgetServices()を介して利用可能なサービスのリストを設定します。
各BluetoothGattServiceにはBluetoothGattCharacteristicのリストが含まれています。特性にはUUID、プロパティ(PROPERTY_READ、PROPERTY_WRITE、PROPERTY_NOTIFY)、オプションの記述子があります。プロパティは許可される操作を決定します:特性にPROPERTY_READがない場合、readCharacteristicを呼び出すとエラーが返されます。特性の記述子を取得するにはgetDescriptors()を使用します。
// サービス検出と特性検索
class GattServiceExplorer {
// UUIDでサービスを検索
fun findService(gatt: BluetoothGatt, uuid: UUID): BluetoothGattService? {
return gatt.services?.firstOrNull { it.uuid == uuid }
}
// サービス内の特性を検索
fun findCharacteristic(
service: BluetoothGattService,
uuid: UUID
): BluetoothGattCharacteristic? {
return service.characteristics?.firstOrNull { it.uuid == uuid }
}
// サポートされているすべての特性操作を取得
fun getCharacteristicProperties(chars: BluetoothGattCharacteristic): List<String> {
val props = mutableListOf<String>()
with(chars.properties) {
if (and(BluetoothGattCharacteristic.PROPERTY_READ) != 0) props.add("READ")
if (and(BluetoothGattCharacteristic.PROPERTY_WRITE) != 0) props.add("書き込み")
if (and(BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0) props.add("応答なし書き込み")
if (and(BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) props.add("NOTIFY")
if (and(BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0) props.add("INDICATE")
}
return props
}
// GATT階層全体をログに記録
fun dumpGattTree(gatt: BluetoothGatt) {
gatt.services?.forEach { service ->
print("Service: ${service.uuid}")
service.characteristics?.forEach { char ->
print(" Characteristic: ${char.uuid}, properties: ${char.properties}")
char.descriptors?.forEach { desc ->
print(" Descriptor: ${desc.uuid}")
}
}
}
}
}
GattServiceExplorerクラスはGATT階層をナビゲートするためのユーティリティを提供します。findServiceとfindCharacteristicはUUIDでサービスと特性を検索します。getCharacteristicPropertiesはand()でプロパティのビットマスクをチェックします。dumpGattTreeはログに完全な階層を出力します — BLEデバイスのデバッグ時に便利です。BluetoothGattに対するすべての操作は、成功したonServicesDiscoveredの後に実行する必要があります。そうしないと、getServices()は空のリストを返します。
readCharacteristic() — リモートBLEデバイスから特性値を読み取るための非同期BluetoothGattメソッドです。結果はBluetoothGattCallbackのonCharacteristicRead()に届きます。デバイスに2つ以上の一致する特性がある場合(可能性は低いですが)、readCharacteristic()は対象外の特性を読み取る可能性があります — UUIDではなくBluetoothGattCharacteristicインスタンスでreadCharacteristic()を呼び出す方が安全です。
readDescriptor() — 特性記述子の値を読み取るメソッドです。典型的な記述子はCCCD(Client Characteristic Configuration Descriptor、UUID 0x2902)で、通知が有効かどうかを決定します。結果はonDescriptorRead()に届きます。実際には記述子を読み取る必要はほとんどありません — CCCDはsetCharacteristicNotification()で管理されますが、カスタム記述子(User Description 0x2901、Presentation Format 0x2904)の場合、readDescriptor()がメタデータを取得する唯一の方法です。
MTUと大規模データの読み取り — 特性値がMTU(BLE 4.0では23バイト)を超える場合、AndroidはBLEスタックを介した読み取り要求のシーケンスによって自動的にデータを分割および再構成します。拡張MTU(最大251バイト)を持つBLE 5.0+の場合、断片化は不要です — 1回の読み取りで完全なデータが返されます。読み取り前にrequestMtu()を呼び出して最大MTUをネゴシエートできます。
// BluetoothGattを介した特性と記述子の読み取り
class GattReader {
fun readHeartRate(gatt: BluetoothGatt) {
// 心拍数サービスUUID = 0x180D
val service = gatt.getService(UUID.fromString("0000180D-0000-1000-8000-00805F9B34FB"))
?: return
// 心拍数測定UUID = 0x2A37
val characteristic = service.getCharacteristic(
UUID.fromString("00002A37-0000-1000-8000-00805F9B34FB")
) ?: return
if (hasProperty(characteristic, BluetoothGattCharacteristic.PROPERTY_READ)) {
gatt.readCharacteristic(characteristic)
}
}
fun readDescriptor(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
// CCCD(0x2902)
val cccd = characteristic.getDescriptor(
UUID.fromString("00002902-0000-1000-8000-00805F9B34FB")
)
cccd?.let { gatt.readDescriptor(it) }
}
// onCharacteristicReadコールバックでデータを処理:
fun parseHeartRate(value: ByteArray): Int {
// BLE心拍数:bytes = flags、second = bpm
return if (value.isNotEmpty()) value[1].toInt() and 0xFF else 0
}
private fun hasProperty(char: BluetoothGattCharacteristic, prop: Int): Boolean {
return char.properties and prop != 0
}
}
GattReaderクラスは心拍数特性の読み取りを示しています。サービス0x180Dには特性0x2A37(Heart Rate Measurement)が含まれています — Bluetooth SIGの標準BLEプロファイルです。読み取り前に、hasPropertyを介してPROPERTY_READプロパティがチェックされます。parseHeartRateはBLE心拍数形式を解析します:最初のバイト — flags(データ形式)、2番目 — bpm値。CCCD記述子(0x2902)は通知ステータスを確認するために読み取られます。
writeCharacteristic() — BLE周辺機器にデータを書き込むBluetoothGattメソッドです。Android API 33+では、writeCharacteristic()はwriteCharacteristic(request)に置き換えられました。ここでBluetoothGattCharacteristicWriteRequestは、特性、バイト配列、WriteTypeを含む要求オブジェクトです。setValue()を使用した古いwriteCharacteristic(characteristic)メソッドは非推奨です。WriteTypeは要求の動作を決定します:WRITE_TYPE_DEFAULT(特性のプロパティに依存)、WRITE_TYPE_NO_RESPONSE(応答なし)、WRITE_TYPE_SIGNED(認証)。
WriteTypeの選択は速度と信頼性に影響します。WRITE_TYPE_DEFAULTは通常、withResponse(特性にPROPERTY_WRITEがある場合)またはwithoutResponse(PROPERTY_WRITE_NO_RESPONSEがある場合)に対応します。ストリーミングデータ(OTAアップデート、ログ)の場合はWRITE_TYPE_NO_RESPONSEを使用します — 最大スループット。配信保証が必要なコマンド(有効化、設定)の場合は — onCharacteristicWriteを介した確認付きのWRITE_TYPE_DEFAULT。
// Android API 33+でのBLE特性書き込み
class GattWriter {
// 応答付き書き込み(withResponse)
fun writeWithResponse(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, data: ByteArray) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// API 33+:writeCharacteristic
val request = BluetoothGattCharacteristicWriteRequest(
characteristic,
data,
BluetoothGattCharacteristicWriteRequest.WRITE_TYPE_DEFAULT,
@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
)
gatt.writeCharacteristic(request)
} else {
// API < 33: (deprecated)
characteristic.setValue(data)
gatt.writeCharacteristic(characteristic)
}
}
// 応答なし書き込み(最大速度)
fun writeWithoutResponse(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, data: ByteArray) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val request = BluetoothGattCharacteristicWriteRequest(
characteristic,
data,
BluetoothGattCharacteristicWriteRequest.WRITE_TYPE_NO_RESPONSE,
@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
)
gatt.writeCharacteristic(request)
} else {
characteristic.setValue(data)
characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
gatt.writeCharacteristic(characteristic)
}
}
// onCharacteristicWriteコールバック(API 33+)
private val writeCallback = object : BluetoothGattCallback() {
override fun onCharacteristicWrite(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray,
status: Int,
callbackType: Int
) {
if (status == BluetoothGatt.GATT_SUCCESS) {
print("Write success: ${value.size} bytes")
}
}
}
}
GattWriterクラスは異なるAPIレベルに対して両方のWriteTypeをサポートしています。writeWithResponseはWRITE_TYPE_DEFAULTを使用します — BLEデバイスがonCharacteristicWriteを介して書き込みを確認します。writeWithoutResponseはWRITE_TYPE_NO_RESPONSEを使用します — データは確認なしで送信され、最大スループットを実現します。API 33+では、BluetoothGattCharacteristicWriteRequestを使用した新しいwriteCharacteristic(request)が使用されます。API < 33では — 古いsetValue() + writeCharacteristic()。
setCharacteristicNotification() — 周辺機器の特性変更に関する通知を購読するBluetoothGattメソッドです。購読をアクティブにすると、BLEデバイスはonCharacteristicChanged()を介して新しい値を送信します。ただし、setCharacteristicNotification()はローカルのAndroid通知のみをアクティブにします — BLEデバイス自体で通知を有効にするには、CCCD記述子(0x2902)に値0x0100も書き込む必要があります。
CCCD(Client Characteristic Configuration Descriptor) — BLE周辺機器からの通知送信を制御する記述子です。値0x0000 — 通知無効、0x0100 — 通知有効、0x0200 — 表示有効。CCCDへの書き込みは、setCharacteristicNotification()を呼び出した後、BluetoothGattのwriteDescriptor()を介して実行されます。Androidは自動的にCCCDを書き込みません — この責任は開発者にあります。
// 正しいBLE通知購読
class GattNotificationManager {
// 1. 通知を有効にする
fun enableNotification(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
// ステップ1:ローカルAndroid購読
val success = gatt.setCharacteristicNotification(characteristic, true)
if (!success) {
print("購読に失敗しました")
return
}
// ステップ2:BLEデバイスにCCCD(0x2902)を書き込む
val cccdDescriptor = characteristic.getDescriptor(
UUID.fromString("00002902-0000-1000-8000-00805F9B34FB")
) ?: return
// 0x0100 = 通知、0x0200 = 表示
val cccdValue = if (characteristic.properties
and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0) {
BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE // [0x01、0x00]
} else {
BluetoothGattDescriptor.ENABLE_INDICATION_VALUE // [0x02、0x00]
}
cccdDescriptor.setValue(cccdValue)
gatt.writeDescriptor(cccdDescriptor)
}
// 2. 特性検出
fun disableNotification(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
gatt.setCharacteristicNotification(characteristic, false)
val cccdDescriptor = characteristic.getDescriptor(
UUID.fromString("00002902-0000-1000-8000-00805F9B34FB")
) ?: return
cccdDescriptor.setValue(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)
gatt.writeDescriptor(cccdDescriptor)
}
// 3. 通知コールバック
private val notificationCallback = object : BluetoothGattCallback() {
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray,
callbackType: Int
) {
// BLE周辺機器から新しい値
print("Notification: ${value.size} bytes")
}
}
}
GattNotificationManagerクラスは、BLE通知購読の正しい2段階プロトコルを実装しています。enableNotificationは最初にAndroidでsetCharacteristicNotification(true)を呼び出し、次にwriteDescriptorを介してCCCD記述子に0x0100を書き込みます。disableNotificationは逆の操作を実行します。CCCDの書き込みがないと、BLEデバイスは通知を送信しません — これはAndroidのBLE開発者にとって最も一般的なエラーです。
完全な例 — KotlinでのGATTクライアントで、非同期処理にコルーチンを使用して、BluetoothGattの作成、検出、読み取り、通知購読を単一のマネージャーに統合しています。
// Kotlinでのコルーチンを使用した完全なGATTクライアント
class GattClient(context: Context) {
private val context = context.applicationContext
private var gatt: BluetoothGatt? = null
// コルーチンで接続
suspend fun connect(device: BluetoothDevice): Boolean =
suspendCoroutine { continuation ->
gatt = device.connectGatt(
context, false,
object : BluetoothGattCallback() {
override fun onConnectionStateChange(
gatt: BluetoothGatt, status: Int, newState: Int
) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
gatt.discoverServices()
} else {
continuation.resume(false)
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
continuation.resume(status == BluetoothGatt.GATT_SUCCESS)
}
},
BluetoothDevice.TRANSPORT_LE
)
}
// コルーチンを介して特性を読み取る
suspend fun readCharacteristicValue(char: BluetoothGattCharacteristic): ByteArray? =
suspendCoroutine { continuation ->
gatt?.let { gatt ->
// コールバック識別用に特性タグを保存
gatt.setCharacteristic(char, null) // API用< 33
gatt.readCharacteristic(char)
}
}
// 接続を閉じる
fun release() {
gatt?.disconnect()
gatt?.close()
gatt = null
}
}
GATTクライアントGattClientはコルーチン(suspendCoroutine)を使用して、コールバックベースのBluetoothGatt APIを順次呼び出しに変換します。connect()はonServicesDiscoveredを待機し、その後GATT階層が利用可能になります。readCharacteristicValue()はonCharacteristicReadを待機します。このアプローチによりコールバックのネストがなくなり、BLEコードが線形になります。release()はリソースのクリーンアップを保証します — ActivityのonDestroyまたはViewModel.onClearedでの必須呼び出しです。
よくある質問
BluetoothGatt — 周辺デバイスとのBLE接続を管理するGATTクライアント用のAndroidクラスです。BluetoothDevice.connectGatt()を介して作成され、discoverServices()、readCharacteristic()、writeCharacteristic()、setCharacteristicNotification()メソッドを提供します。すべての操作の結果はBluetoothGattCallbackを介して非同期に届きます。BluetoothGattなしでは、Androidでの双方向BLE通信は不可能です。
ステータス133(GATT_ERROR)はAndroid BLEスタックの内部エラーを示します。原因:検出中にデバイスが切断された、MTUが最小値(23バイト)未満、またはBLEスタックが過負荷。解決策:500msの遅延でdiscoverServices()を再試行し、デバイスのRSSIを確認し、周辺機器が現在の状態でGATT検出をサポートしていることを確認してください。
確認付きで書き込むには、WRITE_TYPE_DEFAULT(API 33+:BluetoothGattCharacteristicWriteRequest)でwriteCharacteristic()を呼び出します。成功すると、BLEデバイスが確認を送信し、AndroidがGATT_SUCCESSでonCharacteristicWriteを呼び出します。デバイスが30秒以内(スタックタイムアウト)に応答しない場合、コールバックはエラーステータスを返します。ウォッチドッグには、postDelayed付きのHandlerを使用してください。
Android 13+(API 33)では、BluetoothGattメソッドが変更されました:writeCharacteristic()はBluetoothGattCharacteristicWriteRequestを受け取り、readCharacteristic()はBluetoothGattCharacteristicReadRequestを受け取ります。古いsetValue()/writeCharacteristic()は非推奨です。BluetoothGattCallbackも変更されました:onCharacteristicRead()、onCharacteristicWrite()、onCharacteristicChanged()はByteArray値とcallbackTypeを受け取ります。分岐にはBuild.VERSION.SDK_INTを使用してください。
Androidは4〜8の同時BLE GATT接続をサポートしています(メーカーとAndroidバージョンによって異なります)。Pixel/Google:最大7、Samsung:最大5、Xiaomi:最大4。制限を超えると、connectGattはnullを返すか、onConnectionStateChangeがエラーになります。多数のデバイスを扱うには、循環接続またはBluetooth Meshを使用してください。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。