前言

  • 最近业务上需要用到蓝牙与硬件交互,经了解,现有分为传统蓝牙和低功耗蓝牙(BLE),本篇讲解传统蓝牙使用
  • 现在市面上蓝牙模块大多数都支持低功耗蓝牙,传统蓝牙适用于较为耗电的操作,如 Android 设备之间的流式传输和通信等,使用场景有限。
  • Android 平台包含蓝牙网络堆栈支持,此支持能让设备以无线方式与其他蓝牙设备交换数据。应用框架提供通过 Android Bluetooth API 访问蓝牙功能的权限。这些 API 允许应用以无线方式连接到其他蓝牙设备,从而实现点到点和多点无线功能。

权限

  • android 6.0 后需要申请运行时权限
    <uses-permission android:name="android.permission.BLUETOOTH" /><!-- 如果targetSdkVersion<=29,可以声明ACCESS_COARSE_LOCATION代替 --><!-- android 6.0后 需要申请运行时权限--><uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /><uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

设备是否支持蓝牙

  • BluetoothAdapter表示设备自身的蓝牙适配器,整个系统只有一个蓝牙适配器,使用此对象进行交互。
    private val mBluetoothAdapter: BluetoothAdapter? by lazy {BluetoothAdapter.getDefaultAdapter()}/*** 设备是否支持蓝牙*/fun isSupportBluetooth(): Boolean {return mBluetoothAdapter != null}

蓝牙是否打开

   /*** 蓝牙是否打开*/fun isEnabled(): Boolean {return mBluetoothAdapter?.isEnabled == true}

开启蓝牙

  • 这里使用了透明的fragment获取 onActivityResult() 返回的结果,方便处理
    /*** 打开蓝牙** @param activity* @param callback 结果回调*/fun openBluetooth(activity: FragmentActivity, callback: ((Boolean) -> Unit)? = null) {if (!isEnabled()) {val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)if (callback != null) {InvisibleFragment.instance(activity).apply {callResult(REQUEST_ENABLE_BT) { _, _ -> callback.invoke(isEnabled()) }startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)}} else {activity.startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)}} else {Timber.tag(TAG).d("蓝牙已开启")}}

蓝牙改变监听

     /*** 注册蓝牙更改监听器** @param listener 监听*/fun registerBluetoothChangeListener(listener: (Boolean) -> Unit) {this.mBluetoothChangeListener = listenerval filter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)getAppContext().registerReceiver(mBluetoothChangeReceiver, filter)}/*** 注销蓝牙更改监听器*/fun unregisterBluetoothChangeListener() {getAppContext().unregisterReceiver(mBluetoothChangeReceiver)}private val mBluetoothChangeReceiver = object : BroadcastReceiver() {override fun onReceive(context: Context?, intent: Intent?) {if (intent?.action == BluetoothAdapter.ACTION_STATE_CHANGED) {when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 0)) {BluetoothAdapter.STATE_TURNING_ON -> {Timber.tag(TAG).d("蓝牙正在打开")}BluetoothAdapter.STATE_TURNING_OFF -> {Timber.tag(TAG).d("蓝牙正在关闭")}BluetoothAdapter.STATE_ON -> {Timber.tag(TAG).d("蓝牙已开启")mBluetoothChangeListener?.invoke(true)}BluetoothAdapter.STATE_OFF -> {Timber.tag(TAG).d("蓝牙已关闭")mBluetoothChangeListener?.invoke(false)}}}}}

获取已配对设备

  • 被配对是指两台设备知晓彼此的存在,具有可用于身份验证的共享链路密钥,并且能够与彼此建立加密连接。
  • 被连接是指设备当前共享一个 RFCOMM 通道,并且能够向彼此传输数据。当前的 Android Bluetooth API 要求规定,只有先对设备进行配对,然后才能建立 RFCOMM 连接。在使用 Bluetooth API 发起加密连接时,系统会自动执行配对。
   /*** 获取已配对设备*/fun getPairedDevices(): MutableSet<BluetoothDevice>? {return mBluetoothAdapter?.bondedDevices}/*** 可获取到对应的蓝牙设备名称和mac地址*/val list = BluetoothUtils.getPairedDevices()?.map { BluetoothDeviceVo(it.name, it.address) }?.toMutableList()

