news 2026/11/4 2:13:33

经典蓝牙扫描设备名称为空的机制分析与解决方法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
经典蓝牙扫描设备名称为空的机制分析与解决方法

一句话先答:不是名字"丢了"——名字的获取有一条两级降级机制兜底,最终的空名字是小概率的漏网之鱼(实测 5% 左右),而漏网的根源,是补查"失败一次就永久放弃"。

做蓝牙的兄弟大概都见过这个场景:手机开蓝牙一扫,列表里大部分设备都有名字——"小米电视"、"JBL Flip 6"、"XXX耳机",可总有那么一两条,地址、信号强度都在,名字那栏空着。

我们项目里就是这么撞上的:实测某个海思平台,扫描结果里 name 为空的比例稳定在5% 左右。就是这个不起眼的 5%,逼着我们把经典蓝牙的发现机制从头扒了一遍——结果发现,"广播"这个词用在这儿本身就是个误会,而那 5% 的来源,比想象的有意思得多。

如果你是做车载、做音频、做任何要对接经典蓝牙(BR/EDR)的嵌入式设备,这个现象迟早会砸到你头上:测试拿着扫描列表来问,"这个设备是不是有问题?名字怎么没扫到?是不是被你广播丢了?"

带着这个问题,我读了一轮 Zephyr 蓝牙协议栈(v3.3.0,subsys/bluetooth/host/classic/br.c)的源码。结论比"丢没丢"有意思得多——BT 扫描里的名字,走的是一条两级降级机制,而这条机制的失败路径,狠到很多人想象不到。


一、先把 name 的完整判别树画清楚(本文核心)

先说清一个概念:经典蓝牙的发现流程是发现方主动发 Inquiry,被发现方在 inquiry scan 信道上应答。应答可能带 EIR(Extended Inquiry Result,最多 240 字节扩展数据),名字就藏在 EIR 里。

一条扫描结果最终有没有名字,取决于下面这棵判别树:

这棵树上有三个关键认知,先立住,后面逐个展开:

  1. EIR 里带了名字 = 必然拿到。对支持 EIR 且带名字 TLV 的设备,扫描结果只有"扫到了"和"没扫到"两种情况——没有"扫到了但名字丢了"这个中间态。
  2. EIR 没带名字 ≠ 最终名字为空。协议栈的补查是默认必走的一环,不是可选项——绝大多数 EIR 没带名字的设备,名字都能被补查救回来。
  3. 最终空名字的唯一来源,是补查失败。这才是那 5% 的真正出处。

二、第一级:EIR 应答直接带名字——这条路没有概率

先看第一级。EIR 里为什么会有名字?这涉及一个反直觉的事实:Zephyr host 从头到尾只发过一次Write_Local_Name(写入本地名字),从来没有发过"写 EIR"的命令——EIR 里的名字是controller 自动代劳填进去的:controller 把 local name 填进 EIR 的 0x09(Complete Local Name)TLV,应答 Inquiry 时随包直接发出,不需要发现方二次请求。

所以对 EIR 带名字的设备:发现方一次 Inquiry,名字就在应答里,完整到手。实测规律完全印证:凡是 EIR 里带名字的设备,每次扫描名字都完整,没有丢失。"扫到了但名字丢了"这个猜想,在这条路径上不成立。

那 EIR 里没名字 TLV 的原因有哪几种?

  • 设备压根没设置 local name——Write_Local_Name没写过,controller 想代劳也没料;
  • controller 不代劳——规范要求填,但各家 controller 实现有差异,有的不往 EIR 里填名字;
  • 老古董设备——只回普通 Inquiry Result(无 EIR),名字 TLV 自然无从谈起。

注意这三种情况在发现方 host 眼里是同一件事:收到的 EIR 数据里没有名字 TLV。host 用eir_has_name()(br.c:303)做判断——这个函数只做一件事:在已收到的应答数据里找有没有 0x08/0x09 类型的 TLV,它本身不发任何请求、查不到任何新东西。查到了就完事,查不到,交给下一级。


三、第二级:Remote Name Request 补查——默认必走的一环

Inquiry Complete 之后,host 对本轮所有结果做一次盘点:check_request_name()(br.c:346)遍历每条结果,凡是eir_has_name()返回"没有"的,逐个发Remote Name Request(LMP 层的单挑请求),单独去问对端"你叫什么"。

流程串起来:

  1. Inquiry Result with RSSI→ 登记结果槽位
  2. Extended Inquiry Result→ 保存 EIR(可能含名字),逐条回调bt_br_discovery_cb.recv()
  3. Inquiry Complete→check_request_name()盘点,对无名字的逐个发 Remote Name Request
  4. 名字拿到后以EIR_SHORT_NAMETLV 补进结果(bt_hci_remote_name_request_complete(),br.c:543)
  5. 全部补完,timeout()回调上报最终列表

这就是两级降级:第一级,EIR 应答时直接带名字;第一级落空,第二级 Remote Name Request 单独补查。正因为有这级补查,EIR 没带名字的设备大多数名字都能到手——最终列表里的空名字,实际是小概率事件。

设计意图很清楚:宁可逐个发请求,也要尽量让列表里每一条都有名字——因为经典蓝牙是人选设备,列表里没有名字的设备,用户看不懂、不敢选、等于连不上。这和 BLE 完全是两个世界:BLE 是程序按 MAC 地址选设备,名字缺失不影响连接。这个对比值得单开一篇细说。

