ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Java蓝牙开发Demo从零到实战:Android平台蓝牙开发全解析

Java蓝牙开发Demo从零到实战:Android平台蓝牙开发全解析 简介Java蓝牙开发demo是一份面向物联网及短距离通信场景的Java蓝牙编程入门示例适合有Java基础、希望掌握JSR-82规范与BlueCove框架的开发者。资源以RAR压缩包发布共17个文件包含5个Java源码和6个编译后的class文件便于对照阅读与运行调试同时附带BlueCove等依赖jar包以及Eclipse项目的prefs、classpath、project等配置文件导入开发环境后即可直接查看工程结构。整套资源仅826KB轻量紧凑。内容围绕设备发现、连接建立、数据传输、服务注册、安全认证及异常处理等关键环节展开完整演示了蓝牙设备从扫描到通信的开发流程并兼顾不同蓝牙版本与协议栈差异对智能家居、健康监测、无线控制等应用场景很有参考价值。目前已有2411人下载学习项目目录结构清晰是一份能边读边练的实用开发Demo。 直接说结论如果你想在Java生态里做蓝牙开发绕不开Android。这倒不是说桌面Java完全没法做蓝牙而是现实中你拿到的需求、能跑通的方案、以及社区里真正有人维护的案例几乎全部集中在Android蓝牙开发这个方向上Java只是语言载体真正的舞台在移动端。这篇内容我会把一套完整的Java蓝牙开发Demo从无到有地拆开讲。不管是经典蓝牙BR/EDR还是低功耗蓝牙BLE从环境准备、基础API、实际代码到调试工具的选择我都会给出可以直接复用的实操方案。适合刚接触蓝牙开发的Java服务端或Android开发也适合那种“领导扔给你一个蓝牙模块让你一周出Demo”的临时接活场景。1. 先把范围框死这个Demo到底做的是哪一层蓝牙在我开始写代码之前有必要先把“蓝牙”这个词拆清楚。很多第一次接触蓝牙开发的同事上来就搜“java蓝牙开发demo”结果下了一堆桌面Java的蓝牙库折腾半天连设备都扫不到原因就是没搞明白蓝牙协议栈的分层和适用范围。1.1 Java蓝牙开发的实际归属地Android而不是纯JavaJava本身只是编程语言真正决定蓝牙能力的是操作系统提供的协议栈和API。Windows和macOS上虽然有Java可以调用的蓝牙库但生态分散、权限繁琐实际项目里几乎遇不到有人用。当年有个叫BlueCove的库可以支持桌面端蓝牙但已经很多年没有实质更新在新系统上问题一堆。真正用得最多的Java蓝牙开发场景是Android应用层开发。Android系统从2.0时代就一直内置蓝牙API到4.3之后加入BLE支持API逐步完善稳定。所以我下面所有代码都基于Android平台来写别在纯Java控制台程序里找蓝牙的出路那是一条死胡同。1.2 两个协议栈经典蓝牙和低功耗蓝牙的区别做蓝牙Demo前第二件要弄清楚的事就是经典蓝牙BR/EDR和低功耗蓝牙BLE的区别。这不是同一个协议的版本号升级而是两套不一样的东西。简单类比经典蓝牙像一条专用的窄公路带宽还行但启动慢、功耗高BLE更像快递柜平时几乎不耗电你要取件的时候才打开柜门通信一下效率极高。从协议栈层面看经典蓝牙对应的是SPP/RFCOMM这类串口仿真协议稳定、支持大数据量传输适合音频、文件传输、串口透传等场景。BLE则基于GATT通用属性协议采用“服务-特征值”模型数据量小、功耗低、连接快适合传感器数据采集、遥控器、信标Beacon等场景。这里有个容易踩坑的点如果你的设备只支持经典蓝牙你用BLE的方式去扫描完全扫不到。反过来也一样。所以拿到任何蓝牙模块第一件事先确认它是双模同时支持两种还是单模然后代码里做分支处理。2. 环境准备Android权限、SDK版本和测试硬件选型环境配置看似简单但实际开发中因为配置不当导致的失败占了很大比例。特别是一些隐藏条件不多跑几次根本发现不了。2.1 权限配置你以为声明了就完了运行时权限才是大头在AndroidManifest.xml里加蓝牙权限是第一步uses-permission android:nameandroid.permission.BLUETOOTH / uses-permission android:nameandroid.permission.BLUETOOTH_ADMIN / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION / uses-permission android:nameandroid.permission.BLUETOOTH_SCAN / uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT /但如果你面向的是Android 12API 31及以上的设备运行时权限请求才是决定成败的关键。以前只声明权限就能扫描现在必须在代码里动态请求BLUETOOTH_SCAN、BLUETOOTH_CONNECT并且还需要位置权限配合用户拒绝任何一个扫描就直接没结果。有个很坑的细节ACCESS_FINE_LOCATION在Android 10以上扫描蓝牙设备时必须声明但很多新手只加了蓝牙权限扫不到设备以为是代码问题其实是被系统静默拦截了。2.2 测试硬件怎么选别随便买最便宜的模块软件Demo跑不通很多时候不是代码问题是硬件选型有问题。我踩过这个坑所以多说两句手机准备两台不同的手机一台做主机跑App一台做从机可开关蓝牙供你扫描。如果只有一台手机你能测通扫描但测不了连接。PC蓝牙适配器如果你在PC上调试注意别买那种免驱但系统适配差的。热词里提到的CSR8510 A10这类老芯片适配器在旧版Windows上插上就能用但在新系统或Linux下驱动支持反而不稳定。调试用可以直接买双模的USB蓝牙适配器能同时兼顾经典蓝牙和BLE两种设备的扫描测试。BLE从机设备没有真实设备的话用手机装一个BLE外设模拟App就能凑合比如LightBlue、nRF Connect这类工具可以模拟出一组服务和特征值完全满足Demo阶段开发调试。3. 经典蓝牙开发RFCOMM通道的连接与通信实践经典蓝牙的开发模型其实比BLE容易理解就是“扫描-配对-建立Socket-收发数据”四个步骤。它在API设计和网络Socket编程非常相似核心用的是BluetoothSocket。3.1 核心Demo流程拆解我用代码串起整个流程先把骨架写出来public class ClassicBluetoothDemo { private BluetoothAdapter bluetoothAdapter; private BluetoothSocket bluetoothSocket; // 1. 获取适配器并打开蓝牙 public void initBluetooth() { bluetoothAdapter BluetoothAdapter.getDefaultAdapter(); if (bluetoothAdapter null) { // 设备不支持蓝牙 return; } if (!bluetoothAdapter.isEnabled()) { // 请求打开蓝牙 } } // 2. 扫描周边经典蓝牙设备 public void startDiscovery() { if (bluetoothAdapter.isDiscovering()) { bluetoothAdapter.cancelDiscovery(); } bluetoothAdapter.startDiscovery(); // 通过BroadcastReceiver监听ACTION_FOUND获得设备Mac和名称 } // 3. 与目标设备建立Socket连接 public void connectDevice(String macAddress) throws IOException { BluetoothDevice device bluetoothAdapter.getRemoteDevice(macAddress); // 这是一个专用于SPP服务的UUID UUID uuid UUID.fromString(00001101-0000-1000-8000-00805F9B34FB); bluetoothSocket device.createRfcommSocketToServiceRecord(uuid); bluetoothSocket.connect(); } }这段代码里有一个非常重要的点就是那个UUID到底怎么来的。00001101-0000-1000-8000-00805F9B34FB是蓝牙SIG组织为SPP服务串口仿真协议分配的固定UUID。大部分串口透传模块比如常见的HC-05、HC-06模块用的都是这个UUID。如果对方设备不是标准SPP服务而是自己定义的服务你就得从设备方拿到对应的UUID否则连接会直接失败或超时。连接是耗时操作一定放在子线程里执行不能阻塞主线程。我自己见过不少同事直接在主线程调connect()结果不是ANR就是崩溃。3.2 数据收发输入输出流的使用与关闭连接成功后就拿到了Socket剩下的操作跟操作普通Socket一模一样public void sendData(String message) throws IOException { OutputStream outputStream bluetoothSocket.getOutputStream(); outputStream.write(message.getBytes(UTF-8)); outputStream.flush(); } public String receiveData() throws IOException { InputStream inputStream bluetoothSocket.getInputStream(); byte[] buffer new byte[1024]; int bytes inputStream.read(buffer); return new String(buffer, 0, bytes, UTF-8); }这里我提醒几个坑读取是一个阻塞操作。inputStream.read()在没有数据到达时会一直卡着所以接收数据必须单独开一个线程循环读取不能跟UI线程混在一起。数据分包问题。如果你一次写入的数据比较大比如超过串口模块的缓冲区模块会自己分包发送接收端一包一包地读可能一次read()根本拿不完完整数据。所以生产级代码需要自己定义协议头、帧长度、校验位这些。Demo阶段可以用短字符串测通就行但心里要清楚真实项目里数据粘包分包逃不掉。蓝牙连接成功后记得在退出页面或断开时关闭Socket、关流、取消Discovery。不然下次连接大概率会失败而且系统日志会给你一个“socket closed”之类模棱两可的报错。这是我见过频率最高的“为什么我第二次连接就连不上”的经典原因。4. BLE开发GATT回调、特征值和数据交互的完整路径BLE相比经典蓝牙结构上多了一个“服务Service”和“特征值Characteristic”的抽象层。初学者往往被这个概念绕晕但如果把它们类比成数据库就很好懂了服务是一张表特征值是表中的字段表里字段可以读、也可以写还可以订阅变化通知。4.1 扫描与连接别在主线程操作GATTBLE扫描用BluetoothLeScanner结果通过ScanCallback回调返回。扫描参数需要设置合理的扫描时间比如10秒就停止否则一直扫描会快速消耗手机电量而且结果回调会非常频繁影响UI性能。拿到设备后连接方式跟经典蓝牙不太一样不能直接Socket而是发起GATT连接BluetoothGatt bluetoothGatt device.connectGatt(context, false, gattCallback);这段代码有几个隐藏条件值得说明connectGatt的第二个参数autoConnect在多数Demo里都传false意思是手机直接主动发起连接。传true时是等待设备主动连接手机这在一些外设场景下有意义但普通开发中建议先用false。BluetoothGattCallback的所有回调包括连接状态变化、服务发现完成、数据到达都在系统Binder线程里回调不是UI线程。要更新界面必须自己切回主线程。这个细节如果没注意你会看到回调里明明拿到了数据但界面一点反应都没有排查半天发现是线程问题。GATT连接本身也有一个坑连接、断开操作之间要有足够的时间间隔连续操作同一个BluetoothGatt对象容易导致系统底层异常。4.2 服务发现与特征值读写一个标准流程连接成功的标志是收到onConnectionStateChange回调且newState为STATE_CONNECTED。这时候必须调用discoverServices()才能拿到设备的服务列表这一步很多人漏掉。服务发现完成后在onServicesDiscovered回调里遍历服务、找到你需要的CharacteristicOverride public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status ! BluetoothGatt.GATT_SUCCESS) { return; } for (BluetoothGattService service : gatt.getServices()) { for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) { // 根据业务的UUID匹配需要读写的特征值 } } }读写特征值的方式也有讲究需要简单的数据交换时直接writeCharacteristic(characteristic)数据作为字节数组传入。想让设备主动推送数据比如心率、温湿度传感器就要setCharacteristicNotification(characteristic, true)然后拿到对应的BluetoothGattDescriptor把通知开关设置为ENABLE_NOTIFICATION_VALUE。Descriptor这一步很关键但特别容易被忽略。**只调用setCharacteristicNotification不往Descriptor里写值设备压根不会主动发数据给你。**这是一个很多初学BLE开发的伙伴会卡很久的地方。4.3 MTUBLE数据传输大小不是想多大就多大经典蓝牙的数据传输可以不管包大小但BLE默认MTU最大传输单元只有23字节其中还包含3字节ATT协议头也就是说单包实际能传输的用户数据只有20字节。如果你的业务数据超过20字节就得自己拆包发送或者通过协商MTU来扩大单包容量bluetoothGatt.requestMtu(247);Android 5.0以上支持这个API。协商结果通过onMtuChanged回调告诉你最终值。很多现成的BLE模块出厂默认支持247或更大的MTU但手机端不主动请求设备也没办法用更大的包通信。热词里有一项“蓝牙br ble区别”我补充一句经典蓝牙SPP不存在MTU这个概念数据流读写随意但BLE的每次写入都需要考虑20字节的默认限制。如果你的Demo是拿BLE透传做数据量较大的业务这个差异会在联调第一天才暴露出来。5. 蓝牙测距的实现思路RSSI转距离的简单估算热搜词里“蓝牙测距”是个高频词。我在实际项目里也做过类似需求一并讲讲。5.1 基于RSSI的测距原理BLE设备在广播包中自带RSSI接收信号强度指示值单位为dBm。RSSI测距的本质就是根据信号衰减模型估算距离最常用的是对数距离路径损耗模型距离 10^((measuredPower - rssi) / (10 * n))其中measuredPower是设备在1米距离时的参考RSSI值通常设备厂家会在广播参数里给出或者你自己实测标定n是环境衰减因子一般取2到4环境越复杂取值越大。5.2 Demo代码实现public static double calculateDistance(int rssi, int measuredPower, double n) { return Math.pow(10, ((double) measuredPower - rssi) / (10 * n)); }这段代码很短但实际用起来误差会让人怀疑人生。我实测在开阔空旷环境下3米以内估算还算靠谱误差大概在1米左右一旦有遮挡或者多径效应比如室内有金属货架、墙边拐角误差直接到5米以上。所以基于RSSI的蓝牙测距只适合做粗略的近距离判断比如“用户是否进入了某个设备3米范围内”不适合做精确定位。真要精准定位得配合多个固定蓝牙信标做三角定位还要引入滤波算法卡尔曼滤波、滑动平均都行这就超出Demo范畴了。演示代码里可以这样简单处理连续采集多个RSSI样本去掉最大值和最小值取平均值后再代入公式。这样至少比单次采样稳定一些。6. 调试与排查抓包工具、日志定位和设备兼容性问题做蓝牙开发有一半时间其实在跟异常和兼容性搏斗。把调试手段用熟练效率能提升一大截。6.1 用Wireshark和日志定位问题热词里“wireshark 蓝牙”和“红米K50怎么进行蓝牙抓包”出现的频率很高。我自己的经验是Android端开启开发者选项里的“蓝牙HCI日志”功能系统会生成一份btsnoop文件。导出后用Wireshark打开就能看到完整的蓝牙协议包交互过程包括连接请求、配对、GATT操作等全部细节。这个文件记录了系统蓝牙协议栈和蓝牙芯片之间的HCI数据能帮你确认到底是App没发出去数据还是设备端没回应还是系统底层把包吞了。抓包时注意开启HCI日志本身可能会引入一点延迟但对调试来说完全值得。6.2 常见且隐蔽的设备兼容性问题不同手机蓝牙协议栈实现其实有细微差异我列几个典型表现同样的代码在小米上扫描正常在某旧款三星上扫不到设备多半是权限或扫描参数问题旧机型对某些扫描设置不兼容可以把扫描模式改成低延迟试试。连接成功后延时很长可能是autoConnect参数传了true设备连接受系统调度时间影响很大。音频设备类BLE/经典蓝牙混合设备比如蓝牙耳机、蓝牙音箱同时支持A2DP音乐播放和HFP通话协议热词里“蓝牙a2dp切sco模式”指的就是语音通话场景下音频通道从A2DP切换到SCO的过程。在App层面做蓝牙开发如果不涉及音频路由一般不用管。但如果你的Demo是做一个蓝牙音频控制器就会遇到切换异常、声音断续等问题需要使用AudioManager的setMode和startBluetoothSco()相关接口去处理。别指望一套代码在所有手机上完美运行Demo阶段至少要在两三个不同品牌的真机上跑一遍才能发现基础问题。7. 总结几个必须留在代码里的好习惯讲到这里整套Demo的骨架已经完整了。最后分享几个我自己长期做蓝牙开发后沉淀的习惯算不上技巧但关键时刻能救命销毁页面时一定要清理资源。蓝牙开发涉及的系统资源非常多扫描回调、连接回调、Socket/InputStream/OutputStream、Gatt客户端任何一个没有及时释放轻则内存泄漏重则下一次连接直接失败。我习惯在onDestroy里统一做Override protected void onDestroy() { super.onDestroy(); if (bluetoothSocket ! null) { try { bluetoothSocket.close(); } catch (IOException ignored) { } } if (bluetoothGatt ! null) { bluetoothGatt.disconnect(); bluetoothGatt.close(); } if (bluetoothAdapter ! null bluetoothAdapter.isDiscovering()) { bluetoothAdapter.cancelDiscovery(); } }把回调线程统一管理起来。BLE回调不在UI线程建议用一个HandlerThread接收所有蓝牙数据再用Handler切回主线程。这样数据流清晰调试日志也好打不会出现多线程竞争数据库或UI的问题。收发数据全部走协议封装。哪怕Demo只有两个指令也建议从一开始就定义数据帧格式和解析器。真实项目和蓝牙模块联调时一定会遇到粘包、半包、乱码问题提前做好协议解析后期省下的排查时间非常多。日志要分级有衔接。蓝牙问题经常要跨模块排查从“App层指令发出”到“系统蓝牙写入”再到“设备端响应”每个环节都要有对应日志。否则出了问题只能干瞪眼。这些内容基本都是我在实际项目里踩过坑之后总结出来的。照着这套路径走一遍整个Java蓝牙开发的脉络会清晰很多后面换任何蓝牙模块、任何业务协议都能很快上手。本文还有配套的精品资源点击获取
返回列表