Generic Attribute Profile (GATT) Development Guide
Note:
Currently in the beta phase.
Introduction
The Generic Attribute Profile (GATT) is an acronym for Generic Attribute, which is a protocol used for data transmission between Bluetooth Low Energy (BLE) devices. It defines a universal framework of attributes and services. Through the GATT protocol, Bluetooth devices can provide services to other devices or obtain services from them.
Scenarios
Main scenarios include:
- Connecting to the server side to read and write information.
- Server-side operations on services and notifying client devices.
Interface Description
For complete Cangjie API documentation and sample code, refer to: GATT Interface.
Detailed interface descriptions are shown in the following table.
| Interface Name | Function Description |
|---|---|
| connect() | Client initiates connection to remote BLE device. |
| disconnect() | Client disconnects from remote BLE device. |
| close() | Closes client functionality, deregisters client from protocol stack. The GattClientDevice instance becomes unusable after this call. |
| getDeviceName() | Client retrieves remote BLE device name. |
| getServices() | Client discovers all services of BLE device (service discovery). |
| readCharacteristicValue() | Client reads characteristic value of specific service from BLE device. |
| readDescriptorValue() | Client reads descriptor contained in specific characteristic from BLE device. |
| writeCharacteristicValue() | Client writes specific characteristic value to BLE device. |
| writeDescriptorValue() | Client writes binary data to specific descriptor of BLE device. |
| getRssiValue() | Client retrieves signal strength (RSSI) of remote BLE device. Must be called after successful connection via connect(). |
| setBLEMtuSize() | Client negotiates Maximum Transmission Unit (MTU) with remote BLE device. Must be called after successful connection via connect(). |
| setCharacteristicChangeNotification() | Requests server to enable notifications for characteristic value changes. |
| setCharacteristicChangeIndication() | Requests server to enable indications for characteristic value changes. |
on(type: BluetoothBleGattClientDeviceCallbackType) |
Subscribes to characteristic value change events. Requires prior call to setNotifyCharacteristicChanged to receive server notifications. |
off(type: BluetoothBleGattClientDeviceCallbackType) |
Unsubscribes from characteristic value change events. |
on(type: BluetoothBleGattClientDeviceCallbackType) |
Client subscribes to BLE connection state change events. |
off(type: BluetoothBleGattClientDeviceCallbackType) |
Unsubscribes from BLE connection state change events. |
on(type: BluetoothBleGattClientDeviceCallbackType) |
Client subscribes to MTU state change events. |
off(type: BluetoothBleGattClientDeviceCallbackType) |
Client unsubscribes from MTU state change events. |
| addService() | Server adds service. |
| removeService() | Removes added service. |
| close() | Closes server functionality, deregisters server from protocol stack. The GattServer instance becomes unusable after this call. |
| notifyCharacteristicChanged() | Server actively notifies connected clients when characteristic value changes. |
| sendResponse() | Server responds to client read/write requests. |
on(type: BluetoothBleGattServerCallbackType) |
Server subscribes to characteristic read request events. |
off(type: BluetoothBleGattServerCallbackType) |
Server unsubscribes from characteristic read request events. |
on(type: BluetoothBleGattServerCallbackType) |
Server subscribes to characteristic write request events. |
off(type: BluetoothBleGattServerCallbackType) |
Server unsubscribes from characteristic write request events. |
on(type: BluetoothBleGattServerCallbackType) |
Server subscribes to descriptor read request events. |
off(type: BluetoothBleGattServerCallbackType) |
Server unsubscribes from descriptor read request events. |
on(type: BluetoothBleGattServerCallbackType) |
Server subscribes to descriptor write request events. |
off(type: BluetoothBleGattServerCallbackType) |
Server unsubscribes from descriptor write request events. |
on(type: BluetoothBleGattServerCallbackType) |
Server subscribes to BLE connection state change events. |
off(type: BluetoothBleGattServerCallbackType) |
Server unsubscribes from BLE connection state change events. |
on(type: BluetoothBleGattServerCallbackType) |
Server subscribes to MTU state change events. |
off(type: BluetoothBleGattServerCallbackType) |
Server unsubscribes from MTU state change events. |
Key Scenario Development Steps
Connecting to Server to Read and Write Information
-
Import required BLE modules.
-
Create gattClient instance object.
-
Connect to gattServer.
-
Read characteristic values and descriptors from gattServer.
-
Write characteristic values and descriptors to gattServer.
-
Disconnect and destroy gattClient instance.
-
Sample code:
import kit.ConnectivityKit.* import ohos.business_exception.BusinessException import ohos.callback_invoke.Callback1Argument const TAG: String = 'GattClientManager' class GattClientManager { var device: ?String = None var gattClient: ?GattClientDevice = None let connectState: ProfileConnectionState = ProfileConnectionState.StateDisconnected let myServiceUuid: String = '00001810-0000-1000-8000-00805F9B34FB' let myCharacteristicUuid: String = '00001820-0000-1000-8000-00805F9B34FB' let myFirstDescriptorUuid: String = '00002902-0000-1000-8000-00805F9B34FB' // 2902 typically used for notification/indication let mySecondDescriptorUuid: String = '00002903-0000-1000-8000-00805F9B34FB' var found: Bool = false // Initialize BLEDescriptor private func initDescriptor(des: String, value: Array<Byte>): BLEDescriptor { let descriptor: BLEDescriptor = BLEDescriptor( this.myServiceUuid, this.myCharacteristicUuid, des, value ) return descriptor } // Initialize BLECharacteristic private func initCharacteristic(): BLECharacteristic { let descValue: Array<UInt8> = [11, 12] let descriptors: Array<BLEDescriptor> = [initDescriptor(this.myFirstDescriptorUuid, [0, 0]), initDescriptor(this.mySecondDescriptorUuid, descValue)] let charValue: Array<UInt8> = [1, 2] let characteristic: BLECharacteristic = BLECharacteristic( this.myServiceUuid, this.myCharacteristicUuid, charValue, descriptors, GattProperties() ) return characteristic } private func logCharacteristic(char: BLECharacteristic) { var message = 'logCharacteristic uuid:' + char.characteristicUuid + '\n' let value = char.characteristicValue message += 'logCharacteristic value: ' for (i in 0..value.size) { message += value[i].toString() + ' ' } Hilog.info(1, "info", message) } private func logDescriptor(des: BLEDescriptor) { var message = 'logDescriptor uuid:' + des.descriptorUuid + '\n' let value = des.descriptorValue message += 'logDescriptor value: ' for (i in 0..value.size) { message += value[i].toString() + ' ' } Hilog.info(1, "info", message) } private func checkService(services: Array<GattService>): Bool { for (i in 0..services.size) { if (services[i].serviceUuid != this.myServiceUuid) { continue } for (j in 0..services[i].characteristics.size) { if (services[i].characteristics[j].characteristicUuid != this.myCharacteristicUuid) { continue } for (k in 0..services[i].characteristics[j].descriptors.size) { if (services[i].characteristics[j].descriptors[k].descriptorUuid == this.myFirstDescriptorUuid) { Hilog.info(1, "info", 'find expected service from server') return true } } } } Hilog.error(1, "info", 'no expected service from server') return false } // 1. Subscribe to connection state changes public func onGattClientStateChange() { if (this.gattClient.isNone()) { Hilog.error(1, "info", 'no gattClient') return } try { this.gattClient?.on(BluetoothBleGattClientDeviceCallbackType.BLE_CONNECTION_STATE_CHANGE, ChangeStateCb()) } catch (e: BusinessException) { Hilog.error(1, "info", 'errCode: ${e.code}, errMessage: ' + e.message) } } // 2. Called when client initiates connection public func startConnect(peerDevice: String) { // Peer device typically obtained via BLE scan if (this.connectState != ProfileConnectionState.StateDisconnected) { Hilog.error(1, "info", 'startConnect failed') return } Hilog.info(1, "info", 'startConnect ' + peerDevice) this.device = peerDevice // 2.1 Create gattClient using device - all subsequent interactions require this instance this.gattClient = createGattClientDevice(peerDevice) try { this.onGattClientStateChange() // 2.2 Subscribe to connection state this.gattClient?.connect() // 2.3 Initiate connection } catch (e: BusinessException) { Hilog.error(1, "info", 'errCode: ${e.code}, errMessage: ' + e.message) } } // 3. After successful connection, perform service discovery public func discoverServices() { if (this.gattClient.isNone()) { Hilog.info(1, "info", 'no gattClient') return } Hilog.info(1, "info", 'discoverServices') try { let result = this.gattClient?.getServices() this.found = this.checkService(result.getOrThrow()) // Ensure server has required services } catch (e: BusinessException) { Hilog.error(1, "info", 'errCode: ${e.code}, errMessage: ' + e.message) } } // 4. After confirming server services, read characteristic values public func readCharacteristicValue() { if (this.gattClient.isNone() || this.connectState != ProfileConnectionState.STATE_CONNECTED) { Hilog.error(1, "info", 'no gattClient or not connected') return } if (!this.found) { // Ensure server has corresponding characteristic Hilog.error(1, "info", 'no characteristic from server') return } let characteristic = this.initCharacteristic() Hilog.info(1, "info", 'readCharacteristicValue') try { this.gattClient?.readCharacteristicValue(characteristic) { e, outData => this.logCharacteristic(outData.getOrThrow()) } } catch (e: BusinessException) { Hilog.error(1, "info", 'errCode: ${e.code}, errMessage: ' + e.message) } } // 5. After confirming server services, write characteristic values public func writeCharacteristicValue() { if (this.gattClient.isNone() || this.connectState != ProfileConnectionState.STATE_CONNECTED) { Hilog.error(1, "info", 'no gattClient or not connected') return } if (!this.found) { // Ensure server has corresponding characteristic Hilog.error(1, "info", 'no characteristic from server') return } let characteristic = this.initCharacteristic() Hilog.info(1, "info", 'writeCharacteristicValue') try { this.gattClient?.writeCharacteristicValue(characteristic, GattWriteType.WRITE) { err => if (let Some(e) <- err) { Hilog.error(1, "info", 'errCode: ${e.code}, errMessage: ' + e.message) return } Hilog.info(1, "info", 'writeCharacteristicValue success') } } catch (e: BusinessException) { Hilog.error(1, "info", 'errCode: ${e.code}, errMessage: ' + e.message) } } // 6. After confirming server services, read descriptors public func readDescriptorValue() { if (this.gattClient.isNone() || this.connectState != ProfileConnectionState.STATE_CONNECTED) { Hilog.error(1, "info", 'no gattClient or not connected') return } if (!this.found) { // Ensure server has corresponding descriptor Hilog.error(1, "info", 'no descriptor from server') return } let descBuffer = Array<Byte>() let descriptor = this.initDescriptor(this.mySecondDescriptorUuid, descBuffer) Hilog.info(1, "info", 'readDescriptorValue') try { this.gattClient?.readDescriptorValue(descriptor) { e, outData => this.logDescriptor(outData.getOrThrow()) } } catch (e: BusinessException) { Hilog.error(1, "info", 'errCode: ${e.code}, errMessage: ' + e.message) } } // 7. After confirming server services, write descriptors public func writeDescriptorValue() { if (this.gattClient.isNone() || this.connectState != ProfileConnectionState.STATE_CONNECTED) { Hilog.error(1, "info", 'no gattClient or not connected') return } if (!this.found) { // Ensure server has corresponding descriptor Hilog.error(1, "info", 'no descriptor from server') return } let descBuffer: Array<UInt8> = [11, 12] let descriptor = this.initDescriptor(this.mySecondDescriptorUuid, descBuffer) Hilog.info(1, "info", 'writeDescriptorValue') try { this.gattClient?.writeDescriptorValue(descriptor) { err => if (let Some(e) <- err) { Hilog.error(1, "info", 'errCode: ${e.code}, errMessage: ' + e.message) return } Hilog.info(1, "info", 'writeDescriptorValue success') } } catch (e: BusinessException) { Hilog.error(1, "info", 'errCode: ${e.code}, errMessage: ' + e.message) } } // 8. Called when client initiates disconnection public func stopConnect() { if (this.gattClient.isNone() || this.connectState != ProfileConnectionState.STATE_CONNECTED) { Hilog.error(1, "info", 'no gattClient or not connected') return } Hilog.info(1, "info", 'stopConnect ' + this.device.getOrThrow()) try { this.gattClient?.disconnect() // 8.1 Disconnect this.gattClient?.off(BluetoothBleGattClientDeviceCallbackType.BLE_CONNECTION_STATE_CHANGE) this.gattClient?.close() // 8.2 Close if gattClient won't be used further } catch (e: BusinessException) { Hilog.error(1, "info", 'errCode: ${e.code}, errMessage: ' + e.message) } } } class ChangeStateCb <: Callback1Argument<BLEConnectionChangeState> { public func invoke(err: ?BusinessException, stateInfo: BLEConnectionChangeState) { let state = match (stateInfo.state) { case STATE_DISCONNECTED => 'DISCONNECTED' case STATE_CONNECTING => 'CONNECTING' case STATE_CONNECTED => 'CONNECTED' case STATE_DISCONNECTING => 'DISCONNECTING' case _ => 'undefined' } Hilog.info(1, "info", 'onGattClientStateChange: device=' + stateInfo.deviceId + ', state=' + state) } } let gattClientManager = GattClientManager() -
For error codes, refer to Bluetooth Service Subsystem Error Codes.
Server-Side Service Operations and Client Notifications
-
Import required BLE modules.
-
Create gattServer instance object.
-
Add service information.
-
Notify gattClient when characteristic values are written to gattServer.
-
Remove service information.
-
Deregister gattServer instance.
-
Sample code:
import kit.ConnectivityKit.* import ohos.business_exception.BusinessException import ohos.callback_invoke.Callback1Argument import std.collection.ArrayList const TAG: String = 'GattServerManager' class GattServerManager { public var gattServer: ?GattServer = None let connectState: ProfileConnectionState = ProfileConnectionState.STATE_DISCONNECTED let myServiceUuid: String = '00001810-0000-1000-8000-00805F9B34FB' let myCharacteristicUuid: String = '00001820-0000-1000-8000-00805F9B34FB' let myFirstDescriptorUuid: String = '00002902-0000-1000-8000-00805F9B34FB' // 2902 typically used for notification/indication```markdown return } try { this.gattServer?.on(BluetoothBleGattServerCallbackType.CONNECTION_STATE_CHANGE, ChangeStateCb()) } catch (e: BusinessException) { Hilog.error(1, "info", 'errCode: ${e.code}, errMessage: ' + e.message) } } // 2. Called when the server registers a service public func registerServer(peerDevice: String) { // Peer device is typically obtained via BLE scan let characteristics: ArrayList<BLECharacteristic> = ArrayList<BLECharacteristic>() let characteristic = this.initCharacteristic() characteristics.add(characteristic) let gattService: GattService = GattService( this.myServiceUuid, true, characteristics.toArray(), Array<GattService>() ) Hilog.info(1, "info", 'registerServer ' + this.myServiceUuid) try { this.gattServer = createGattServer() // 2.1 Construct gattServer instance for subsequent interactions this.onGattServerStateChange() // 2.2 Subscribe to connection state changes this.gattServer?.addService(gattService) } catch (e: BusinessException) { Hilog.error(1, "info", 'errCode: ${e.code}, errMessage: ' + e.message) } } // 3. Called when subscribing to characteristic read requests from gattClient public func onCharacteristicRead() { if (this.gattServer.isNone()) { Hilog.info(1, "info", 'no gattServer') return } Hilog.info(1, "info", 'onCharacteristicRead') try { this.gattServer?.on(BluetoothBleGattServerCallbackType.CHARACTERISTIC_READ, ReadRequestCb()) } catch (e: BusinessException) { Hilog.error(1, "info", 'errCode: ${e.code}, errMessage: ' + e.message) } } // 4. Called when subscribing to characteristic write requests from gattClient public func onCharacteristicWrite() { if (this.gattServer.isNone()) { Hilog.error(1, "info", 'no gattServer') return } Hilog.info(1, "info", 'onCharacteristicWrite') try { this.gattServer?.on(BluetoothBleGattServerCallbackType.CHARACTERISTIC_WRITE, WriteRequestCb()) } catch (e: BusinessException) { Hilog.error(1, "info", 'errCode: ${e.code}, errMessage: ' + e.message) } } // 5. Called when subscribing to descriptor read requests from gattClient public func onDescriptorRead() { if (this.gattServer.isNone()) { Hilog.error(1, "info", 'no gattServer') return } Hilog.info(1, "info", 'onDescriptorRead') try { this.gattServer?.on(BluetoothBleGattServerCallbackType.DESCRIPTOR_READ, DescriptorReadRequestCb()) } catch (e: BusinessException) { Hilog.error(1, "info", 'errCode: ${e.code}, errMessage: ' + e.message) } } // 6. Called when subscribing to descriptor write requests from gattClient public func onDescriptorWrite() { if (this.gattServer.isNone()) { Hilog.error(1, "info", 'no gattServer') return } Hilog.info(1, "info", 'onDescriptorWrite') try { this.gattServer?.on(BluetoothBleGattServerCallbackType.DESCRIPTOR_WRITE, DescriptorWriteRequestCb()) } catch (e: BusinessException) { Hilog.error(1, "info", 'errCode: ${e.code}, errMessage: ' + e.message) } } // 7. Called when the server removes a service (no longer in use) public func unRegisterServer() { if (this.gattServer.isNone()) { Hilog.error(1, "info", 'no gattServer') return } Hilog.info(1, "info", 'unRegisterServer ' + this.myServiceUuid) try { this.gattServer?.removeService(this.myServiceUuid) // 7.1 Remove service this.gattServer?.off(BluetoothBleGattServerCallbackType.CONNECTION_STATE_CHANGE) this.gattServer?.close() // 7.3 Close gattServer if no longer needed } catch (e: BusinessException) { Hilog.error(1, "info", 'errCode: ${e.code}, errMessage: ' + e.message) } } } class ChangeStateCb <: Callback1Argument<BLEConnectionChangeState> { public func invoke(err: ?BusinessException, stateInfo: BLEConnectionChangeState) { let state = match (stateInfo.state) { case STATE_DISCONNECTED => 'DISCONNECTED' case STATE_CONNECTING => 'CONNECTING' case STATE_CONNECTED => 'CONNECTED' case STATE_DISCONNECTING => 'DISCONNECTING' case _ => 'undefined' } Hilog.info(1, "info", 'onGattClientStateChange: device=' + stateInfo.deviceId + ', state=' + state) } } let gattServerManager = GattServerManager() class ReadRequestCb <: Callback1Argument<CharacteristicReadRequest> { public func invoke(err: ?BusinessException, charReq: CharacteristicReadRequest): Unit { let deviceId: String = charReq.deviceId let transId: Int32 = charReq.transId let offset: Int32 = charReq.offset Hilog.info(1, "info", 'receive characteristicRead') let rspBuffer: Array<UInt8> = [21, 22] let serverResponse: ServerResponse = ServerResponse( deviceId, transId, 0, // 0 indicates success offset, rspBuffer ) try { gattServerManager.gattServer?.sendResponse(serverResponse) } catch (e: BusinessException) { Hilog.error(1, "info", 'errCode: ${e.code}, errMessage: ' + e.message) } } } class WriteRequestCb <: Callback1Argument<CharacteristicWriteRequest> { public func invoke(err: ?BusinessException, charReq: CharacteristicWriteRequest): Unit { let deviceId: String = charReq.deviceId let transId: Int32 = charReq.transId let offset: Int32 = charReq.offset Hilog.info(1, "info", 'receive characteristicWrite: needRsp=${charReq.needRsp}') if (!charReq.needRsp) { return } let rspBuffer: Array<UInt8> = [0] let serverResponse: ServerResponse = ServerResponse( deviceId, transId, 0, // 0 indicates success offset, rspBuffer ) try { gattServerManager.gattServer?.sendResponse(serverResponse) } catch (e: BusinessException) { Hilog.error(1, "info", 'errCode: ${e.code}, errMessage: ' + e.message) } } } class DescriptorReadRequestCb <: Callback1Argument<DescriptorReadRequest> { public func invoke(err: ?BusinessException, desReq: DescriptorReadRequest): Unit { let deviceId: String = desReq.deviceId let transId: Int32 = desReq.transId let offset: Int32 = desReq.offset Hilog.info(1, "info", 'receive descriptorRead') let rspBuffer: Array<UInt8> = [31, 32] let serverResponse: ServerResponse = ServerResponse( deviceId, transId, 0, // 0 indicates success offset, rspBuffer ) try { gattServerManager.gattServer?.sendResponse(serverResponse) } catch (e: BusinessException) { Hilog.error(1, "info", 'errCode: ${e.code}, errMessage: ' + e.message) } } } class DescriptorWriteRequestCb <: Callback1Argument<DescriptorWriteRequest> { public func invoke(err: ?BusinessException, desReq: DescriptorWriteRequest): Unit { let deviceId: String = desReq.deviceId let transId: Int32 = desReq.transId let offset: Int32 = desReq.offset Hilog.info(1, "info", 'receive descriptorWrite: needRsp=${desReq.needRsp}') if (!desReq.needRsp) { return } let rspBuffer: Array<UInt8> = [0] let serverResponse: ServerResponse = ServerResponse( deviceId, transId, 0, // 0 indicates success offset, rspBuffer ) try { gattServerManager.gattServer?.sendResponse(serverResponse) } catch (e: BusinessException) { Hilog.error(1, "info", 'errCode: ${e.code}, errMessage: ' + e.message) } } } -
For error codes, please refer to Bluetooth Service Subsystem Error Codes.