扫描 / 发现设备

  • 执行设备发现将消耗蓝牙适配器的大量资源。在找到要连接的设备后,使用 cancelDiscovery() 停止发现,然后再尝试连接。此外,不应在连接到设备的情况下执行设备发现,因为发现过程会大幅减少可供任何现有连接使用的带宽。
  • 发现设备,只需调用 startDiscovery()。该进程为异步操作,并且会返回一个布尔值,判断发现进程是否已成功启动。发现进程通常包含约 12 秒钟的查询扫描,随后会对发现的每台设备进行页面扫描,以检索其蓝牙名称。
  • Android 6.0后需要开启定位信息,才可以扫描蓝牙设备。
/*** 扫描/发现设备*/fun scanDevice(callback: (BluetoothDevice?) -> Unit) {this.mScanDeviceCallback = callbackif (mBluetoothAdapter?.startDiscovery() == true) {val filter = IntentFilter(BluetoothDevice.ACTION_FOUND)getAppContext().registerReceiver(mScanDeviceReceiver, filter)} else {Timber.tag(TAG).d("开启扫描设备失败")}}private val mScanDeviceReceiver = object : BroadcastReceiver() {override fun onReceive(context: Context, intent: Intent?) {if (intent?.action == BluetoothDevice.ACTION_FOUND) {val device: BluetoothDevice? =intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)mScanDeviceCallback?.invoke(device)}}}/*** 停止扫描/发现设备*/fun cancelScanDevice() {try {mBluetoothAdapter?.cancelDiscovery()getAppContext().unregisterReceiver(mScanDeviceReceiver)} catch (e: Exception) {e.printStackTrace()}}

位置信息

  /*** 检查是否打开定位*/fun checkLocation(): Boolean {val manager = getAppContext().getSystemService(Context.LOCATION_SERVICE) as LocationManagerreturn LocationManagerCompat.isLocationEnabled(manager)}/*** 打开位置信息*/fun openLocation(activity: FragmentActivity,callback: ((Boolean) -> Unit)? = null) {val locationIntent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)if (callback != null) {InvisibleFragment.instance(activity).apply {callResult(REQUEST_ENABLE_LOCATION) { _, _ -> callback.invoke(checkLocation()) }startActivityForResult(locationIntent, REQUEST_ENABLE_LOCATION)}} else {activity.startActivityForResult(locationIntent, REQUEST_ENABLE_BT)}}

启用可检测性(可被其他设备发现)

  • 启用可检测性若蓝牙未开启可自动开启蓝牙。
  • 默认情况下,设备处于可检测到模式的时间为 120 秒(2 分钟),最高可为3600秒(一小时)。
  • 若可检测时间设为0,则设备将始终处于可检测到模式。此配置安全性低,非常不建议使用。
  • 如果要发起对远程设备的连接,则无需启用设备可检测性。只有当应用对接受传入连接的服务器套接字进行托管时,才有必要启用可检测性,因为在发起对其他设备的连接之前,远程设备必须能够发现这些设备。
    /*** 启用可检测性(可被其他设备发现)* * @param activity* @param time 可检测时间 最高可为3600秒(一小时)* @param callback 开启结果回调*/fun enableDiscoverable(activity: FragmentActivity,time: Int? = null,callback: ((Boolean) -> Unit)? = null) {val discoverableIntent = Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE)time?.let {discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, it)}if (callback != null) {InvisibleFragment.instance(activity).apply {callResult(REQUEST_ENABLE_DISCOVERABLE) { _, resultCode ->callback.invoke(resultCode != Activity.RESULT_CANCELED)}startActivityForResult(discoverableIntent, REQUEST_ENABLE_DISCOVERABLE)}} else {activity.startActivityForResult(discoverableIntent, REQUEST_ENABLE_DISCOVERABLE)}}

