尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Delphi经典蓝牙控件设计与Android真机联调实战

Delphi经典蓝牙控件设计与Android真机联调实战 简介面向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: TProcTBytes; FOnDisconnected: TProc; public function Connect(const AMac, AUUID: string): Boolean; procedure SendBytes(const AData: TBytes); procedure Disconnect; property Connected: Boolean read FConnected; property OnDataReceived: TProcTBytes 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:nameandroid.permission.BLUETOOTH android:maxSdkVersion30 / uses-permission android:nameandroid.permission.BLUETOOTH_ADMIN android:maxSdkVersion30 / uses-permission android:nameandroid.permission.BLUETOOTH_SCAN / uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT / uses-permission android:nameandroid.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 UUIDApp 频繁闪退蓝牙 Socket 在工作线程回调中访问 UI使用TThread.Queue切回主线程还有一个容易被忽略的点Android 模拟器默认不带蓝牙硬件无论怎么配置权限都扫不到设备。如果你用 Delphi 自带的 Android Emulator 调试建议直接放弃这个思路改用真机环境问题会少很多。4.2 连接不稳、数据乱码的排查方向连接稳定性问题首当其冲是“重复连接”。我见过不少人写了Connect后发现没连上也不调用Disconnect又调用一次Connect结果第二次必然失败。底层 Socket 对象只要连接过一次失败状态就不可复用了必须把旧对象释放掉重新创建。所以在TClassicBTClient实现里Connect第一步一定会先检查并清理旧 Socket这个细节虽然不起眼但能省一大半连接问题。数据乱码主要看三点。第一字符串编码是否一致Android 端默认用 UTF-8Delphi 侧必须统一转换成 UTF-8不要直接用AnsiString拼接。第二发送和接收的流式处理是否正确短数据一帧能收完长数据可能分多段如果外部代码只处理第一段就会丢数据。第三外设本身的波特率配置经典蓝牙 RFCOMM 虽然不暴露波特率参数但有些工控模块内部还有串口配置必须和模块侧保持一致。4.3 控件落地后的维护建议自研控件做完不是终点后面维护更要花心思。我的经验是在控件单元顶部写清楚支持的最低 Android 版本和 Delphi 版本并留一个常量数组保存相关信息。因为这个控件可能被多个项目引用两年后新项目改需求时回来看代码的人已经不是当初的作者注释写清楚能省掉无数沟通成本。另一个建议是把“连接状态事件”做成统一入口比如OnStatusChanged(AStatus: TBTStatus)。项目里如果既有扫码枪又有打印机不同页面可以根据状态值分别展示提示文案而不是每个页面各自监听一堆底层回调后续加心跳检测、重连策略都好扩展。实测下来这种事件模型的代码结构比“到处注册回调”干净很多最后收尾时也能让新人快速上手。个人体会是经典蓝牙在 Delphi 世界确实冷门但并不神秘核心就是把 Socket 连接、字节流传输、线程切换和权限处理这四个点吃透。如果你也是刚入门建议先写一个“只加载已配对设备并发送固定字符串”的最小 Demo跑通之后再逐步加扫描、加自动重连、加协议解析整个链路很快就会清晰起来。后续还可以在这个控件基础上扩展心跳包机制和断线自动重连做工业场景的老手知道这两项才是稳定性的关键。本文还有配套的精品资源点击获取
返回列表