10 · 蓝牙协议速成(新人第一课)

核心文件:MiBluetoothUtils.kt(CoD 过滤 :141-170)、ConnectionUtils.kt(CarPlay UUID :30-31) 适用范围:新人第一课,协议地基,不依赖具体业务代码。先建心智模型,再看 01-发现流程。 已核实基准:_已核实事实基准 · 日志实证:MANNPROB-2956 bugreport

0. 一句话心智模型

蓝牙有 三层独立状态——bond(配对/密钥)、ACL(链路)、profile(业务),各自可单独成立或失效。新人 90% 的困惑都来自把这三层混成一层。全篇围绕这句话展开。

1. 两套蓝牙:BR/EDR vs BLE

蓝牙从根上分两套物理传输,并存、不兼容、各有用途

维度BR/EDR(经典蓝牙)BLE(低功耗蓝牙)
全称Basic Rate / Enhanced Data RateBluetooth Low Energy
物理信道79 个 1MHz 信道40 个信道,信道间距 2MHz(GFSK 占用带宽约 1MHz)
拓扑微微网(1 主 7 从)1 主多从
吞吐高(Mbps 级)低(kBps 级)
典型用途HFP/PBAP/A2DP(电话/通讯录/音频)钥匙、胎压、手环、Beacon
服务发现SDPGATT/ATT

车机连手机走 BR/EDR(经典蓝牙):手机互联用的 HFP(电话)、PBAP(通讯录)、A2DP(音频)都是经典蓝牙 profile。

⚠️ CarPlay 不是蓝牙 profile。它是 Apple 私有协议,主数据走 WiFi;蓝牙只通过 EIR 中携带的私有 UUID 2d8d2466-... 做”设备识别”(ConnectionUtils.kt:30-31CARPLAY_128UUID:101-107 isCarPlaySupportDevicegetEirDataUuids() 匹配)。EIR UUID 不是 SDP 服务 UUID,更不是 profile。详见 12-Profile体系 的”蓝牙跳板”机制。

协议栈分层图(新人先有这张图):

flowchart LR
    subgraph APP["应用 / profile 层"]
        HFP[HFP 电话]
        PBAP[PBAP 通讯录]
        A2DP[A2DP 音频]
        GATT[GATT 钥匙/传感器]
    end
    subgraph SDP["服务发现层"]
        SDPX[SDP 经典]
        GATTX[GATT/ATT BLE]
    end
    subgraph L2CAP["L2CAP 数据链路"]
    end
    subgraph LINK["链路 / 基带层"]
        ACL1[ACL 链路 BR/EDR]
        ACL2[ACL 链路 LE]
    end
    subgraph PHY["物理射频"]
        BR[BR/EDR 79ch]
        LE[BLE 40ch]
    end
    HFP --> SDPX
    PBAP --> SDPX
    A2DP --> SDPX
    GATT --> GATTX
    SDPX --> L2CAP --> ACL1 --> BR
    GATTX --> L2CAP --> ACL2 --> LE

日志取证(MANNPROB-2956,车机配对 iPhone)

  • 16:24:40.698 createBond ... transport=0 —— 入参 transport=0=TRANSPORT_AUTO(App 不指定,交系统选)
  • 16:24:44.860 bondStateChangeCallback ... Transport: 1 newState: BOND_BONDED —— 协商结果 Transport:1=TRANSPORT_BREDR
  • 配对后 46.583 HFP(HeadsetClient) 自动连接 —— 落定后自动连经典 profile,反证走 BR/EDR

2. 设备怎么被「发现」

flowchart TD
    A[车机 startDiscovery] --> B[BR/EDR Inquiry 主动扫]
    A --> C[BLE Scan 被动收广播]
    D[手机 Advertise 广播自身] --> E[车机收到 RSSI+Name+EIR]
    B --> E
    E --> F[ACTION_FOUND 逐设备上报]
    F --> G[读 Class of Device CoD]
    G --> H{isDeviceClassTypeSupport?<br/>MiBluetoothUtils:141-170}
    H -->|true| I[进可用列表]
    H -->|false| J[丢弃]
  • Inquiry:BR/EDR 主动扫描;Scan:BLE 被动监听广播;Advertise:被发现端不停广播自己。
  • Android BluetoothAdapter.startDiscovery() 同时做 Inquiry + BLE Scan,默认 12s 后自发 ACTION_DISCOVERY_FINISHED