可检测状态监听

   /*** 注册蓝牙可检测性监听器*/fun registerBluetoothDiscoverableListener(listener: (Boolean) -> Unit) {this.mBluetoothDiscoverableListener = listenerval filter = IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED)getAppContext().registerReceiver(mBluetoothDiscoverableReceiver, filter)}/*** 注销蓝牙可检测性监听器*/fun unregisterBluetoothDiscoverableListener() {getAppContext().unregisterReceiver(mBluetoothDiscoverableReceiver)}private val mBluetoothDiscoverableReceiver = object : BroadcastReceiver() {override fun onReceive(context: Context?, intent: Intent?) {if (intent?.action == BluetoothAdapter.ACTION_SCAN_MODE_CHANGED) {when (intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE, 0)) {BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE -> {Timber.tag(TAG).d("设备处于可检测到模式")mBluetoothChangeListener?.invoke(true)}BluetoothAdapter.SCAN_MODE_CONNECTABLE -> {Timber.tag(TAG).d("设备未处于可检测到模式,但仍能收到连接")}BluetoothAdapter.SCAN_MODE_NONE -> {Timber.tag(TAG).d("设备未处于可检测到模式,且无法收到连接")mBluetoothChangeListener?.invoke(false)}}}}}

连接设备

  • 如果在两台设备之间创建连接,必须同时实现客户端和服务端机制,因为其中一台设备必须开放服务器套接字,而另一台设备必须使用服务器设备的MAC地址发起连接。服务端设备和客户端设备均会以不同方法获取所需的BluetoothSocket。接受传入连接后,服务器会收到套接字信息。在开启与服务器连接的RFCOMM通道时,客户端会提供套接字信息。
  • 当服务器和客户端在同一 RFCOMM 通道上分别拥有已连接的 BluetoothSocket 时,即可将二者视为彼此连接。这种情况下,每台设备都能获得输入和输出流式传输,并开始传输数据。
  • 如果两台设备之前尚未配对,则在连接过程中,Android 框架会自动向用户显示配对请求通知或对话框。因此,在尝试连接设备时,应用无需担心设备是否已配对。在成功配对两台设备之前, RFCOMM 连接尝试会一直阻塞,并且如果用户拒绝配对,或者配对过程失败或超时,则该尝试便会失败。
  • 一种实现技术是自动将每台设备准备为一个服务器,从而使每台设备开放一个服务器套接字并侦听连接。在此情况下,任一设备都可发起与另一台设备的连接,并成为客户端。或者,其中一台设备可显式托管连接并按需开放一个服务器套接字,而另一台设备则发起连接。

启动服务

  1. 通过调用 listenUsingRfcommWithServiceRecord() 获取 BluetoothServerSocket,这里uuid双方使用同一标识。
  2. 开启子线程调用 accept() 阻塞侦听连接请求,接收请求后关闭当前服务。
  3. 通过keep和mIsAccept判断是否需要保持服务。
    /*** 启动服务* * @param name 服务的可识别名称* @param keep 是否保持服务*/fun start(name: String, keep: Boolean = false) {close()mIsAccept = truemServerSocket = BtUtils.mBluetoothAdapter?.listenUsingInsecureRfcommWithServiceRecord(name, mUUID)mPool.execute {do {while (mIsAccept) {try {mServerSocket?.accept()} catch (e: IOException) {Timber.tag(BtUtils.TAG).d(e, "socket的accept()方法失败")try {mServerSocket?.close()} catch (e: IOException) {Timber.tag(BtUtils.TAG).e(e, "关闭ServerSocket失败")}break}?.also {connected(it)}}} while (keep && mIsAccept)}}/*** 关闭服务*/fun close() {mIsAccept = falsetry {mServerSocket?.close()} catch (e: IOException) {Timber.tag(BtUtils.TAG).e(e, "关闭ServerSocket失败")}}

连接服务/设备

  1. 开启子线程,关闭扫描,通过调用 device.createRfcommSocketToServiceRecord(UUID) 获取 BluetoothSocket对象,UUID双方保持一致,确保device已启动服务。
  2. 通过调用 connect() 发起连接。
    fun connect(device: BluetoothDevice) {mPool.execute {BtUtils.cancelScanDevice()val socket = device.createRfcommSocketToServiceRecord(mUUID)try {socket.connect()} catch (e: IOException) {Timber.tag(TAG).e(e, "连接失败-> deviceName:${device.name}")ThreadUtils.runOnUiThread {mStatusListener?.invoke(device, BtAction.STATE_CONNECT_FAILED, e)}return@execute}connected(socket)}}

