Skip to content

Releases: jjuugk/CarPToCarL

CarPToCarL 1.2.10-switchpool — 奥迪 MIB3 3801 专版连接 + 分支改名

Choose a tag to compare

@jjuugk jjuugk released this 01 Nov 13:03

先说一件事:这个分支改名了

bieke → switch-pool(切换连接池)。

它早就不只服务别克了——大众朗逸 Pro、宝马 iD9、别克大连屏、奥迪 MIB3 走的都是同一套
建链规则,再叫「别克分支」纯属误导。包名后缀 -bieke 同步改为 -switchpool,
旧分支与旧 Release 已按约定清理/保留,见文末。

这版改了什么

连奥迪时走一套为 MIB3 3801 固件校准过的参数;连别克、大众、宝马、测试台时行为完全不变。

改动 之前 现在 为什么
车机专版识别 无 按 0x1D01 身份判 AUDI_MIB3 只有奥迪走激进参数,别的车机不能跟着变
MFi 挑战长度 32B(3s) → 20B(6s) → 16B(3s) 20B(1.5s) → 16B(3s) 3801 收到 32B 会「ACK 挑战帧后整段静默」——不拒绝、不拆链,只把会话挂成僵尸
0x5703 交接 单次 1 秒窗口后并行发 0x4300 首窗 3 秒,未到则每 3 秒补发 0x4E0E + 0x5702,共 5 轮 / 15 秒 对齐真 iPhone sysdiagnose 的 9.87 / 12.96 / 15.97 / 18.99 / 21.99 秒节奏

判据刻意不写死车型号(3260/3874)和固件版本(3801):那个固件串长这样
CL37_MIB3H_AU_AS_G4x_2302700PROD,里面读不出这两个数字,按它分流等于靠猜。
只看「含 audi」或「含 delphi 且含 mib」——前者是车厂名,后者是 MIB 平台,
同代车机时序一致。

老车机不会被误伤

专版把 32B 从基线里拿掉了,但只认 32B 的老 MMI 0000 不受影响:本项目按车机记忆
MFi 长度,该车机成功过的档位会被强制排第一,哪怕基线里没有它。升级后仍先试 32B,
只有冷启动第一次多等 4.5 秒。

装哪个包

包 版本
本分支 CarPToCarL-1.2.10-switchpool.apk 1.2.10-switchpool / 10210
主线 CarPToCarL-1.2.9.apk 1.2.9 / 10209

1.2.10 起本分支领先主线,两边的包不再保证互相覆盖安装。把主线的 1.2.9 装到已装
1.2.10 的机器上会被系统判为降级拒绝,需要 adb install -r -d 或先卸载。

adb install -r CarPToCarL-1.2.10-switchpool.apk

上车先看这四行日志

iap2 车机专版:奥迪 MIB3 专版(MFi 20B 单档 + 0x5703 五轮交接重试)
iap2 tx=0xaa02 challenge=20B(第 1 次,窗口 1500ms)
iap2 0x5703 第 N 轮未到——补发 0x4e0e + 0x5702
IAPLINK: 隧道 MFi 基线 奥迪专版 20/16

如果实际是奥迪、但打的是「通用参数」,括号里会带身份原文——大概率是那个字段里没有
audi / delphi 字样,把它补进判据即可(ReceiverProfile.variantOf)。

边界

  • 未实车验证 3801:参数来自同源分叉项目 xi_zhu/AuDiCar 的 3801 实车结论与本项目
    旧日志,本机尚未跑过。取舍与逐条回退办法见
    docs/audi-mib3-3801.md。
  • 回退一行的事:variantOf 直接返回 GENERIC,全部退回原行为。
  • 只动连接建立,不碰画质、通话、封面、编码器。

仓库变更

  • 分支 bieke 已改名为 switch-pool,远端旧分支已删除(历史提交完整保留在新分支的历史里)。
  • v1.2.9-bieke 及更早的 Release 与 tag 原样保留,老包下载链接继续可用。
  • 本版对应的 tag 是 v1.2.10-switchpool。

单测 351 例全绿。

CarPToCarL 1.2.9-bieke — 别克分支(配对错误码透传 + 失败定性)

Choose a tag to compare

@jjuugk jjuugk released this 01 Nov 03:17

CarPToCarL 1.2.9-bieke

别克(bieke)分支 · 版本号与主线相同(1.2.9 / versionCode 10209),只是 versionName 多了 -bieke 标记——
两个分支的包可以互相覆盖安装,不会被系统以「版本号更小」拒装。

本版与主线内容一致:配对错误码透传 + 失败定性 + 阶段预算 + 多屏选择。
别克分支自带的「大连屏(车机发起)」档位与车机帧率跟随策略保持原样,不受本次改动影响。

车机拒绝配对时,我们一直在把「拒绝」读成「成功」

2026-10-30 的日志 221747 里有一台车:画面出来了,声音一直没有,过一会断链。
日志里给过两条原因——「加密 SETUP 被拒(403)」、「弹窗里没勾声音」——两条都是假线索。

真因在配对应答里,而我们把那个字段丢了:

车机 pair-verify M4 回:{ Error: 2, State: 4 }
                                    ↑       ↑
                              真实原因   我们只看了这个

State: 4 是「正常完成」标记,Error: 2 是「不认这台手机的配对身份」。从 1.0 起,
代码只判 State == 4 就认为配对成功,于是双方派生出两把对不上的密钥,
加密 SETUP 被车机静默丢弃 10 秒,随后降级成明文会话——明文没有共享密钥,音频与触摸整条链路直接被跳过。
「画面有、声音没」就是这么来的,跟弹窗勾没勾、跟 403 都没有关系。

① 配对错误码透传 + 失败定性(本版主项)

pair-setup / pair-verify 的每个应答现在先看 Error,再看 State。PairingPolicy 把「阶段 + 错误码」翻成处置:

错误码 含义 处置
0x02 AUTHENTICATION 不认本机配对身份 确定性失败;pair-setup 段改走已配对快路径(与 403 同义)
0x04 MAX_PEERS 车机保存的设备已满 确定性失败,立刻停
0x05 MAX_TRIES 尝试次数用尽 确定性失败,立刻停
0x03 BACKOFF / 0x06 UNAVAILABLE / 0x07 BUSY 暂时状态 可重试(BACKOFF 停 5s,其余 1s)
私有码 / 无码 未知 可重试——宁可多试一次,也不把能连的车机判死