Class of Device(CoD):24bit 类型码,majorDeviceClass 大类有 PHONE / AUDIO_VIDEO / COMPUTER / WEARABLE / PERIPHERAL 等。

为什么本仓扫描列表只显示手机? 直读 MiBluetoothUtils.isDeviceClassTypeSupportMiBluetoothUtils.kt:141-170,注意行号以本仓为准):

condition1 = when (majorDeviceClass) {
    AUDIO_VIDEO, UNCATEGORIZED -> false
    COMPUTER -> deviceClass != COMPUTER_LAPTOP   // 排除笔记本
    WEARABLE -> deviceClass == WEARABLE_PAGER    // 仅寻呼机
    else -> true                                  // PHONE 走这里 → true
}
condition2 = when (deviceClass) {
    AUDIO_VIDEO_CAR_AUDIO -> false               // 屏蔽车机自身
    AUDIO_VIDEO_WEARABLE_HEADSET, HEADPHONES -> false  // 耳机(死代码:condition1 已 false)
    else -> true
}
res = condition1 && condition2

关键结论(已对抗核实,非 bug):因 AUDIO_VIDEO 在 condition1 即 false,所有音视频设备(耳机/音箱/车机自身)都被屏蔽。车机蓝牙列表只显示手机类——这是有意设计:车机蓝牙用于手机互联(电话/通讯录/CarPlay),不连蓝牙耳机听歌。condition2 的耳机分支(:161-164)因 condition1 已 false 而成死代码(防御性冗余)。

3. SDP 与 UUID

SDP(Service Discovery Protocol):经典蓝牙的服务发现协议(BLE 侧对应 GATT/ATT)。配对前后,车机通过 SDP 查询对端”你支持哪些服务(profile)”。

UUID 标识服务:全长 128bit;SIG 预定义 16bit 短码,按规则展开:0000XXXX-0000-1000-8000-00805F9B34FB(XXXX 是 16bit 短码)。