管理连接

  1. 通过socket.remoteDevice获取远程的BluetoothDevice对象,获取address(mac)地址,通过map保存已连接的socket。
  2. 开启子线程接收对方传过来的消息
    private fun connected(socket: BluetoothSocket) {val device = socket.remoteDeviceThreadUtils.runOnUiThread { mStatusListener?.invoke(device, BtAction.STATE_CONNECT, null) }Timber.tag(TAG).d("已连接-> deviceName:${device.name};deviceAddress:${device.address}")mBtMap[socket.remoteDevice.address] = socketmPool.execute {receiveMessage(socket)}}private fun receiveMessage(socket: BluetoothSocket) {val inputStream = socket.inputStreamval buffer = ByteArray(1024)val device = socket.remoteDeviceval name = device.namevar numBytes: Intwhile (true) {numBytes = try {inputStream.read(buffer)} catch (e: IOException) {ThreadUtils.runOnUiThread { mStatusListener?.invoke(device, BtAction.STATE_DISCONNECT, e) }Timber.tag(TAG).d(e, "设备 $name 断开连接")mBtMap.remove(device.address)break}val message = String(buffer, 0, numBytes)Timber.tag(TAG).d("接收到设备 $name 的消息:${message}")ThreadUtils.runOnUiThread { mReceiveListener?.invoke(device, message) }}}

发送消息

  /*** 发送消息** @param address 目标设备的mac地址* @param data 数据* @param callback 结果回调*/fun send(address: String, data: ByteArray, callback: ((Boolean) -> Unit)? = null) {val socket = mBtMap[address]if (socket == null) {Timber.tag(TAG).e("未获取到设备mac地址为 $address 的socket,发送消息失败")callback?.invoke(false)return}if (!socket.isConnected) {Timber.tag(TAG).e("未连接到设备mac地址为 $address 的socket,发送消息失败")callback?.invoke(false)return}mPool.execute {try {socket.outputStream.write(data)Timber.tag(TAG).d("发送到设备mac地址为 $address 的消息成功")callback?.let { ThreadUtils.runOnUiThread { it.invoke(true) } }} catch (e: IOException) {callback?.let { ThreadUtils.runOnUiThread { it.invoke(false) } }Timber.tag(TAG).e(e, "发送到设备mac地址为 $address 的消息失败")}}}

断开连接

  fun disconnect(address: String) {val socket = mBtMap[address] ?: return Timber.tag(TAG).e("未获取到设备mac地址为 $address 的socket,断开连接失败")try {socket.close()} catch (e: IOException) {Timber.tag(BtUtils.TAG).e(e, "关闭bluetoothSocket失败")}}

结束语

  • 资料大多来自官网:https://developer.android.google.cn/guide/topics/connectivity/bluetooth
  • 写了个传统蓝牙多人聊天的Demo可以作为参考,地址:https://gitee.com/mayuzyb/sample-code/tree/master/Bluetooth