判定为确定性拒绝时,不再触发「断 BT 二段 hack」:那个补救只会用同一份身份再被拒一次,
真车上还会把蓝牙控制通道拆掉(别克 B233 上尤其明显)。明文会话继续兜底画面,但会当场写清后果:

AIRPLAY: ⚠ 本会话无共享密钥(车机拒绝配对):画面正常,音频与触摸不可用。
         要恢复声音需在车机侧删掉本机的配对记录后重连

看到这一行就不用再翻别的日志:没有声音是必然的,不是没配好。

② 短期记忆:省掉 120 秒白等

确定性拒绝之后,旧行为仍会在 120 秒配对窗口里每 2 秒重试一次,每次都拿到同一个错误码。
PairingMemory 按 pk → deviceid → SSID 三级降级记一条,15 分钟内直接跳过 pair-setup。
三条自我纠正保证不误伤:15 分钟 TTL;只跳过 pair-setup、不跳过明文 SETUP;任何一次配对成功立刻清除记忆。
记忆键不用 IP——车机热点每轮可能换地址。

③ 候选地址只是候选,身份只看 pk

现场事实:mDNS 通告地址与 0x4301 自报地址可能既不同族也不同值(一台 DiPlay 的 0x4301 报 IPv6 链路本地,mDNS 通告 IPv4 192.168.43.1)。
代码结构本来就是这个原则,本版补的是「出了事看不出来」的两件事:配对开始打完整候选(含地址族);
pk 签名不匹配时显式告警并指出风险,但仍不中断——部分自制接收端 pk 本身是空壳,硬失败会把能用的车机挡在门外。

AIRPLAY: 连接候选 192.168.43.1(IPv4)、fe80::1%wlan0(链路本地 IPv6)(身份判定只看 pk,与地址无关)

④ 阶段预算:这次连接到底花了多久

util/ConnectBudget 逐阶段计时,结束打一行汇总。预算全部沿用已有常量,不引入第二套超时:

STAGE: 本轮开始(0x4301)——预算:等端点 5min / 热点 60s / 配对 120s / SETUP 30s / 首帧 20s
STAGE: 会话建立(SETUP)开始(上段 AirPlay 配对 用时 120.4s,**超预算**,本轮累计 131.7s)
STAGE: 本轮小结(0x4301)共 23.5s:等车机连接信息 8.2s → 加入车机热点 3.1s → AirPlay 配对 12.2s → 会话建立 0.0s → 等首帧 0.0s

首帧门:SETUP 成功后 20 秒仍无帧会写明原因。只报告不拆链——拆链归视频看门狗与冻结恢复,
再叠一套超时就是两套语义互相打架(别克分支上这一条尤其不能加)。

⑤ 多屏选择

车机没声明 type → 取第一块有效屏(与旧行为完全一致),别克「大连屏」档位走的就是这条;
声明了 type 时按 type == 0 → 面积 → 帧率 → 声明序选主屏。displayUuid 与 maxFps 改取选中那块屏的值。

AIRPLAY: /info 共 2 块屏候选:displays[0] 800×480 type=1;displays[1] 1920×720 type=0 maxFPS=60 → 选中 displays[1]

⚠ 排序是假设,不是实车结论——它只在车机自己声明 type 时生效,且一定把候选与选择结果写进日志,实车一跑就能验证。

排查时搜这几行

