התחברות לשרת GATT

השלב הראשון באינטראקציה עם מכשיר BLE הוא להתחבר אליו. למעשה, מדובר בחיבור לשרת GATT במכשיר. כדי להתחבר לשרת GATT במכשיר BLE, משתמשים בשיטה connectGatt(). השיטה הזו מקבלת שלושה פרמטרים: אובייקט Context,‏ autoConnect (ערך בוליאני שמציין אם להתחבר אוטומטית למכשיר BLE ברגע שהוא זמין) והפניה אל BluetoothGattCallback:

var bluetoothGatt: BluetoothGatt? = null
// ...
bluetoothGatt = device.connectGatt(this, false, bluetoothGattCallback)

הפעולה הזו מתחברת לשרת GATT שמארח מכשיר ה-BLE, ומחזירה מופע BluetoothGatt, שאפשר להשתמש בו כדי לבצע פעולות של לקוח GATT. הגורם שקורא (אפליקציית Android) הוא לקוח GATT. ה-BluetoothGattCallback משמש להעברת תוצאות ללקוח, כמו סטטוס החיבור, וגם לביצוע פעולות נוספות של לקוח GATT.

הגדרת שירות קשור

בדוגמה הבאה, אפליקציית BLE מספקת פעילות (DeviceControlActivity) כדי להתחבר למכשירי Bluetooth, להציג נתוני מכשיר ולהציג את שירותי ה-GATT והמאפיינים הנתמכים על ידי המכשיר. בהתאם לקלט המשתמש, הפעילות הזו מתקשרת עם Service שנקרא BluetoothLeService, שמתקשר עם מכשיר ה-BLE באמצעות ה-API של BLE. התקשורת מתבצעת באמצעות שירות מאוגד שמאפשר לפעילות להתחבר אל BluetoothLeService ולקרוא לפונקציות כדי להתחבר למכשירים. ל-BluetoothLeService נדרשת הטמעה של Binder שמאפשרת גישה לשירות לצורך הפעילות.

class BluetoothLeService : Service() {

    private val binder = LocalBinder()

    override fun onBind(intent: Intent): IBinder? {
        return binder
    }

    inner class LocalBinder : Binder() {
        fun getService(): BluetoothLeService {
            return this@BluetoothLeService
        }
    }
}

הפעילות יכולה להפעיל את השירות באמצעות bindService(), להעביר Intent כדי להפעיל את השירות, הטמעה של ServiceConnection כדי להאזין לאירועי החיבור והניתוק, ודגל כדי לציין אפשרויות חיבור נוספות.

class DeviceControlActivity : AppCompatActivity() {

    private var bluetoothService: BluetoothLeService? = null
    // Code to manage Service lifecycle.
    private val serviceConnection: ServiceConnection = object : ServiceConnection {
        override fun onServiceConnected(
            componentName: ComponentName,
            service: IBinder
        ) {
            bluetoothService = (service as LocalBinder).getService()
            bluetoothService?.let { bluetooth ->
                // call functions on service to check connection and connect to devices
            }
        }

        override fun onServiceDisconnected(componentName: ComponentName) {
            bluetoothService = null
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.gatt_services_characteristics)

        val gattServiceIntent = Intent(this, BluetoothLeService::class.java)
        bindService(gattServiceIntent, serviceConnection, Context.BIND_AUTO_CREATE)
    }
}

הגדרת ה-BluetoothAdapter

אחרי שהשירות מקושר, הוא צריך לגשת אל BluetoothAdapter. הוא אמור לבדוק שהמתאם זמין במכשיר. מידע נוסף על BluetoothAdapter זמין במאמר הגדרת Bluetooth. בדוגמה הבאה, קוד ההגדרה הזה עטוף בפונקציה initialize() שמחזירה ערך Boolean שמציין שהפעולה הצליחה.

private const val TAG = "BluetoothLeService"

class BluetoothLeService : Service() {

    private var bluetoothAdapter: BluetoothAdapter? = null

    fun initialize(): Boolean {
        bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
        if (bluetoothAdapter == null) {
            Log.e(TAG, "Unable to obtain a BluetoothAdapter.")
            return false
        }
        return true
    }

    // ...
}

הפעילות קוראת לפונקציה הזו בהטמעה של ServiceConnection. הטיפול בערך החזרה False מהפונקציה initialize() תלוי באפליקציה שלכם. אפשר להציג למשתמש הודעת שגיאה שמציינת שהמכשיר הנוכחי לא תומך בפעולת Bluetooth, או להשבית תכונות שדורשות Bluetooth כדי לפעול. בדוגמה הבאה, הפונקציה finish() נקראת בפעילות כדי להחזיר את המשתמש למסך הקודם.

class DeviceControlActivity : AppCompatActivity() {

