简介:面向Delphi开发者的Android蓝牙开发资源,内含经典蓝牙控件源码及配套演示工程,解决移动端设备间无线通信需求。资源共160个文件,压缩包仅1.03MB,以.pas与.fmx源码、.dpr/.dproj工程文件、.dcu编译单元及配置文件为主,并包含部署配置与签名文件,整体轻量精炼。已有1189人学习浏览。包体分为CBT_Component控件库与CBT_Demo演示程序两层:前者提供TBluetoothManager、TBluetoothDevice、TBluetoothService、TBluetoothSocket等组件,覆盖设备开关、扫描、连接、服务发现及Socket数据收发;后者为完整的Android蓝牙示例工程,演示设备列表展示、连接状态反馈、收发数据线程处理、权限配置与异常处理。资源还涉及FMX跨平台界面、事件驱动编程和蓝牙通信协议等关键知识点,并针对Android权限声明、设备兼容性差异、用户交互反馈及安全隐私控制给出可参考的处理思路,适合中高级Delphi开发者快速移植或二次扩展,也可作为初学者理解蓝牙原理的实操参考。
1. 为什么我又造了一个蓝牙控件
1.1 经典蓝牙和低功耗蓝牙,别选错了
做 Android 工控类 App 这几年,我最常被问到的技术点,就是 Delphi 下怎么处理经典蓝牙。这里说的经典蓝牙,不是 BLE,而是基于 RFCOMM 的 SPP 串口协议,也就是我们连接 HC-05 蓝牙串口模块、OBD 汽车诊断盒子、蓝牙热敏打印机、工业扫码枪时最常用的那类通道。很多新手一上来就搜“Delphi 蓝牙控件”,结果找到的多半是低功耗蓝牙 BLE 的封装库,买回来才发现根本连不上手头的外设,原因就是两者走的协议栈、UUID、连接方式完全不一样。
经典蓝牙适合传输数据量中等、对实时性和兼容性要求更高的场景。BLE 虽然省电、连接快,但一次最多传 20 字节左右的有效载荷,还要做 MTU 协商、特征值读写,对于很多老式串口模块根本不适用。而经典蓝牙的 RFCOMM 相当于一个虚拟串口,用户可以像操作 COM 口一样发数据,底层还自动做了分片和重组,上手门槛低不少。所以我们项目里保留了一套面向经典蓝牙的自研控件,配套一个 Android 平台的 Demo,今天把整体设计和踩坑记录整理出来,给有同样需求的人一个参考。
1.2 自研控件的边界:能省则省
决定自己封装控件之前,我也犹豫过要不要直接上商业 UI 组件。后来算了一笔账:项目只需要“扫描设备、连接 RFCOMM 通道、收发字节流、断线监听”这四个能力,商业控件虽然漂亮,但有大量我用不到的 BLE 功能,授权费还不便宜。更重要的是,外设协议各家有各家的私有帧格式,控件封装越重,越难做定制化改造。
这版控件的设计原则就三条。第一,只做通用连接层,不掺业务协议;第二,代码尽量薄,方便客户改源码;第三,所有异步回调都在主线程触发,避免开发者在 FMX 界面里手动切换线程。按这个边界做出来的控件,单个单元文件不到 800 行,接入新项目时只需要复制两个 pas 文件就能编译,维护成本非常低。
2. 控件整体设计与核心类
2.1 设备发现层:扫描和配对列表复用
控件对外暴露了两个设备来源。一个是系统已配对设备列表,这个最稳定,绝大多数生产环境都建议直接用;另一个是主动扫描周边设备,适合第一次部署时做配对的场景。我封装成一个TClassicBTScanner类,内部管理一个设备列表,每条记录包含设备名称、MAC 地址、配对状态三个字段。
扫描这块要特别注意,Android 从 6.0 开始,经典蓝牙扫描需要定位权限,Android 12 之后又把蓝牙权限拆成了BLUETOOTH_SCAN和BLUETOOTH_CONNECT,不处理这些运行时权限,扫描回调就是一个空列表。所以我把权限检查和申请也封装进了扫描器:调用StartScan时,如果发现当前权限不足,会先弹权限申请,等用户授权后再真正发起底层扫描。这样 Demo 代码就清爽了,不用每个页面都写一遍权限逻辑。
2.2 连接层:RFCOMM 通道封装
连接层是整个控件的核心,对应一个TClassicBTClient类。它的职责是接收一个 MAC 地址和一个 UUID,建立 RFCOMM 通道,然后向上层提供SendBytes和Disconnect两个公开方法。下面这段不是完整源码,是我对外的核心接口,方便理解后续 Demo 怎么调。
type TClassicBTClient = class private FSocket: TBluetoothSocket; FConnected: Boolean; FOnDataReceived: TProc<TBytes>; FOnDisconnected: TProc; public function Connect(const AMac, AUUID: string): Boolean; procedure SendBytes(const AData: TBytes); procedure Disconnect; property Connected: Boolean read FConnected; property OnDataReceived: TProc<TBytes> read FOnDataReceived write FOnDataReceived; property OnDisconnected: TProc read FOnDisconnected write FOnDisconnected; end;经典蓝牙的连接原理说起来不复杂:系统根据设备 MAC 地址找到远程设备,再根据 UUID 匹配到对应的 SPP 服务,最后建立一条类似 TCP 的双向数据管道。实际开发中,UUID 用错是最典型的连接失败原因,串口模块一般都走00001101-0000-1000-8000-00805F9B34FB,也就是 Serial Port Profile 的默认 UUID。但个别厂商会把 UUID 改掉,买来的模块不一样,必须先确认硬件手册。
2.3 数据层:粘包、断线、线程回调
数据收发这块,我踩过最大的坑就是“把蓝牙 Socket 当普通 TCP Socket 写”。在 Delphi 里直接用TBluetoothSocket.ReadData这类方法,默认行为可能一次只返回几个字节,也可能把多帧数据拼在一起返回,完全取决于系统缓冲区和处理时序。所以控件内部维护了一个MemoryStream作为接收缓冲区,每次收到原始字节先追加进去,再交给外部业务层按自己的帧协议解析。
业务层解析怎么做?我的建议是定义三要素:帧头、长度、校验。比如帧头用0xAA 0x55,第 3 字节表示内容长度,后续内容做 CRC 校验。控件不做拆分,只保证把完整数据流透传出来,业务层拿到处理后,再返回剩余未消费的字节。这样做的好处是,将来接不同外设时,连接层完全不用改。
另一个容易犯的错是线程回调。Delphi 的蓝牙 API 接收数据时通常跑在系统工作线程,如果直接在这个线程里弹一个ShowMessage,轻则界面卡顿,重则闪退。我的做法是在TClassicBTClient内部记录一个TThreadID,收到数据后通过TThread.Queue切回主线程,再触发外部事件。这样接入方写起事件响应函数就跟写普通 UI 事件一样自然。
3. Demo 从零跑通:Android 真机联调
3.1 权限配置与动态申请
Demo 采用最常见的页面结构:一个TListView显示设备列表,一个“扫描”按钮,一个“连接”按钮,一个TMemo显示接收日志,再加一个TEdit输入要发送的内容。编译前需要在项目设置的 Android Manifest 里加上蓝牙权限,分安卓版本看,完整配置基本是这个样子。
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" /> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />注意后三个权限在 Android 12 以上属于运行时权限,仅在 Manifest 里声明还不够,还要在代码里调用请求。Delphi 里可以用PermissionsService.RequestPermissions来申请,建议在FormCreate里统一发起,把结果回调保存到一个布尔变量,等用户点击“扫描”时再检查一遍。实测下来,如果权限没有全部就位,底层扫描很容易直接返回空列表,还不会抛异常,新手排查半天也想不通。
3.2 核心步骤:发现、连接、收发
第一个按钮是加载已配对设备,这个最简单,调用TClassicBTScanner.LoadPairedDevices,把 MAC 列表刷到TListView上。第一次调试我建议只做这个功能,因为它对权限要求最苛刻,能跑通,说明环境基本没问题。
第二步才是主动扫描。扫描的回调可能持续好几秒,期间不要刷新整个 ListView,否则列表会频繁跳动。我的做法是先清空列表,然后开一个“扫描中”的提示,每个设备回调回来时用TListView.Items.Add追加一行,扫描结束后再恢复按钮状态。真机上扫描速度取决于周围设备数量,通常三到五秒出结果。
第三步是连接。连接按钮按下后,取得当前选中设备条目的 MAC 地址,调用TClassicBTClient.Connect。这里有一个很容易犯的低级错误:扫描列表里的设备不一定已经配对,而 RFCOMM 连接通常要求先配对。在 Demo 里,我是点击设备时先判断IsPaired,如果没配对就调用系统配对方法;但系统配对会弹系统级对话框,自动化测试不好处理,所以生产环境更推荐让用户提前在手机设置里完成配对,再进 App 使用。
连接成功后,发送文本就非常简单了。SendBytes内部把字符串转成 UTF-8 字节数组,再写入 Socket。接收端回调里再把TBytes转回字符串,追加到日志框。第一个能互发字符串的 Demo 出来,整个链路基本就通了一半。
3.3 Demo 界面和调试技巧
界面布局我用的是单页面,没有做多页导航,这样发布出来给别人看的时候,结构最简单。左边设备区,右边日志区,中间一个连接状态栏,底部是数据发送区。为了能看到原始十六进制数据,日志框我做了双份输出:一份按 UTF-8 解码后的可读文本,一份按字节转十六进制字符串。很多外设调试问题其实都藏在原始字节里,比如出现过几次字符串显示正常,但设备就是不动作,最后发现是少了结尾的回车换行\r\n。
再分享一个调试技巧:不要一上来就连真实设备,先用两个蓝牙串口模块配对,或者用手机上的“蓝牙串口助手”App 做对端服务端,PC 端写好小工具来回发数据。这样能先把控件的问题跟外设协议的问题分开,等通道稳定了再对接硬件。我见过太多人把设备协议问题归咎于控件,最后发现是外设手册里的帧格式看错了。
4. 常见问题排查与避坑清单
4.1 Android 版本差异带来的问题
经典蓝牙在 Android 上的权限演进非常折腾,我用一个表格把典型问题整理出来,方便对照排查。
| 现象 | 常见原因 | 处理方式 |
|---|---|---|
| 点击扫描无任何返回 | 缺少定位权限或蓝牙扫描权限 | 检查 Manifest 和运行时权限 |
| 已配对设备列表为空 | 未授予BLUETOOTH_CONNECT权限(Android 12+) | 申请权限后在OnPermissionResult里重新加载 |
| 能搜索到但无法连接 | 设备进入可发现模式后超时 | 重新触发配对或重启外设 |
| 连接后收发都无响应 | UUID 与外设服务不匹配 | 用 nRF Connect 等工具查看外设 SPP UUID |
| App 频繁闪退 | 蓝牙 Socket 在工作线程回调中访问 UI | 使用TThread.Queue切回主线程 |
还有一个容易被忽略的点,Android 模拟器默认不带蓝牙硬件,无论怎么配置权限都扫不到设备。如果你用 Delphi 自带的 Android Emulator 调试,建议直接放弃这个思路,改用真机,环境问题会少很多。
4.2 连接不稳、数据乱码的排查方向
连接稳定性问题,首当其冲是“重复连接”。我见过不少人写了Connect后发现没连上,也不调用Disconnect,又调用一次Connect,结果第二次必然失败。底层 Socket 对象只要连接过一次失败,状态就不可复用了,必须把旧对象释放掉重新创建。所以在TClassicBTClient实现里,Connect第一步一定会先检查并清理旧 Socket,这个细节虽然不起眼,但能省一大半连接问题。
数据乱码主要看三点。第一,字符串编码是否一致,Android 端默认用 UTF-8,Delphi 侧必须统一转换成 UTF-8,不要直接用AnsiString拼接。第二,发送和接收的流式处理是否正确,短数据一帧能收完,长数据可能分多段,如果外部代码只处理第一段就会丢数据。第三,外设本身的波特率配置,经典蓝牙 RFCOMM 虽然不暴露波特率参数,但有些工控模块内部还有串口配置,必须和模块侧保持一致。
4.3 控件落地后的维护建议
自研控件做完不是终点,后面维护更要花心思。我的经验是,在控件单元顶部写清楚支持的最低 Android 版本和 Delphi 版本,并留一个常量数组保存相关信息。因为这个控件可能被多个项目引用,两年后新项目改需求时,回来看代码的人已经不是当初的作者,注释写清楚能省掉无数沟通成本。
另一个建议是把“连接状态事件”做成统一入口,比如OnStatusChanged(AStatus: TBTStatus)。项目里如果既有扫码枪又有打印机,不同页面可以根据状态值分别展示提示文案,而不是每个页面各自监听一堆底层回调,后续加心跳检测、重连策略都好扩展。实测下来,这种事件模型的代码结构比“到处注册回调”干净很多,最后收尾时,也能让新人快速上手。
个人体会是,经典蓝牙在 Delphi 世界确实冷门,但并不神秘,核心就是把 Socket 连接、字节流传输、线程切换和权限处理这四个点吃透。如果你也是刚入门,建议先写一个“只加载已配对设备并发送固定字符串”的最小 Demo,跑通之后再逐步加扫描、加自动重连、加协议解析,整个链路很快就会清晰起来。后续还可以在这个控件基础上扩展心跳包机制和断线自动重连,做工业场景的老手知道,这两项才是稳定性的关键。
本文还有配套的精品资源,点击获取