想知道 搜
这次连接一共多久、慢在哪 STAGE: 本轮小结
车机是否拒绝配对、为什么 Error= / 确定性拒绝配对
本会话有没有声音 本会话无共享密钥(有这行就必然没有音频/触摸)
按什么顺序试了哪些地址 AIRPLAY: 连接候选
选了哪块屏 块屏候选 / 选中 displays[

完整判据见 docs/connect-hardening.md。


装包说明:CarPToCarL-1.2.9-bieke.apk 为别克分支正式包(versionCode 10209)。
与 main 的 1.2.9 同 versionCode,可互相覆盖安装;升级不会丢配置与已配对车机信息。

CarPToCarL 1.2.9 — 车机拒绝配对时,别再把拒绝读成成功

Choose a tag to compare

@jjuugk jjuugk released this 01 Nov 03:17

车机拒绝配对时,我们一直在把「拒绝」读成「成功」

2026-10-30 的日志 221747 里有一台车:画面出来了,声音一直没有,过一会断链。
日志里给过两条原因——「加密 SETUP 被拒(403)」、「弹窗里没勾声音」——两条都是假线索。

真因在配对应答里,而我们把那个字段丢了:

车机 pair-verify M4 回:{ Error: 2, State: 4 }
                                    ↑       ↑
                              真实原因   我们只看了这个

State: 4 是「正常完成」标记,Error: 2 是「不认这台手机的配对身份」。从 1.0 起,
代码只判 State == 4 就认为配对成功,于是双方派生出两把对不上的密钥,
加密 SETUP 被车机静默丢弃 10 秒,随后降级成明文会话——明文没有共享密钥,音频与触摸整条链路直接被跳过。
「画面有、声音没」就是这么来的,跟弹窗勾没勾、跟 403 都没有关系。

这一版做的事情,都是围绕**「别再把失败读成成功」和「失败之后要能看懂」**展开的。

① 配对错误码透传 + 失败定性(本版主项)

pair-setup / pair-verify 的每个应答现在先看 Error,再看 State,抛出的异常带着阶段与原始错误码。
PairingPolicy 把「阶段 + 错误码」翻成处置:

错误码 含义 处置
0x02 AUTHENTICATION 不认本机配对身份 确定性失败;pair-setup 段改走已配对快路径(与 403 同义)
0x04 MAX_PEERS 车机保存的设备已满 确定性失败,立刻停
0x05 MAX_TRIES 尝试次数用尽 确定性失败,立刻停
0x03 BACKOFF / 0x06 UNAVAILABLE / 0x07 BUSY 暂时状态 可重试(BACKOFF 停 5s,其余 1s)
私有码 / 无码 未知 可重试——宁可多试一次,也不把能连的车机判死

判定为确定性拒绝时,不再触发「断 BT 二段 hack」:那个补救只会用同一份身份再被拒一次,真车上还会把蓝牙控制通道拆掉。
明文会话继续兜底画面,但会当场写清后果:

AIRPLAY: ⚠ 本会话无共享密钥(车机拒绝配对):画面正常,音频与触摸不可用。
         要恢复声音需在车机侧删掉本机的配对记录后重连

看到这一行就不用再翻别的日志了:没有声音是必然的,不是没配好。

② 短期记忆:省掉 120 秒白等

确定性拒绝之后,旧行为仍会在 120 秒配对窗口里每 2 秒重试一次,每次都拿到同一个错误码。
PairingMemory 按接收端记一条(pk → deviceid → SSID 三级降级),15 分钟内直接跳过 pair-setup。

三条自我纠正保证它不误伤本来能连的车机:15 分钟 TTL 自动失效;只跳过 pair-setup、不跳过明文 SETUP
(明文 SETUP 若被拒,retryWithPairing 仍会补做一次配对);任何一次配对成功立刻清除记忆。

记忆键不用 IP——车机热点每轮可能换地址,用地址当键要么永远命中不了,要么命中到隔壁设备。

③ 候选地址只是候选,身份只看 pk

现场事实:mDNS 通告的地址与 0x4301 自报的地址可能既不同族也不同值——一台 DiPlay 的 0x4301 报 IPv6 链路本地,mDNS 通告 IPv4 192.168.43.1。

代码结构本来就是这个原则(按 IPv4 → 全局 IPv6 → 链路本地 IPv6 逐个试),本版补的是两件「出了事看不出来」的事:

AIRPLAY: 连接候选 192.168.43.1(IPv4)、fe80::1%wlan0(链路本地 IPv6)(身份判定只看 pk,与地址无关)

以及 pk 签名不匹配时不再只是一句 =false,而是显式告警并指出风险(TXT 里的 pk 过期,或连到了同网段的另一台 AirPlay 设备)。
仍然不中断——部分自制接收端的 pk 本身就是空壳,硬失败会把能用的车机挡在门外。

④ 阶段预算:这次连接到底花了多久

七个阶段的耗时原来散落在各自日志里,每次都要人工掐表。util/ConnectBudget 现在逐阶段计时,结束打一行汇总:

STAGE: 本轮开始(0x4301)——预算:等端点 5min / 热点 60s / 配对 120s / SETUP 30s / 首帧 20s
STAGE: 会话建立(SETUP)开始(上段 AirPlay 配对 用时 120.4s,**超预算**,本轮累计 131.7s)
STAGE: 本轮小结(0x4301)共 23.5s:等车机连接信息 8.2s → 加入车机热点 3.1s → AirPlay 配对 12.2s → 会话建立 0.0s → 等首帧 0.0s

预算值全部沿用已有常量,不引入第二套超时。

首帧门:SETUP 成功后 20 秒仍无帧会写明原因(会话已建立,20 秒出不来就不是「慢」而是「没有」)。
故意只报告不拆链——拆链由视频看门狗与冻结恢复负责,再叠一套超时就是两套语义互相打架,这个坑踩过好几次。

⑤ 多屏选择

旧实现固定取第一块宽高可解析的屏。多屏车机(大连屏 / 仪表)若先声明副屏,画面就推到那块屏上,比例与触摸坐标全错。

现在的规则:车机没声明 type → 取第一块有效屏(与旧行为完全一致);声明了 type 时按
type == 0 → 面积 → 帧率 → 声明序选主屏。displayUuid 与 maxFps 也改为取选中那块屏的值。

AIRPLAY: /info 共 2 块屏候选:displays[0] 800×480 type=1;displays[1] 1920×720 type=0 maxFPS=60 → 选中 displays[1]

⚠ 这条排序是假设,不是实车结论——目前还没有多屏车机接入过,它只在车机自己声明 type 时生效,且一定会把候选与选择结果写进日志。实车一跑就能验证。

排查时搜这几行

想知道 搜
这次连接一共多久、慢在哪 STAGE: 本轮小结
哪个阶段超预算 **超预算**
车机是否拒绝配对、为什么 Error= / 确定性拒绝配对
本会话有没有声音 本会话无共享密钥(有这行就必然没有音频/触摸)
按什么顺序试了哪些地址 AIRPLAY: 连接候选
选了哪块屏 块屏候选 / 选中 displays[
画面为什么没出来 STAGE: ⚠ 等首帧超时

完整判据与「为什么不做某些事」见 docs/connect-hardening.md。


装包说明:CarPToCarL-1.2.9.apk 为主线正式包(versionCode 10209)。
与别克分支的 1.2.9-bieke 同 versionCode,可互相覆盖安装;升级不会丢配置与已配对车机信息。

CarPToCarL 1.2.8-bieke — 别克分支(音频通道看门狗)

Choose a tag to compare

@jjuugk jjuugk released this 01 Nov 01:44

CarPToCarL 1.2.8-bieke

别克(bieke)分支 · 版本号与主线相同(1.2.8 / versionCode 10208),只是 versionName 多了 -bieke 标记——
两个分支的包可以互相覆盖安装,不会被系统以「版本号更小」拒装。

本版只有一件事:给音频通道补上看门狗,修「画面早就有了、声音要等 88 秒」。

有画面、没声音要等 88 秒

一份实车日志(2026-11-01 09:01)把时间线摊开:

08:57:38  CARLIFE: 媒体 音频通道断开 - Connection reset     ← 9240 死了
08:58:00  CARLIFE: TTS 音频通道断开 - Connection reset      ← 9241 也死了
08:58:0x  (视频看门狗重连 8240 → 车机上画面恢复)
09:00:22  CARLIFE: 媒体 音频格式宣告 44100Hz 2ch 16bit       ← 88 秒后才出声

WiFi 入网把荣耀 CarLife 引擎重启,视频 / 媒体 / TTS 三条数据 socket 同时被 reset。
视频活下来了,因为它本来就有一条看门狗;音频没有——audioReader() 读到异常只打一行日志就结束,
sockets[9240] 还留在表里,没有任何代码会去重建它。声音只能等引擎下一次自己复位推流。

路径 A:socket 死了,立刻重连

媒体(9240)与 TTS(9241)各自挂一个通道状态,看门狗每秒检查一次;读线程退出即判死,随后重连该端口、重挂读线程。
断链重连不设次数上限(引擎冷启动期可能连续 reset),超过 3 次后每次多停 2 秒——和视频看门狗同一套节奏。

坑:重连要先把旧 socket close(),旧读线程随即走 finally;若无条件置死就会把刚接上的新通道秒判死,
形成「重连 → 判死 → 重连」的死循环。现在每次挂读线程递增一个「代次」,读线程只在自己这一代仍是最新时才允许置死。

路径 B:socket 活着但不出声,重连一次(带三重设限)

必须全部满足才动手:

连续无 PCM ≥ 20 秒            ← 音乐的自然停顿不会这么久
距通道建立 ≥ 20 秒            ← 刚接上要给引擎推流时间
视频 3 秒内有帧               ← 引擎活着,问题只在这条流上
引擎发过 0x30002 且未发 0x30003,或音乐会话正在播放
最多 3 次                     ← 退避 20 / 40 / 60 秒,收到 PCM 即清零

设限不是保守:屏幕上没在放声音是常态,只按「静默」判会在每次停车等待时反复重连,
自己把引擎的音频管道打断——比不修更糟。TTS 只做路径 A:没开导航时 9241 本就该长时间静默。

另一个必须保留的细节:格式宣告状态要跨重连保留。它原来是读函数里的局部变量,重连后新线程会以为
「引擎没宣告过格式」,给上层报 48kHz 立体声兜底值,把真实的 44.1kHz 覆盖掉——声音会变调。

重连只换 socket 与读线程,上层 sink 与发送端不动,车机侧 RTP 流不中断,听感是「卡一下」而不是「断一次」。

日志里怎么确认

CARLIFE: 媒体 音频通道断开 - Connection reset(看门狗会自动重连)
CARLIFE: 媒体 音频通道已重连(第 1 次断链重连)
CARLIFE: 媒体 音频通道 25 秒零 PCM(画面正在正常出帧)——重连 9240(第 1/3 次,20 秒后才会再试)
CARLIFE: 媒体 音频通道恢复(断流 25 秒,重连 1 次后恢复)

看到这条说明引擎侧已不推流,该做的是重开 CarLife+ 的「无线连接」,看门狗会停在这里:

CARLIFE: 媒体 音频通道连续 3 次重连仍无 PCM——停止重试

完整梳理见 docs/audio-channel-watchdog.md。


装包说明:CarPToCarL-1.2.8-bieke.apk 为别克分支正式包(versionCode 10208)。
与 main 的 1.2.8 同 versionCode,可互相覆盖安装;升级不会丢配置与已配对车机信息。

CarPToCarL 1.2.8 — 音频通道看门狗:有画面没声音不再干等 88 秒

Choose a tag to compare

@jjuugk jjuugk released this 01 Nov 01:41

有画面、没声音要等 88 秒——音频链路终于也配了看门狗

「等一会才连上车机,播放音乐也要等一段时间才出音频」——这句话背后其实是两件事。
连接慢那一半是引擎与 WiFi 的老账(见 1.2.6 的说明),声音迟到这一半是明确的漏配:音频通道没有看门狗。

一份实车日志(2026-11-01 09:01)把时间线摊开:

08:57:38  CARLIFE: 媒体 音频通道断开 - Connection reset     ← 9240 死了
08:58:00  CARLIFE: TTS 音频通道断开 - Connection reset      ← 9241 也死了
08:58:0x  (视频看门狗重连 8240 → 车机上画面恢复)
09:00:22  CARLIFE: 媒体 音频格式宣告 44100Hz 2ch 16bit       ← 88 秒后才出声

WiFi 入网把荣耀 CarLife 引擎重启,视频 / 媒体 / TTS 三条数据 socket 同时被 reset。
视频活下来了,因为它本来就有一条看门狗:每秒检查 socket 是否还在,死了就重连 8240 并重发编码初始化。
音频没有。audioReader() 读到异常只打一行日志就结束,sockets[9240] 还留在表里,没有任何代码会去重建它——
声音只能等引擎下一次自己复位推流。所以它「自己好了」,代价是 88 秒。

路径 A:socket 死了,立刻重连(本版主项)

媒体(9240)与 TTS(9241)各自挂一个通道状态,看门狗每秒检查一次;读线程退出即判死,随后重连该端口、重挂读线程。
断链重连不设次数上限(引擎冷启动期可能连续 reset),超过 3 次后每次多停 2 秒,避免把引擎打满——和视频看门狗同一套节奏。

一个容易踩的坑:重连要先把旧 socket close(),旧读线程随即走 finally;若无条件置死,就会把刚接上的新通道秒判死,
形成「重连 → 判死 → 重连」的死循环。现在每次挂读线程都递增一个「代次」,读线程只在自己这一代仍是最新时才允许置死。

路径 B:socket 活着但不出声,重连一次(带三重设限)

画面在动、音乐在播、9240 却连续零 PCM——这是「通道活着但流断了」,重连一次通常就能把引擎拉回来。
但这条判据必须设限,否则会比不修更糟:屏幕上没在放声音是常态,只按「静默」判会在每次停车等待时反复重连,自己把引擎的音频管道打断。

所以全部满足才动手:

连续无 PCM ≥ 20 秒            ← 音乐的自然停顿不会这么久
距通道建立 ≥ 20 秒            ← 刚接上要给引擎推流时间
视频 3 秒内有帧               ← 引擎活着,问题只在这条流上;视频也停了是会话层的事,别跟着乱动
引擎发过 0x30002 且未发 0x30003,或音乐会话正在播放
最多 3 次                     ← 退避 20 / 40 / 60 秒,收到 PCM 即清零

TTS 只做路径 A:没开导航时 9241 本来就该长时间静默,按沉默判会天天误重连;但它的 socket 一样会随引擎重启而死。

还有一个必须保留的细节:格式宣告状态要跨重连保留。它原来是读函数里的局部变量,重连后新线程会以为「引擎没宣告过格式」,
给上层报一个 48kHz 立体声兜底值,把真实的 44.1kHz 覆盖掉——声音会变调。现在挂在通道状态上,重连不动它。

重连只换 socket 与读线程,上层的音频 sink 与发送端保持不动,所以车机侧的 RTP 流不会中断,听感是「卡一下」而不是「断一次」。

日志里怎么确认

CARLIFE: 媒体 音频通道断开 - Connection reset(看门狗会自动重连)
CARLIFE: 媒体 音频通道已重连(第 1 次断链重连)
CARLIFE: 媒体 音频通道 25 秒零 PCM(画面正在正常出帧)——重连 9240(第 1/3 次,20 秒后才会再试)
CARLIFE: 媒体 音频通道恢复(断流 25 秒,重连 1 次后恢复)

如果看到这条,说明引擎侧已经不推流了,此时该做的是重开 CarLife+ 的「无线连接」,而不是继续重连——
看门狗停在这里,把判断权交回给人:

CARLIFE: 媒体 音频通道连续 3 次重连仍无 PCM——停止重试

排查思路的完整梳理见 docs/audio-channel-watchdog.md。


装包说明:CarPToCarL-1.2.8.apk 为正式包(versionCode 10208)。可以直接覆盖安装,配置与已配对车机信息都会保留。

CarPToCarL 1.2.7-bieke — 别克分支(冻结自愈 + 大连屏档位)

Choose a tag to compare

@jjuugk jjuugk released this 31 Oct 14:36

CarPToCarL 1.2.7-bieke

别克(bieke)分支 · 版本号与主线相同(1.2.7 / versionCode 10207),只是 versionName 多了 -bieke 标记——
两个分支的包可以互相覆盖安装,不会被系统以「版本号更小」拒装。

本版两件事:冻结不再靠猜、大屏多了个档位。

一、进程被冻结:当场定性,解冻后自己爬起来

ROM 把进程冻住时线程全部挂起、不报错、不回调,日志整段消失。以前只能靠三个旁证去猜
(日志空白、解冻瞬间帧率飙到 397fps、积压日志在同一毫秒炸开)。现在保活服务每秒一次心跳,
比对 elapsedRealtime() 的跳变,命中就落一行:

PROC: 进程被系统冻结 133 秒(心跳中断 134s)——冻结期间不发帧、不回 keepAlive,车机多半已按超时撤掉会话

冻结 <10 秒只记录不重建(多半只是主线程被占,会话没受伤,重建反而多拆一次链)。≥10 秒才进入恢复:

PROC: 解冻 133s 自检——CarLife:7240=在监听,车机 AirPlay=不可达,推流=已断
PROC: 重建会话 第 1/4 次(退避 3000ms · 端点 GeXu-Audi_5GHz)
PROC: ✓ 会话已自动恢复(第 1 次尝试)

先等 1.5 秒让积压的帧和回调喘完 → 自检 CarLife 7240 与车机 AirPlay 端口 → 按 3 / 6 / 12 / 24 秒
退避重建,每轮 8 秒观察窗,最多 4 轮。重建靠最后一份端点快照——车机不会因为「手机刚解冻」再补发一遍
0x4301。

二、新增「别克 大连屏(车机发起)」档位

面向另一台别克(30 吋级一体式宽屏,GL8 / 世纪 / 君越那类)。

别克 E5(车机发起) 别克 大连屏(车机发起)
建链 B233(75B 双 group 0x4300、32B MFi、不发 0x5702、不拆 HFP/A2DP) 同左,一字不差
方向 等车机接入 同左(不主动连就改选「手机拨号」)
命中条件 蓝牙名像别克、或有历史记录 选了就生效,不要求名字像别克
车机没报 maxFPS 时 60fps 30fps(大屏解码器只吃 30)

「选了就生效」是特意加的:大屏车机的蓝牙名几乎肯定不叫 BUICK_xxxx,照搬 E5 那条「名字要对得上」的
规矩,用户选了档位等于白选。一旦在「蓝牙配对」里指定了目标车机,仍然只认那一个地址,不会连到旁边的耳机。

三、帧率:车机报了就跟随(对所有档位生效)

targetFps = min(本地偏好 60, 车机 /info 报的 maxFPS)
  • 车机报得比 60 高 → 不抬高(上限由本地偏好决定)
  • 车机报荒谬值(如 10)→ 按 24 兜底,不至于卡成 PPT
  • 车机没报 → 只有大屏档退回 30,其余档位保持 60,行为与 1.2.6 完全一致

合成器的出帧节奏也接上了:attach 时读的是 60,车机上限要到 SETUP 之后才知道,不同步的话多出来的帧
白烧编码。

选大屏档时只提示缩放建议 90%(不自动改用户设置——显示参数是用户自己调过的,悄悄覆盖会让人以为设置丢了)。

四、日志降噪

日志 之前 现在
媒体进度 xxxms 每秒一条,解冻时 0.5 秒刷 100+ 条 10 秒一条
媒体信息(xxB)但解析为空 每次媒体变化一条 5 秒一条
通知监听器未被系统绑定——已请求重绑 每 10 秒一条(一台机器刷过 90+ 条) 动作仍每 10 秒做,话 5 分钟说一次

适用车型

车型 怎么选档位
别克 E5(B233 / E2LB) 默认「自动识别」即可命中
大众朗逸 Pro 「连接配置」→ 手动选 「别克 E5(车机发起)」
宝马 iD9 同上:「别克 E5(车机发起)」
别克 大连屏(GL8 / 世纪 / 君越这类宽屏,尚未实车验证) 手动选 「别克 大连屏(车机发起)」;依据与验证清单见 docs/buick-bigscreen-profile.md

反向不成立:奥迪、凯迪拉克等本来在「自动识别」下就正常的车机不要手动切别克档。两条线的规则相反
(别克要求保留 HFP/A2DP、不发 0x5702),强行套用会连不上。

安装

  • APK:CarPToCarL-1.2.7-bieke.apk(versionName=1.2.7-bieke,versionCode=10207,release 签名)
  • 覆盖安装:adb install -r -d CarPToCarL-1.2.7-bieke.apk
  • 与主线 1.2.7 的 versionCode 相同,两个分支的包可以来回换装

已知限制

  • 大连屏档还没在真车上跑过:参数按最可能的同栈(SAIC-GM 8155 / Cinemo)取,实车跑通后会把
    0x1D01 身份标记补进「自动识别」,届时不必再手选。
  • 后台被系统冻结仍会掉线:本版能自动重建,但第一次冻结造成的画面中断已经发生。彻底避免仍要把
    本应用设为「省电策略:无限制」并允许自启动。
  • 无线 CarPlay 的画面来自劫持本机 CarLife+ 引擎,CarLife+ 被系统清掉后需手动重新打开并点「无线连接」。

CarPToCarL 1.2.7 — 冻结自愈 + 帧率跟随车机

Choose a tag to compare

@jjuugk jjuugk released this 31 Oct 14:52

目标帧率改为跟随车机(同版本补充,包已重打)

车机在 /info displays[0].maxFPS 里本来就声明了自己的解码上限,以前一律按 60 发。
大屏车机常见 30,发 60 的结果是车机侧丢帧、解码队列堆积,严重时按超时拆链。车机既然报了,就没有理由跟它对着干。

carMaxFps > 0  → 按车机值出帧(夹在 24…120)
carMaxFps <= 0 → 按 60(车机没声明,沿用通用线一直以来的值)

两个保护:下限 24 防车机报 10、15 这类荒谬值(多半是字段解析错位);上限 120 防编码器与合成器跟着疯。

改动不只换了编码器参数——合成器的出帧节奏也跟着走。建屏发生在 AirPlay SETUP 之前,那时还不知道车机上限,所以先按 60 起,拿到 /info 后由会话层改。不同步的话,多出来的帧会白白送去编码再丢掉。

日志里能直接对账,三种来源分得清:

DISPLAY: 车机 1920×720(/info 通告)→ 流 1920×720(100%)@30fps(跟随车机 maxFPS=30)
DISPLAY: 车机 …@60fps(车机未声明 maxFPS,按 60 声明)
DISPLAY: 车机 …@24fps(车机报 10fps(低于下限),按 24 声明)

顺带修掉一个把来源搞混的旧兜底:/info 里没有 maxFPS 时,旧代码直接给 30,于是「车机要 30」和「车机什么都没说」在数据上再也分不开,上面这条规则会永远退化成按 30 发。现在未声明就是 0,SETUP 完成 那行也会照实写「车机未声明fps」。

没有设置项,也不需要用户配置:车机报 60 就是 60,报 30 就是 30。


进程被冻结不再靠猜:一秒定性,解冻后自己爬起来

「画面突然定住,一两分钟后车机把会话撤了」——这个现象在日志上几乎是灵异事件:整段空白、没有任何异常、没有崩溃。上一版透过一份实车日志(Xiaomi 13 Ultra,奥迪 MIB3)才确认:进程被系统冻结了 133 秒,冻结期间不发帧、不回 keepAlive,车机熬到超时就把 AirPlay 会话撤了。

以前只能靠三个旁证去推:日志整段消失、解冻瞬间帧率飙到 397fps、积压日志在同一毫秒涌入。本版把它变成一行确定的话,并让 App 在解冻后自己重建会话。

一、冻结监视(PROC 日志)

保活服务里每秒一次心跳,比对 elapsedRealtime()——这是单调时钟,进程被冻结时它照常走。所以解冻后第一次心跳就会看到「本该 1 秒前跑的那次现在才到」:

PROC: 进程被系统冻结 133 秒(心跳中断 134s)——冻结期间不发帧、不回 keepAlive,车机多半已按超时撤掉会话

阈值取 4 秒(3 秒容差),避开主线程跑重活造成的正常迟到。3~10 秒只记录不动作——抖动多半只是主线程被占,会话没受伤,重建反而多拆一次链。

二、解冻后自动恢复

冻结超过 10 秒才动手,流程:

PROC: 解冻 133s 自检——CarLife:7240=在监听,车机 AirPlay=不可达(多半已被车机撤掉会话),推流=已断
PROC: 重建会话 第 1/4 次(退避 3000ms · 端点 GeXu-Audi_5GHz)
PROC: ✓ 会话已自动恢复(第 1 次尝试)
  • 先等 1.5 秒,让积压的帧与回调喘完,再判断现状
  • 复核「是否真在推流」;还在推就只记一行收工,不做多余动作
  • 自检 CarLife 7240 与车机 AirPlay 两个源,按 3 / 6 / 12 / 24 秒退避重建,每轮 8 秒观察窗,最多 4 轮
  • 重建依赖最后一份可用端点快照——车机不会因为「手机刚解冻」再补发一遍交接报文
  • 恢复线程互斥:冻结常常是「抖几下」,不互斥会叠出多轮并行重建互相拆台

判定用的是「用户是否想要会话」(点「启动连接」置真、「断开」置假),不是「连接是否被取消」——链路一断后者就会置真,而「链断了」正是冻结后的常态,用它等于永不恢复。

三、日志降噪

解冻瞬间积压的日志会把真正有用的信息冲走,本版限流三类高频行:

日志 之前 现在
媒体进度 xxxms 每秒一条,解冻时 0.5 秒刷 100+ 条 10 秒一条
媒体信息(xxB)但解析为空 每次媒体变化一条 5 秒一条
通知监听器未被系统绑定——已请求重绑 每 10 秒一条(单机刷过 90+ 条) 动作仍每 10 秒做,话 5 分钟说一次

已知局限

  • 只覆盖「进程还活着」的冻结。被系统直接杀进程走的是「靠近车机自动拉起」路径(1.2.6 已有),不是这条。
  • 冻结期间车机侧已经判死,恢复必然是一次完整重连(3~10 秒黑屏),做不到无缝。
  • 若冻结时用户已经拔掉车机或走远,恢复会白跑四轮(约 45 秒 + 观察窗),最后落一行「自动恢复未成功」。
  • 用户先按了「断开」再解冻,会明确跳过恢复并记一行 PROC: 解冻后用户已断开——不自动恢复。

安装

下载 CarPToCarL-1.2.7.apk 覆盖安装即可(versionCode 10207,可直接覆盖 1.2.6)。CarPToCarL-latest.apk 与它内容一致,保留这个固定名字方便直接取最新版。

本次是同版本重发:1.2.7 / 10207 没变,只多了「帧率跟随车机」那一节。装过旧 1.2.7 的可以直接覆盖,
系统不会因为「版本号没涨」拒装。

三项权限仍建议保持开启,否则后台链路会被系统拦掉:

  1. 忽略电池优化——冻结的主要成因,不给的话进程随时可能被挂起
  2. 显示在其他应用上层——不给的话,靠近车机时「后台打开界面」会被静默拦掉
  3. 自启动 / 关联启动(部分 ROM)——关掉的话靠近广播根本投不到本应用

测试

单元测试 270 例通过(本版新增 FreezeWatchTest 6 例、LogThrottleTest 4 例)。

文档

docs/process-freeze-recovery.md:PROC 日志样例、判定原理、恢复时序、三条边界、排查关键字。

CarPToCarL 1.2.6-bieke — 别克分支(含大众朗逸 Pro / 宝马 iD9)

Choose a tag to compare

@jjuugk jjuugk released this 30 Oct 14:12

CarPToCarL 1.2.6-bieke

别克(bieke)分支 · 版本号与主线相同(1.2.6 / versionCode 10206),只是 versionName 多了 -bieke 标记——
两个分支的包可以互相覆盖安装,不会被系统以「版本号更小」拒装。

这个包是什么

主线 1.2.6 的全部内容(靠近车机自动拉起 / 自动连接、通知栏可见可断开、远离 30 秒收摊、划掉任务收摊、
抑制 10 分钟窗口),再加上别克专用建链:

  • 0x4300 用 75 字节双 group(group0 设备标识 + group1 真实蓝牙 MAC),不是通用线的单 group
  • MFi 挑战只认精确 32 字节,长度不符立即断链、不回落
  • 不拆 HFP/A2DP(拆了车机不开 iAP2 服务窗口),不发 0x5702(能力清单未声明可收)
  • 默认等车机发起 RFCOMM(手机主动拨号是特例,最短间隔 15 秒)
  • 车机零回帧时不催问(催问本身就是 E2LB 拆链元凶)

适用车型

车型 怎么选档位
别克 E5(B233 / E2LB,SAIC-GM 8155 / Cinemo 栈) 默认「自动识别」即可命中(0x1D01 里同时有 buick + saic general motors + 车型代号)
大众朗逸 Pro App 首页「连接配置」→ 手动选 「别克 E5(车机发起)」
宝马 iD9 同上:「别克 E5(车机发起)」

大众与宝马之所以要手动选一次:自动识别只按别克的身份标记判定,这两台车的 0x1D01 里没有这些字样,
会落到通用线(单 group 0x4300)——而它们需要的正是别克这一版报文与节奏。手动选档 = forcesB233,
不看身份文本,只要连上的是你选定的那台车机就按别克规则走。选一次就记住,改动下次连接生效。

反过来不成立:奥迪、凯迪拉克等本来在「自动识别」下就正常的车机不要手动切别克档,
两条线的规则是相反的(别克要求保留 HFP/A2DP、不发 0x5702),强行套用会连不上。

靠近车机,自动拉起并自动连接

环节 行为
触发 只听系统已投递的 ACL_CONNECTED:不扫描、不轮询、无常驻服务、无通知;进程没在跑时由系统重新拉起接收器
命中判定 开关开 + 已选目标车机且地址相等 + 当前无会话 + 不在「刚手动断开」抑制窗口 + 距上次触发 ≥ 90 秒
拉起 只把界面带到前台,连接交给前台走完整流程(后台直连会出现「RFCOMM 能建、握手能过,但车机收不到数据」)
可见 常驻通知每 3 秒跟随阶段刷新:已出画面 / 正在连 / 连上了等车机下发信息 / 空闲;带「断开」与「打开界面」两个动作
收摊 远离 30 秒 / 通知栏「断开」/ 划掉最近任务,三个入口同一条路径:结束会话 → 停服务 → 清通知 → 退进程

退进程不影响静默检测:ACL_* 在系统隐式广播豁免列表里,下次靠近系统会重新拉起接收器。

三个开关(首页「连接准备」)

  • 靠近车机自动启动:默认关。打开后「未指定目标车机」会自动引导去「蓝牙配对」里选。
  • 触发后不弹出界面:默认关。打开后自动触发只留通知栏,界面建好后 1.2 秒退到后台。
    注意不能真的「不建界面」——整条管线(CarLife 劫持、编码器、自检、权限门禁)都挂在 Activity 上。
  • 启动后自动连接:已有开关,与靠近触发配合使用。

首次必给的三项权限

  1. 显示在其他应用上层(后台拉起界面的前提,Android 10+ 静默拒绝、不报错)
  2. 忽略电池优化(首页有引导,7 天节流)
  3. 手机厂商的「后台弹界面 / 自启动」开关(国产 ROM 需要,荣耀、小米、ColorOS 都在应用管理里)

没有第 1 项时,靠近触发会「日志说已拉起、屏幕什么都没有」。1.2.3 起这种情况会发一条通知说明原因,
并给「打开界面」入口。

安装

  • APK:CarPToCarL-1.2.6-bieke.apk(versionName=1.2.6-bieke,versionCode=10206,release 签名)
  • 覆盖安装:adb install -r -d CarPToCarL-1.2.6-bieke.apk
  • 与主线 1.2.6 的 versionCode 相同,两个分支的包可以来回换装

已知限制

  • 后台被系统冻结会掉线:手机把进程冻住十几秒,车机侧 AirPlay 就按超时拆链(实测容忍约 14 秒)。
    日志表现为「整段无输出 + 解冻时几十行日志挤在同一毫秒」。需要把本应用设为「省电策略:无限制」
    并允许自启动。
  • 无线 CarPlay 的画面来自劫持本机 CarLife+ 引擎,CarLife+ 被系统清掉后需要手动重新打开并点「无线连接」。
  • 加密卡在 51% 左右的个别固件(Citroën 一类)见 README 故障排查章节。

CarPToCarL 1.2.6 — 靠近车机自动拉起并自动连接

Choose a tag to compare

@jjuugk jjuugk released this 30 Oct 08:05

靠近车机,自动拉起、自动连接(本版重点)

上车不用再掏手机。车机进入范围后会自己回连手机蓝牙,系统随即发出 ACL_CONNECTED 广播——本版就在这一刻把 App 拉到前台并自动发起连接。

检测是静默的:不扫描、不轮询、无常驻服务。接收器在清单里静态注册,App 没打开、进程没起来也能收到这条广播(系统投递时会拉起进程)。

一条完整闭环

阶段 行为
命中 目标车机 ACL 连上就触发;非目标设备(耳机/手表)静默忽略
拉起 把界面带到前台,带标记让前台重新武装「启动后自动连接」
连接 前台走完整流程:权限门禁 → 蓝牙监听 → 等车机回连 → 会话
可见 常驻通知显示当前阶段(已出画面 / 正在连 / 等待车机下发),带「断开」按钮
收摊 车机断开满 30 秒 / 通知栏断开 / 划掉最近任务 → 结束会话、清通知、退出进程

为什么必须「拉到前台」而不是后台直连

1.2.0 最初在后台直接起连接:RFCOMM 能建、握手能过,但车机端收不到任何数据。整条管线不只是 iAP2 拨号——CarLife 劫持、虚拟屏与编码器、启动自检、权限门禁全挂在 Activity 上,绕开界面就是「握手成功、出流失败」。所以现在接收器只负责把 App 叫到前台,连接交给前台。

三个开关(首页「连接准备」)

  • 靠近车机自动启动(默认开):命中目标车机时自动拉起并连接
  • 触发后不弹出界面(默认关):界面建好后 1.2 秒退到后台,进度与「断开」都在通知栏
  • 启动后自动连接(默认开):冷启动即自动发起连接

未指定「目标车机」时本功能完全不动作,避免连到耳机、手表或别人的手机。

其它改动

  • 通知栏升级:从「运行中」改为跟随会话阶段刷新(每 3 秒),新增「断开」动作;换用新通道,避免旧通道被系统折叠进静默通知
  • 后台启动被拦兜底:Android 10+ 静默拒绝后台启动界面(不报错、界面就是不出现)。拉起后 3.5 秒核对一次,没起来就发通知说清原因并给「打开界面」;首页新增「显示在其他应用上层」授权引导
  • 远离 30 秒自动收摊:结束会话 → 停保活服务 → 清通知 → 退出进程,不再让手机替一辆不在跟前的车保持常亮。判定用「画面是否真在推」而不是「会话是否存在」——后者在等车机回连时长期为 true,会让收摊永不触发
  • 划掉最近任务也收摊:此前只销毁界面,前台服务照跑、通知照挂
  • 「刚断开过」抑制加 10 分钟时限:原先只在蓝牙断开时清除,可人在车上关掉 App 后蓝牙一直连着、永远等不到断开事件,标记永久挂住,人在车里却再也唤不醒。现在窗口外自然失效,主动打开 App 也立刻清除
  • 单元测试:260 例通过(新增触发判定、抑制窗口、收摊判定三组)

安装

下载 CarPToCarL-1.2.6.apk 覆盖安装即可(versionCode 10206,可直接覆盖旧版)。CarPToCarL-latest.apk 与它内容一致,保留这个固定名字方便直接取最新版。

首次使用需要给三个权限

  1. 显示在其他应用上层——不给的话,系统会静默拦掉「后台打开界面」,靠近时什么都不会发生
  2. 忽略电池优化——否则进程可能被提前回收,广播投递不及时
  3. 自启动 / 关联启动(部分 ROM,如荣耀/华为)——关掉的话广播根本投不到本应用

前两项在首页「连接准备」里有引导入口;第三项需到系统设置里手动开。

CarPToCarL 1.2.1 — 靠近车机自动启动 + 通话音频通道

Choose a tag to compare

@jjuugk jjuugk released this 30 Oct 06:10

CarPToCarL 1.2.1 — 靠近车机自动启动 + 通话音频通道

versionCode 10201。本版两个主项:车机靠近就自动拉起并建链、通话音频通路。

覆盖安装即可(签名与 1.0.0 起各版一致),无需卸载。

一、靠近车机自动启动

不用再手动打开 App、手动点「启动连接」。车机进入范围后会自己回连手机蓝牙,系统随即投递 ACL 连接事件,本版据此自动完成「打开 App → 开始连接」。

为什么不用扫描:主动 startDiscovery 会持续占用射频、弹系统提示、还要额外的扫描权限,与「静默」相悖。车机本来就会自己回连,只听系统已经在投递的事件即可——不扫描、不轮询、无常驻服务、无常驻通知。

怎么保证抢在前面:广播到达后按「抢前台 → 抢链路 → 写日志」排序——

  1. 立刻 startActivity(拉界面是最慢的一环,越早排队越好,其余动作不阻塞它);
  2. 紧接着不等界面直接发起连接,只依赖已缓存的蓝牙地址,不做任何磁盘/网络准备;
  3. 最后才写文件日志,磁盘 IO 不拖慢前两步。

防误触发:

  • 未指定「目标车机」时一律不动(不会误连耳机、手表、测试台);
  • 同一段靠近 90 秒内只触发一次;地址比对不区分大小写;
  • 手动「断开/取消」后置抑制位,避免「断开 → 车机回连 → 又被拉起」的死循环;蓝牙链路真断(人离开/熄火)时自动解除。

需要你做一步:到「蓝牙配对」里把车机设为目标车机,否则按设计不会触发(这是故意的)。另外荣耀/华为系 ROM 若禁止本应用「自启动」,系统不会为广播拉起进程,功能会静默失效——界面在电池优化未豁免时会提示一次,按提示放行即可。

二、通话音频通道

通话在车机上没声音的结构性原因:audioType 只在 SETUP 里出现一次,而车机把 media 与 telephony 当成两台不同的输出设备。媒体流一直占着媒体通路,通话通路从来没有被打开过——调音量、补同步包都不可能修好。

  • AudioProfile 增输入档(麦克风上行);下行档 key 保持历史格式,音频档位记忆不失效
  • AirPlayClient:从 /info 的 telephony 档生成候选(AAC 优先、102 先于 100);应答改为按 streamConnectionID 认流——多流并存时按 type 会取到上一条流的 dataPort
  • SessionController:媒体 ↔ 通话换流(先建新、成功后停旧、补 RECORD);通话看门狗改双向 + 2 秒防抖 + 跟随 audioSender 字段(换流后不重启线程)
  • VirtualIap2Link:0x4154 订阅应答改用真实通话状态(原先恒报无通话),通话档上线后主动补推 0x4155

限制(已写入 docs/audio-call-issues.md):普通 App 抓不到通话双方音频(AudioPlaybackCapture 不允许 VOICE_COMMUNICATION,VOICE_CALL 需系统签名),所以本版只做到协议层与通路;麦克风上行只生成候选并打日志,端口/密钥方向待车机日志确认。

验证

  • 单测 250 例全绿:新增 CarApproachPolicyTest(10 例,守住「不误触发/不重复触发」)、AudioProfileKeyTest(守住档位 key 的历史格式,记忆不失效)
  • 蓝牙路径真机跑通:日志可见 APPROACH: 检测到目标车机蓝牙靠近,App 已被自动拉起 → AUTOCONNECT 接管并开始连接
  • 通话通路需实车确认,本版只保证协议与换流逻辑

分支说明

本版从 main 构建。bieke 分支已并入本版全部内容,另含别克 E5(B233/E2LB)建链、0x4300 报文体形态阶梯修复、链路层 DETECT 心跳,属实验线,不在本 Release 内。