    // Code to manage Service lifecycle.
    private val serviceConnection: ServiceConnection = object : ServiceConnection {
        override fun onServiceConnected(
            componentName: ComponentName,
            service: IBinder
        ) {
            bluetoothService = (service as LocalBinder).getService()
            bluetoothService?.let { bluetooth ->
                if (!bluetooth.initialize()) {
                    Log.e(TAG, "Unable to initialize Bluetooth")
                    finish()
                }
                // perform device connection
            }
        }

        override fun onServiceDisconnected(componentName: ComponentName) {
            bluetoothService = null
        }
    }

    // ...
}

התחברות למכשיר

אחרי שמאתחלים את מופע BluetoothLeService, אפשר להתחבר למכשיר BLE. הפעילות צריכה לשלוח את כתובת המכשיר לשירות כדי שהוא יוכל ליזום את החיבור. השירות יתקשר קודם אל getRemoteDevice() ב-BluetoothAdapter כדי לגשת למכשיר. אם המתאם לא מצליח למצוא מכשיר עם הכתובת הזו, getRemoteDevice() מחזיר IllegalArgumentException.

fun connect(address: String): Boolean {
    bluetoothAdapter?.let { adapter ->
        try {
            val device = adapter.getRemoteDevice(address)
        } catch (exception: IllegalArgumentException) {
            Log.w(TAG, "Device not found with provided address.")
            return false
        }
        // connect to the GATT server on the device
        return true
    } ?: run {
        Log.w(TAG, "BluetoothAdapter not initialized")
        return false
    }
}

ה-DeviceControlActivity קורא לפונקציית connect() הזו אחרי שהשירות עובר אתחול. הפעילות צריכה להעביר את הכתובת של מכשיר ה-BLE. בדוגמה הבאה, כתובת המכשיר מועברת לפעילות כתוספת של intent.

// Code to manage Service lifecycle.
private val serviceConnection: ServiceConnection = object : ServiceConnection {
    override fun onServiceConnected(
        componentName: ComponentName,
        service: IBinder
    ) {
        bluetoothService = (service as LocalBinder).getService()
        bluetoothService?.let { bluetooth ->
            if (!bluetooth.initialize()) {
                Log.e(TAG, "Unable to initialize Bluetooth")
                finish()
            }
            // perform device connection
            deviceAddress?.let { bluetooth.connect(it) }
        }
    }

    override fun onServiceDisconnected(componentName: ComponentName) {
        bluetoothService = null
    }
}

הצהרה על קריאה חוזרת (callback) של GATT

אחרי שהפעילות מציינת לשירות לאיזה מכשיר להתחבר והשירות מתחבר למכשיר, השירות צריך להתחבר לשרת GATT במכשיר BLE. החיבור הזה דורש BluetoothGattCallback כדי לקבל התראות על מצב החיבור, על גילוי שירותים, על קריאות של מאפיינים ועל התראות על מאפיינים.

הנושא הזה מתמקד בהתראות על מצב החיבור. במאמר העברת נתונים באמצעות BLE מוסבר איך לבצע גילוי שירותים, קריאת מאפיינים ובקשת התראות על מאפיינים.

הפונקציה onConnectionStateChange() מופעלת כשהחיבור לשרת ה-GATT של המכשיר משתנה. בדוגמה הבאה, פונקציית ה-callback מוגדרת במחלקה Service כדי שאפשר יהיה להשתמש בה עם BluetoothDevice אחרי שהשירות מתחבר אליה.

private val bluetoothGattCallback = object : BluetoothGattCallback() {
    override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
        if (newState == BluetoothProfile.STATE_CONNECTED) {
            // successfully connected to the GATT Server
        } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
            // disconnected from the GATT Server
        }
    }
}

התחברות לשירות GATT

אחרי שמצהירים על BluetoothGattCallback, השירות יכול להשתמש באובייקט BluetoothDevice מהפונקציה connect() כדי להתחבר לשירות GATT במכשיר.

נעשה שימוש בפונקציה connectGatt(). לשם כך, נדרש אובייקט Context, דגל בוליאני autoConnect ו-BluetoothGattCallback. בדוגמה הזו, האפליקציה מתחברת ישירות למכשיר BLE, ולכן הערך false מועבר ל-autoConnect.

נוסף גם נכס BluetoothGatt. כך השירות יכול לסגור את החיבור כשאין בו יותר צורך.

class BluetoothLeService : Service() {

    // ...
    private var bluetoothGatt: BluetoothGatt? = null