到这里一切都很美好。但源码往下读几行,就会撞到那个狠角色——5% 的真正来源。


四、狠角色:失败一次,永久放弃,本轮扫描就地终结

看bt_hci_remote_name_request_complete()(br.c:543)的处理,注意顺序:

priv->resolve_state = RESOLVE_REMOTE_NAME_RESOLVED; // 先无条件标记"已完成" if (evt->status) { goto check_names; // 失败:不写名字,直接跳走 } /* ……只有成功才把名字写进 result->eir */ check_names: if (check_request_name()) { return; // 只重发 state 仍为 PENDING 的设备 } atomic_clear_bit(bt_dev.flags, BT_DEV_INQUIRY); // 全部 RESOLVED → 结束本轮 listener->timeout(discovery_results, discovery_results_count);

两个要命的细节:

第一,失败即终态。resolve_state在检查evt->status之前就被置成了 RESOLVED。即,不管这次 Remote Name Request 成没成功,这条设备都被标记为"处理完了"——而check_request_name()只会对状态还是 PENDING 的设备发请求。失败一次 = 这条设备的名字被永久放弃,本轮扫描里不会再有第二次机会。

而 Remote Name Request 会偶发失败:对端恰好不在 page scan 上、射频抖了、时序没对齐,一次没应上,就成了。这就是实测里那 5% 左右空 name 的机制性来源。每一次补查失败,就落出一条空名字的记录,且当轮无法挽回。

第二,扫描随之终结。当所有设备都进入 RESOLVED(不管成败),BT_DEV_INQUIRY标志被清除,timeout()回调把最终结果上报——含着那条没名字的记录,然后本轮 Inquiry 彻底结束。

对着用户看,现象就是:列表里出现一条地址/信号都在、名字为空的设备,然后扫描停止了。看起来特别像"名字丢了、扫描也坏了",协议栈却按部就班走完了流程,只是那条失败记录被悄悄埋了。


五、解法:重扫一轮,就这一招

搞清机制,解法反而不玄乎。

某条设备没名字,别修"广播",也别怀疑丢包,重新发起一轮 discovery 就行。新一轮会重新 Inquiry、重新走一遍 Remote Name Request,上次要是偶发失败,这次大概率就补上了。反过来,重扫一次能补上名字的,是偶发;重扫十次还是空的,就是那台设备真的没设名字,或不在 page scan 上。

轮次时长给个参考:Inquiry 时长按 HCI Inquiry 的时间片走,单位是 1.28 秒(bt_br_discovery_start()的param->length就是这个单位)。单轮设 12 秒上下(9~10 个时间片单位)比较合适——真实设备一般在 5 秒内就能被扫到,超过 20 秒新增发现明显变少,再等下去收益有限。如果是产品里的持续扫描界面,别停一次就完,15 秒一轮循环重启,顺带把上一轮空名字的设备再补查一次。

想实锤这条链路,开CONFIG_BT_DEBUG_LOG,盯bt_hci_remote_name_request_complete收到的 status 码和check_request_name()的走向,失败还是成功一目了然。

如果你的产品是"被发现方",把Write_Local_Name的名字写对就够了。名字进 EIR 是 controller 代劳的,这一处写对,第一级就稳了,根本走不到那个"失败即放弃"的岔路口。


六、小结

回看开头的判别树,name 的三种命运现在都清楚了:

  • EIR 带名字→ 必然完整到手,100% 稳定,谈不上"丢";
  • EIR 没带→ 默认补查救回,绝大多数仍有名字;
  • 补查失败→ 空 name(实测约 5%),当轮永久放弃,重扫一轮是唯一补救。

顺带留个引子:文中反复提到经典蓝牙的"广播"和 BLE 的广播根本不是一回事。一边是"你喊我应答",一边是"我主动喊你听";一边名字由 controller 代劳进 EIR,一边要应用自己决定放 ad 还是 sd。这两套机制的完整对比,下一篇展开,敬请期待。


你在扫描列表里见过空名字的设备吗?最后查明原因了吗?评论区聊聊。觉得有用点个"在看",让更多踩这个坑的兄弟看到。


系列衔接:本系列下一篇《经典蓝牙 Inquiry 与 BLE 广播机制对比:基于 Zephyr 源码》,讲清两套发现机制的设计哲学差异。

嵌入式 蓝牙 经典蓝牙 Zephyr 源码分析

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/4 2:13:01

C# 系列教程_6 运算符 系统精讲(新手友好・详尽版)

运算符(Operator)就是用来对数据执行运算、比较、判断的符号。参与运算的数据叫做操作数。例:int a 1 2;:运算符1 和 2:操作数1 2:表达式(Expression),表达式运算后会…

作者头像 李华
网站建设 2026/11/4 2:10:28

告别 Node 环境难题!Codex 一键安装包完整实操教程

前言 相比手动安装 Node.js、再逐条敲命令,一键安装包已经把环境配置、依赖安装和客户端部署全部打包好,省去了繁琐的搭建步骤,特别适合不想折腾、希望快速上手的用户。整个过程中只需按提示操作,几分钟就能开始使用。 一、下载…

作者头像 李华