16bit 短码profile(SIG 公布)
0x1108HSP(耳机)
0x110A / 0x110BA2DP Source / Sink
0x111E / 0x111FHFP(角色对应见 12-Profile
0x112E / 0x112FPBAP PCE(客户端)/ PSE(服务端)(profile 本体=0x1130)
2d8d2466-e14d-451c-88bc-7301abea291aCarPlay 私有识别 UUID(非 profile,本仓 ConnectionUtils.kt:30-31

ACTION_UUID 何时来?答:SDP 查询完成后。2956 日志 44.862 BondStateMachine: is bonded, wait for service discovery UUIDs 正是此机制——协议栈 44.860 已 BONDED,但应用层 BONDED 广播被 SDP 服务发现推迟约 1.67s 到 46.534 才发(详见 20-Android框架全景)。

4. ACL 连接 vs Profile 连接

  • ACL(Asynchronous Connection-Less):BR/EDR 链路层”基础管道”。先有 ACL,profile 才能在上面跑。
  • ACTION_ACL_CONNECTED/DISCONNECTED:底层 ACL 链路广播,与 profile 无关
  • profile 连接:HFP/A2DP/PBAP 各自在已建立的 ACL 之上独立连接。

关系:ACL(管道) ⊃ profile(管道上的业务)

2956 实证(ACL 与 profile 解耦)

  • 16:24:41.737 btm_acl_created: Created new ACL connection [BT_TRANSPORT_BR_EDR] —— ACL 链路建立
  • 16:24:46.583 connectEnabledProfile(HeadsetClientService) —— HFP 发起连接
  • ACL 比 HFP 连接早约 4.8 秒,证明 ACL 早已存在、profile 连接是后来的事,二者完全解耦。

5. 地址 / 名称 / 别名

概念本质API可变?
地址(MAC)48bit 出厂烧录唯一标识getAddress()否(出厂固定)
名称(Name)对端广播的友好名(EIR/local name)getName()是(对端可改);扫描早期可能 null
别名(Alias)本地用户起的备注名,UI 显示时覆盖 NamegetAliasName() / setAlias()是(仅本地存储)

代码里 UI 显示别名优先;日志里 address 是唯一稳定主键(去重、mPendingDevices 按 MAC put)。

6. 三层状态独立(重点图)

逐个证伪常见混淆:

混淆证伪
「配对=连接」错。bond 只交换并落盘密钥,不建 ACL,更不连 profile。2956 中 44.860 BONDED、46.583 才 HFP 连接。
「发现到=可连接」错。Inquiry 只听见设备在广播,能否连接还看对端是否可连接、信号是否够。
「bond 完成=能用」错。bond 完只代表有钥匙;要用 HFP 还得 ACL up + HFP profile connect。

三层状态图(必须内化)——bond / ACL / profile 各有独立状态机:

stateDiagram-v2
    direction LR
    state "Bond 层(密钥/配对)" as Bond {
        [*] --> BOND_NONE
        BOND_NONE --> BOND_BONDING: createBond
        BOND_BONDING --> BOND_BONDED: SSP成功+密钥落盘
        BOND_BONDED --> BOND_NONE: removeBond
        BOND_BONDING --> BOND_NONE: cancelBondProcess(仅BONDING有效)
    }
    state "ACL 层(链路管道)" as ACL {
        [*] --> ACL_DOWN
        ACL_DOWN --> ACL_UP: ACTION_ACL_CONNECTED
        ACL_UP --> ACL_DOWN: ACTION_ACL_DISCONNECTED
    }
    state "Profile 层(HFP/PBAP/A2DP)" as Prof {
        [*] --> DISCONNECTED
        DISCONNECTED --> CONNECTED: profile.connect(需ACL_UP)
        CONNECTED --> DISCONNECTED: profile.disconnect / ACL_DOWN
    }
    note right of Bond: bond 可在无 ACL 时长期存在<br/>(已配对设备离线仍 remembered)
    note right of ACL: ACL_UP 通常需 BOND_BONDED
    note right of Prof: 每个 profile 独立状态<br/>共享同一条 ACL

关于 2956 的 onCancel bug(归因纠正):该 bug 本质是 bond 状态机内部的取消语义错误——onCancel 误把 cancelBondProcess(只对 BOND_BONDING 有效)当成能撤销 BOND_BONDED 的键,协议栈 IDLE 态空转(Res: 11)。它不是 bond/ACL/profile 三层混淆(三层混淆的典型例子是把”ACL_CONNECTED 广播到达=profile 已连接”)。该 bug 的修复方案为 onCancel 加 bonded→removeBond 降级(commit 7239803be 在 dev_a17_0610 为悬空提交、当前未合入,bug 仍在),详见 11-配对与SSP

7. 一页速查卡

概念关键广播/API本仓锚点
Inquiry/Scan发现startDiscovery() / ACTION_FOUNDMiBluetoothUtils.kt:234-246
CoD 过滤发现isDeviceClassTypeSupportMiBluetoothUtils.kt:141-170
SDP/UUID服务发现ACTION_UUID(SDP 完成后)2956: 44.862 wait for service discovery
Bond配对BOND_BONDED / removeBondBOND_NONE=10/BONDING=11/BONDED=12
ACL链路ACTION_ACL_CONNECTED2956: 41.737 ACL created
Profile业务HFP/PBAP 各自 connect2956: 46.583 HFP 连接
Address/Name/Alias标识getAddress/getName/setAlias列表按 MAC 去重

记住三件事:① 车机连手机=BR/EDR;② 扫描列表只显手机=CoD 过滤有意为之;③ bond/ACL/profile 三层独立,取消已配对设备必须 removeBond