    // ...
    fun connect(address: String): Boolean {
        bluetoothAdapter?.let { adapter ->
            try {
                val device = adapter.getRemoteDevice(address)
                // connect to the GATT server on the device
                bluetoothGatt = device.connectGatt(this, false, bluetoothGattCallback)
                return true
            } catch (exception: IllegalArgumentException) {
                Log.w(TAG, "Device not found with provided address.  Unable to connect.")
                return false
            }
        } ?: run {
            Log.w(TAG, "BluetoothAdapter not initialized")
            return false
        }
    }
}

עדכונים לגבי שידורים

כשהשרת מתחבר לשרת GATT או מתנתק ממנו, הוא צריך להודיע על מצב הפעילות החדש. יש כמה דרכים לעשות את זה. בדוגמה הבאה נעשה שימוש בשידורים כדי לשלוח את המידע מהשירות לפעילות.

השירות מכריז על פונקציה לשידור המצב החדש. הפונקציה הזו מקבלת מחרוזת פעולה שמועברת לאובייקט Intent לפני שהיא משודרת למערכת.

private fun broadcastUpdate(action: String) {
    val intent = Intent(action)
    sendBroadcast(intent)
}

אחרי שמגדירים את פונקציית השידור, משתמשים בה בתוך BluetoothGattCallback כדי לשלוח מידע על מצב החיבור עם שרת GATT. הקבועים והסטטוס הנוכחי של החיבור לשירות מוצהרים בשירות שמייצג את הפעולות Intent.

class BluetoothLeService : Service() {

    private var connectionState = STATE_DISCONNECTED

    private val bluetoothGattCallback = object : BluetoothGattCallback() {
        override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                // successfully connected to the GATT Server
                connectionState = STATE_CONNECTED
                broadcastUpdate(ACTION_GATT_CONNECTED)
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                // disconnected from the GATT Server
                connectionState = STATE_DISCONNECTED
                broadcastUpdate(ACTION_GATT_DISCONNECTED)
            }
        }
    }

    // ...
    companion object {
        const val ACTION_GATT_CONNECTED =
            "com.example.bluetooth.le.ACTION_GATT_CONNECTED"
        const val ACTION_GATT_DISCONNECTED =
            "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED"

        private const val STATE_DISCONNECTED = 0
        private const val STATE_CONNECTED = 2
    }
}

האזנה לעדכונים בפעילות

אחרי שהשירות משדר את עדכוני החיבור, הפעילות צריכה להטמיע BroadcastReceiver. צריך לרשום את המקלט הזה כשמגדירים את הפעילות, ולבטל את הרישום שלו כשהפעילות יוצאת מהמסך. הפעילות מאזינה לאירועים מהשירות, ולכן היא יכולה לעדכן את ממשק המשתמש על סמך מצב החיבור הנוכחי למכשיר BLE.

class DeviceControlActivity : AppCompatActivity() {

    private val gattUpdateReceiver: BroadcastReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            when (intent.action) {
                BluetoothLeService.ACTION_GATT_CONNECTED -> {
                    connected = true
                    updateConnectionState(R.string.connected)
                }
                BluetoothLeService.ACTION_GATT_DISCONNECTED -> {
                    connected = false
                    updateConnectionState(R.string.disconnected)
                }
            }
        }
    }

    override fun onResume() {
        super.onResume()
        registerReceiver(gattUpdateReceiver, makeGattUpdateIntentFilter())
        bluetoothService?.let { service ->
            deviceAddress?.let { address ->
                val result = service.connect(address)
                Log.d(TAG, "Connect request result=$result")
            }
        }
    }

    override fun onPause() {
        super.onPause()
        unregisterReceiver(gattUpdateReceiver)
    }

    private fun makeGattUpdateIntentFilter(): IntentFilter {
        return IntentFilter().apply {
            addAction(BluetoothLeService.ACTION_GATT_CONNECTED)
            addAction(BluetoothLeService.ACTION_GATT_DISCONNECTED)
        }
    }
}

בקטע Transfer BLE data, ה-BroadcastReceiver משמש גם כדי להעביר את נתוני הגילוי של השירות וגם את נתוני המאפיינים מהמכשיר.

סגירת חיבור GATT

שלב חשוב כשעובדים עם חיבורי Bluetooth הוא לסגור את החיבור כשמסיימים להשתמש בו. כדי לעשות את זה, קוראים לפונקציה close() באובייקט BluetoothGatt. בדוגמה הבאה, השירות מחזיק בהפניה אל BluetoothGatt. כשהפעילות מבטלת את הקישור לשירות, החיבור נסגר כדי למנוע ניקוז של הסוללה במכשיר.

class BluetoothLeService : Service() {

    // ...
    override fun onUnbind(intent: Intent?): Boolean {
        close()
        return super.onUnbind(intent)
    }

    private fun close() {
        bluetoothGatt?.let { gatt ->
            gatt.close()
            bluetoothGatt = null
        }
    }
}