前言
??在上一篇中我們進(jìn)行掃描設(shè)備的處理,本文中進(jìn)行連接和發(fā)現(xiàn)服務(wù)的數(shù)據(jù)處理,運(yùn)行效果圖如下所示:
目錄
- Ble藍(lán)牙App(一)掃描
- Ble藍(lán)牙App(二)連接與發(fā)現(xiàn)服務(wù)
- Ble藍(lán)牙App(三)特性和屬性
- Ble藍(lán)牙App(四)UI優(yōu)化和描述符
- Ble藍(lán)牙App(五)數(shù)據(jù)操作
正文
??現(xiàn)在我們從MainActivity進(jìn)入到ScanActivity,選中一個(gè)設(shè)備返回到MainActivity,下面要對(duì)選中的設(shè)備進(jìn)行處理,首先我們來(lái)做連接。
一、GATT回調(diào)
??在之前我們寫(xiě)了一個(gè)BleCore,這里面是對(duì)掃描的封裝,那么對(duì)于連接來(lái)說(shuō)我們同樣可以封裝到這里,我們可以在BleCore中寫(xiě)一個(gè)BleGattCallback 類(lèi),代碼如下所示:
class BleGattCallback : BluetoothGattCallback() {
/**
* 連接狀態(tài)改變
*/
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
}
/**
* 發(fā)現(xiàn)服務(wù)
*/
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
}
}
??因?yàn)楸疚囊龅氖虑槭沁B接和發(fā)現(xiàn)服務(wù),所以我們就先重寫(xiě)這兩個(gè)函數(shù),注意一點(diǎn)的是,藍(lán)牙的操作都是在子線程中進(jìn)行的,如果我們需要知道當(dāng)前是否連接,則需要寫(xiě)一個(gè)接口用于回調(diào)到Activity中,在ble包下新建一個(gè)BleCallback接口,代碼如下所示:
interface BleCallback {
/**
* 設(shè)備的所有信息
*/
fun deviceInfo(info: String)
/**
* 連接狀態(tài)
* @param state true or false
*/
fun onConnectionStateChange(state: Boolean)
/**
* 發(fā)現(xiàn)服務(wù)
*/
fun onServicesDiscovered(services: List<BluetoothGattService>)
}
??接口中定義了三個(gè)函數(shù),通過(guò)注釋我們清晰的知道都是什么作用,這里著重介紹第一個(gè)函數(shù),這個(gè)函數(shù)會(huì)顯示設(shè)備各個(gè)時(shí)候的狀態(tài)信息,從連接之后的所有動(dòng)作,如果我們需要保存設(shè)備的操作日志的話,可以通過(guò)這個(gè)來(lái)進(jìn)行處理保存。
然后回到BleCore,在companion object
中聲明變量和設(shè)置接口回調(diào)的函數(shù):
@SuppressLint("StaticFieldLeak")
companion object {
...
private var mGatt: BluetoothGatt? = null
private var mBleCallback: BleCallback? = null
private lateinit var mBleGattCallback: BleGattCallback
/**
* 是否連接
*/
private var mIsConnected = false
fun getInstance(context: Context) = instance ?: synchronized(this) {
instance ?: BleCore(context).also {
instance = it
//藍(lán)牙掃描
bleScan = BleScan.getInstance(context)
//初始化
mBleGattCallback = BleGattCallback()
}
}
/**
* 設(shè)備信息
*/
private fun deviceInfo(info: String) = mBleCallback?.deviceInfo(info)
/**
* 連接狀態(tài)
*/
private fun connectState(state: Boolean) {
mIsConnected = state
mBleCallback?.onConnectionStateChange(state)
}
}
同時(shí)在 companion object
外創(chuàng)建一個(gè)函數(shù),代碼如下所示:
fun setBleCallback(bleCallback: BleCallback) {
mBleCallback = bleCallback
}
此函數(shù)和setPhyScanCallback()
函數(shù)是同級(jí)的,下面我們?cè)黾舆B接和斷連的函數(shù)。
二、連接和斷連
在BleCore中增加如下代碼:
/**
* 連接藍(lán)牙設(shè)備
*/
fun connect(device: BluetoothDevice) {
deviceInfo("連接中...")
mGatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
device.connectGatt(context, false, mBleGattCallback, BluetoothDevice.TRANSPORT_LE, BluetoothDevice.PHY_LE_2M_MASK)
} else {
device.connectGatt(context, false, mBleGattCallback)
}
}
/**
* 斷開(kāi)連接
*/
fun disconnect() {
deviceInfo("斷開(kāi)連接...")
mGatt?.disconnect()
}
連接與斷開(kāi)連接,調(diào)用時(shí)會(huì)觸發(fā)onConnectionStateChange()
函數(shù)。
三、連接狀態(tài)回調(diào)
下面修改這個(gè)函數(shù)的代碼,如下所示:
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
val address = gatt.device.address
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
deviceInfo("已連接:$address")
connectState(true)
}
BluetoothProfile.STATE_DISCONNECTED -> {
deviceInfo("已斷開(kāi)連接:$address")
connectState(false)
}
else -> {
Log.d(TAG, "onConnectionStateChange: $status")
connectState(false)
mGatt?.close()
mGatt = null
}
}
}
在回調(diào)中,連接成功和斷開(kāi)連接都會(huì)有一個(gè)對(duì)應(yīng)的狀態(tài)碼,通過(guò)狀態(tài)回調(diào)到接口函數(shù)中,然后回到MainActivity中使用一下這個(gè)回調(diào),首先我們修改一下activity_main.xml
中的代碼,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/orange"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navigationIcon="@drawable/ic_scan_ble"
app:title="GoodBle"
app:titleCentered="true"
app:titleTextColor="@color/white">
<TextView
android:id="@+id/tv_disconnect"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="8dp"
android:visibility="gone"
android:padding="8dp"
android:text="斷開(kāi)連接"
android:textColor="@color/white" />
</com.google.android.material.appbar.MaterialToolbar>
<TextView
android:id="@+id/tv_device_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="設(shè)備信息"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar" />
</androidx.constraintlayout.widget.ConstraintLayout>
在XML中只增加了兩個(gè)TextView,分別用于斷連和顯示設(shè)備狀態(tài),然后我們修改MainActivity中的代碼,如下所示:
class MainActivity : BaseActivity(), BleCallback {
private val binding by viewBinding(ActivityMainBinding::inflate)
private lateinit var bleCore: BleCore
@SuppressLint("MissingPermission")
private val scanIntent =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
if (result.data == null) return@registerForActivityResult
//獲取選中的設(shè)備
val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
result.data!!.getParcelableExtra("device", BluetoothDevice::class.java)
} else {
result.data!!.getParcelableExtra("device") as BluetoothDevice?
}
//連接設(shè)備
if (device != null) bleCore.connect(device)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
bleCore = (application as BleApp).getBleCore()
bleCore.setBleCallback(this@MainActivity)
//進(jìn)入掃描頁(yè)面
binding.toolbar.setNavigationOnClickListener { scanIntent.launch(Intent(this,ScanActivity::class.java)) }
//斷開(kāi)連接
binding.tvDisconnect.setOnClickListener {
binding.tvDisconnect.visibility = View.GONE
bleCore.disconnect()
}
}
override fun deviceInfo(info: String) {
runOnUiThread {
binding.tvDeviceInfo.text = info
}
}
override fun onConnectionStateChange(state: Boolean) {
runOnUiThread {
if (state) binding.tvDisconnect.visibility = View.VISIBLE
}
}
override fun onServicesDiscovered(services: List<BluetoothGattService>) {
}
}
??這里我們首先是通過(guò)Activity Result API的StartActivityForResult()
函數(shù)進(jìn)行頁(yè)面跳轉(zhuǎn),在返回的時(shí)候拿到device對(duì)象,這在前一篇已經(jīng)寫(xiě)好了,拿到device對(duì)象之后調(diào)用BleCore
的connect()
函數(shù)進(jìn)行連接設(shè)備,在onCreate()
函數(shù)中進(jìn)行BleCore的賦值,然后設(shè)置Ble的回調(diào),實(shí)現(xiàn)BleCallback
接口,重寫(xiě)里面的函數(shù),當(dāng)連接成功之后會(huì)通過(guò)回調(diào)deviceInfo()
得到設(shè)備狀態(tài),因?yàn)槭亲泳€程所以在ui線程中渲染UI。而onConnectionStateChange()
函數(shù),回調(diào)連接成功或者失敗,如果成功則為ture,就顯示tvDisconnect
控件,此時(shí)連接成功,點(diǎn)擊這個(gè)tvDisconnect
就會(huì)斷開(kāi)連接,點(diǎn)擊監(jiān)聽(tīng)就在onCreate()
中寫(xiě)好了,下面我們運(yùn)行一下看看效果。
從這個(gè)效果圖來(lái)看,我們連接成功之后有狀態(tài),點(diǎn)擊斷開(kāi)連接也會(huì)有狀態(tài)改變,那么連接就寫(xiě)好了。
四、發(fā)現(xiàn)服務(wù)
??連接寫(xiě)好了,下面可以寫(xiě)發(fā)現(xiàn)服務(wù)了,我們可以在連接成功的處理中進(jìn)行發(fā)現(xiàn)服務(wù),下面我們修改一下BleGattCallback
中的onConnectionStateChange()
函數(shù)中的代碼,如下圖所示:
通過(guò)gatt.discoverServices()
進(jìn)行發(fā)現(xiàn)服務(wù)的動(dòng)作,在此之前通過(guò)deviceInfo設(shè)置當(dāng)前的動(dòng)作狀態(tài),發(fā)現(xiàn)服務(wù)執(zhí)行會(huì)觸發(fā)onServicesDiscovered()
回調(diào),在這個(gè)回調(diào)中我們可以回調(diào)到頁(yè)面,修改代碼如下所示:
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) {
deviceInfo("發(fā)現(xiàn)了 ${gatt.services.size} 個(gè)服務(wù)")
gatt.services?.let { mBleCallback?.onServicesDiscovered(it) }
}
}
在回調(diào)中設(shè)置發(fā)現(xiàn)服務(wù)的個(gè)數(shù),然后回調(diào),因?yàn)榉?wù)是多個(gè)的,那么下面我們就需要使用一個(gè)列表是裝載服務(wù),首先我們修改一下activity_main.xml,在里面增加一個(gè)RecyclerView,代碼如下所示:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout...>
...
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_service"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_device_info" />
</androidx.constraintlayout.widget.ConstraintLayout>
五、服務(wù)適配器
??要顯示服務(wù)列表數(shù)據(jù),首先需要一個(gè)適配器,而適配器又需要一個(gè)item去渲染數(shù)據(jù),下面我們?cè)趌ayout下創(chuàng)建一個(gè)item_service.xml
,代碼如下所示:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/item_service"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:background="@color/white"
android:orientation="vertical">
<TextView
android:id="@+id/tv_service_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:text="服務(wù)"
android:textColor="@color/black"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_uuid_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="UUID:"
app:layout_constraintStart_toStartOf="@+id/tv_service_name"
app:layout_constraintTop_toBottomOf="@+id/tv_service_name" />
<TextView
android:id="@+id/tv_service_uuid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="UUID"
android:textColor="@color/black"
app:layout_constraintBottom_toBottomOf="@+id/tv_uuid_title"
app:layout_constraintStart_toEndOf="@+id/tv_uuid_title"
app:layout_constraintTop_toTopOf="@+id/tv_uuid_title" />
<TextView
android:id="@+id/tv_service_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="PRIMARY SERVICE"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/tv_service_name"
app:layout_constraintTop_toBottomOf="@+id/tv_uuid_title" />
</androidx.constraintlayout.widget.ConstraintLayout>
下面我們?cè)赽le包下新建一個(gè)BleUtils
類(lèi),代碼如下所示:
object BleUtils {
private val generic = "-0000-1000-8000-00805F9B34FB"
/**
* 獲取藍(lán)牙服務(wù)名稱(chēng)
* @param uuid UUID
*/
fun getServiceName(uuid: UUID) =
when ("0x${uuid.toString().substring(4, 8).uppercase(Locale.getDefault())}") {
"0x1800" -> "Generic Access service"
"0x1801" -> "Generic Attribute service"
"0x1802" -> "Immediate Alert service"
"0x1803" -> "Link Loss service"
"0x1804" -> "Tx Power service"
"0x1805" -> "Current Time service"
"0x1806" -> "Reference Time Update service"
"0x1807" -> "Next DST Change service"
"0x1808" -> "Glucose service"
"0x1809" -> "Health Thermometer service"
"0x180A" -> "Device Information service"
"0x180D" -> "Heart Rate service"
"0x180E" -> "Phone Alert Status service"
"0x180F" -> "Battery service"
"0x1810" -> "Blood Pressure service"
"0x1811" -> "Alert Notification service"
"0x1812" -> "Human Interface Device service"
"0x1813" -> "Scan Parameters service"
"0x1814" -> "Running Speed and Cadence service"
"0x1815" -> "Automation IO service"
"0x1816" -> "Cycling Speed and Cadence service"
"0x1818" -> "Cycling Power service"
"0x1819" -> "Location and Navigation service"
"0x181A" -> "Environmental Sensing service"
"0x181B" -> "Body Composition service"
"0x181C" -> "User Data service"
"0x181D" -> "Weight Scale service"
"0x181E" -> "Bond Management service"
"0x181F" -> "Continuous Glucose Monitoring service"
"0x1820" -> "Internet Protocol Support service"
"0x1821" -> "Indoor Positioning service"
"0x1822" -> "Pulse Oximeter service"
"0x1823" -> "HTTP Proxy service"
"0x1824" -> "Transport Discovery service"
"0x1825" -> "Object Transfer service"
"0x1826" -> "Fitness Machine service"
"0x1827" -> "Mesh Provisioning service"
"0x1828" -> "Mesh Proxy service"
"0x1829" -> "Reconnection Configuration service"
"0x183A" -> "Insulin Delivery service"
"0x183B" -> "Binary Sensor service"
"0x183C" -> "Emergency Configuration service"
"0x183D" -> "Authorization Control service"
"0x183E" -> "Physical Activity Monitor service"
"0x183F" -> "Elapsed Time service"
"0x1840" -> "Generic Health Sensor service"
"0x1843" -> "Audio Input Control service"
"0x1844" -> "Volume Control service"
"0x1845" -> "Volume Offset Control service"
"0x1846" -> "Coordinated Set Identification service"
"0x1847" -> "Device Time service"
"0x1848" -> "Media Control service"
"0x1849" -> "Generic Media Control service"
"0x184A" -> "Constant Tone Extension service"
"0x184B" -> "Telephone Bearer service"
"0x184C" -> "Generic Telephone Bearer service"
"0x184D" -> "Microphone Control service"
"0x184E" -> "Audio Stream Control service"
"0x184F" -> "Broadcast Audio Scan service"
"0x1850" -> " Published Audio Capabilities service"
"0x1851" -> "Basic Audio Announcement service"
"0x1852" -> "Broadcast Audio Announcement service"
"0x1853" -> "Common Audio service"
"0x1854" -> "Hearing Access service"
"0x1855" -> "Telephony and Media Audio service"
"0x1856" -> "Public Broadcast Announcement service"
"0x1857" -> "Electronic Shelf Label service"
else -> "Unknown Service"
}
fun getServiceUUID(uuid: UUID) =
"0x${uuid.toString().substring(4, 8).uppercase(Locale.getDefault())}"
}
??這里需要說(shuō)明一下藍(lán)牙的UUID,藍(lán)牙UUID(Universally Unique Identifier)
是用于唯一標(biāo)識(shí)藍(lán)牙設(shè)備和服務(wù)的一種標(biāo)識(shí)符。它是一個(gè)128位長(zhǎng)的數(shù)字,在藍(lán)牙通信中起到唯一標(biāo)識(shí)的作用。藍(lán)牙UUID按照標(biāo)準(zhǔn)分為兩種類(lèi)型:
-
16位UUID:這些UUID通常用于藍(lán)牙標(biāo)準(zhǔn)定義的一些通用服務(wù)和特性。例如,設(shè)備名稱(chēng)服務(wù)的UUID是 00001800-0000-1000-8000-00805F9B34FB。
-
128位UUID:這些UUID通常用于自定義的服務(wù)和特性,以確保全球唯一性??梢宰孕猩梢粋€(gè)128位的UUID作為自定義的服務(wù)或特性標(biāo)識(shí)。例如,一個(gè)自定義的服務(wù)UUID可以是 0000XXXX-0000-1000-8000-00805F9B34FB,其中的 XXXX 部分可以是任意的16進(jìn)制數(shù)字。
在藍(lán)牙通信中,設(shè)備使用UUID來(lái)發(fā)布和查找服務(wù)以及識(shí)別特性。UUID是藍(lán)牙設(shè)備之間進(jìn)行通信時(shí)的重要標(biāo)識(shí),確保了設(shè)備和服務(wù)的唯一性。
那么getServiceName()
中的鍵你就知道是什么意思了,0x1800就是16進(jìn)制數(shù)字,而對(duì)應(yīng)的值則是SIG
定義的,可以參考這個(gè)文檔:Assigned_Numbers.pdf。如果你的值找不到對(duì)應(yīng)的,那說(shuō)明它不是SIG規(guī)范的,你這個(gè)服務(wù)UUID就是自己公司自定義的。
下面我們寫(xiě)適配器,在adapter包下新建一個(gè)ServiceAdapter
類(lèi),代碼如下所示:
class ServiceAdapter(
private val services: List<BluetoothGattService>
) : RecyclerView.Adapter<ServiceAdapter.ViewHolder>() {
private var mOnItemClickListener: OnItemClickListener? = null
fun setOnItemClickListener(mOnItemClickListener: OnItemClickListener?) {
this.mOnItemClickListener = mOnItemClickListener
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val viewHolder = ViewHolder(ItemServiceBinding.inflate(LayoutInflater.from(parent.context), parent, false))
viewHolder.binding.itemService.setOnClickListener { mOnItemClickListener?.onItemClick(it, viewHolder.adapterPosition) }
return viewHolder
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.binding.tvServiceName.text = BleUtils.getServiceName(services[position].uuid)
holder.binding.tvServiceUuid.text = BleUtils.getServiceUUID(services[position].uuid)
}
override fun getItemCount() = services.size
class ViewHolder(itemView: ItemServiceBinding) : RecyclerView.ViewHolder(itemView.root) {
var binding: ItemServiceBinding
init {
binding = itemView
}
}
}
這里的代碼就是比較簡(jiǎn)單的,就是基本的寫(xiě)法,下面回到MainActivity中進(jìn)行顯示數(shù)據(jù)。
六、顯示服務(wù)
首先聲明變量:
private var mServiceAdapter: ServiceAdapter? = null
private val mServiceList: MutableList<BluetoothGattService> = mutableListOf()
然后實(shí)現(xiàn)OnItemClickListener
接口
class MainActivity : BaseActivity(), BleCallback, OnItemClickListener {
重寫(xiě)onItemClick()函數(shù)。
override fun onItemClick(view: View?, position: Int) {
showMsg(mServiceList[position].uuid.toString())
}
修改onServicesDiscovered()
函數(shù),代碼如下所示:
override fun onServicesDiscovered(services: List<BluetoothGattService>) {
runOnUiThread {
mServiceList.clear()
mServiceList.addAll(services)
mServiceAdapter ?: run {
mServiceAdapter = ServiceAdapter(mServiceList)
binding.rvService.apply {
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
layoutManager = LinearLayoutManager(this@MainActivity)
adapter = mServiceAdapter
}
mServiceAdapter!!.setOnItemClickListener(this@MainActivity)
mServiceAdapter
}
mServiceAdapter!!.notifyDataSetChanged()
}
}
這里的寫(xiě)法其實(shí)和掃描設(shè)備哪里如出一轍,下面我們運(yùn)行一下看看,什么效果。
七、源碼
如果對(duì)你有所幫助的話,不妨 Star 或 Fork,山高水長(zhǎng),后會(huì)有期~文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-792365.html
源碼地址:GoodBle文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-792365.html
到了這里,關(guān)于Android Ble藍(lán)牙App(二)連接與發(fā)現(xiàn)服務(wù)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!