android 传统蓝牙相关推荐

  1. android 传统蓝牙Bluetooth联通性

    Android平台包含了对蓝牙网络协议栈的支持,它允许一个蓝牙设备跟其他的蓝牙设备进行无线的数据交换.应用程序通过Android蓝牙API提供访问蓝牙的功能.这些API会把应用程序无线连接到其他的蓝牙 ...

  2. Android 传统蓝牙配对连接断开 附demo

    简单的描述下分享给大家个demo: 网上找了一大堆,刚开始配对是没问题的,但是断开连接和连接设备就出现很多报错,用的是 BluetoothSocket.connect,发现完全不是同一个方向,一直报错 ...

  3. Android -传统蓝牙通信聊天

    概述 Android 传统蓝牙的使用,包括开关蓝牙.搜索设备.蓝牙连接.通信等. 详细 代码下载:http://www.demodashi.com/demo/10676.html 原文地址: Andr ...

  4. Android 蓝牙开发(一) -- 传统蓝牙聊天室

    Android 蓝牙开发(一) – 传统蓝牙聊天室 Android 蓝牙开发(三) – 低功耗蓝牙开发 项目工程BluetoothDemo 一.蓝牙概览 以下是蓝牙的介绍,来自维基百科: 蓝牙(英语: ...

  5. android专题-蓝牙扫描、连接、读写

    android专题-蓝牙扫描.连接.读写 概念 外围设备 可以被其他蓝牙设备连接的外部蓝牙设备,不断广播自身的蓝牙名及其数据,如小米手环.共享单车.蓝牙体重秤 中央设备 可以搜索并连接周边的外围设备, ...

  6. Android BLE蓝牙详细解读

    代码地址如下: http://www.demodashi.com/demo/15062.html 随着物联网时代的到来,越来越多的智能硬件设备开始流行起来,比如智能手环.心率检测仪.以及各式各样的智能 ...

  7. 蓝牙协议栈模组在linux ubuntu 跑蓝牙协议栈 --传统蓝牙搜索演示以及实现原理

    零. 概述 主要介绍下用Linux ubuntu虚拟机外接我们的蓝牙扩展版跑蓝牙协议栈的初始化以及搜索演示 一. 声明 本专栏文章我们会以连载的方式持续更新,本专栏计划更新内容如下: 第一篇:蓝牙综合 ...

  8. 传统蓝牙服务问询协议SDP概念

    零.概述 本文主要介绍传统蓝牙SDP的概念,SDP在整个协议栈中的架构,SDP的UUID,服务类,以及服务类属性介绍. 服务发现协议(SDP)为应用程序提供了一种方法来发现哪些服务可用,并确定这些可用 ...

  9. 【转载】传统蓝牙协议栈 串口协议SPP

    零. 概述 主要介绍下蓝牙协议栈(bluetooth stack) 串口协议(bluetooth SPP)Serial Port Profile 协议概念介绍. 一. 声明 本专栏文章我们会以连载的方 ...

最新文章

  1. Consul入门07 - Consul Web界面
  2. 2.3.NLTK工具包安装、分词、Text对象、停用词、过滤掉停用词、词性标注、分块、命名实体识别、数据清洗实例、参考文章
  3. 三角形和矩形傅里叶变换_信号与系统:第三章傅立叶变换2.ppt
  4. 数字图像处理:四连通域与八连通域
  5. python抓取图片_Python3简单爬虫抓取网页图片
  6. win10 漏洞 蓝屏代码
  7. java值传递和引用传递的例子,Java中的值传递和引用传递实例介绍
  8. MFC中如何在CMainFrame类中访问CxxxView视图类中的成员
  9. 苹果mac能安装计算机题库吗,苹果电脑能装windows系统吗_苹果电脑安装windows系统的方法...
  10. 【软件工程】课程设计库存管理系统
  11. 移动硬盘访问错误 - 磁盘结构损坏且无法读取、拒绝访问
  12. WPS如何隔列填充背景颜色
  13. 基于Opencv的开源的中文车牌识别系统
  14. FFmpeg[15] - 从官网下载FFmpeg时的坑,你有遇到吗?
  15. Android 气泡图片
  16. python3的各种经典案例,总共299个案例,直接可以运行(中:100个案例)
  17. 腾讯游戏天美工作室实习感悟
  18. python和excel相关的是什么知识点_Python 与 Excel 不得不说的事
  19. springboot + vue 时区问题
  20. 第六章贪心(三):排序不等式、绝对值不等式

热门文章

  1. xp系统打开itunes显示服务器失败,xp电脑无法读取文件itunes library.itl如何解决?...
  2. 记录深度学习的detection系列过程--RCNN系列
  3. 世界读书日线上知识竞赛答题活动方案及模板分享
  4. Mac dyld: Library not loaded: /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib
  5. 专精特新申报的标准及材料
  6. 数据结构:有向完全图和无向完全图的边数
  7. IINA+ for Mac(在IINA播放器上观看直播)
  8. 正则表达式判断字符串是否全是数字、小数点、正负号组成等
  9. 跟牛老师一起学WEBGIS——WEBGIS基础(WMS服务)
  10. 智能微电网研究(PythonMatlab代码实现)