fix(bt): M54 入站 ACL(> ACL)解析 — 耳机自主回连后 connected=0 而声音正常
bt_acl_parse 原先只认行内 "< ACL ",而 hcitool con 对**远端发起**的连接打的是
"> ACL "。耳机自动回连(最常见的连接方式)时于是:bt connected 空、bt status
connected=0、bt connect 报「未生效(hcitool con 无该 ACL)」,而 audiod 用
strstr("ACL") 认得出、声音正常从耳机出 —— 用户看到的就是「看不出到底连没连」。
改法:白名单取代裸子串匹配 —— 行首(可带空白)→ '<' 或 '>' → ' ' → "ACL" → ' '
→ 17 字符 BD_ADDR(且必须在此断词:再跟 hex/':' 不算)。因此 SCO 行、表头、
截断行都不收。MAC 统一大写(与 audiod bt_dev_acl_macs / bluetoothctl 一致)。
单一解析器:bt_acl_macs / bt_acl_has_tri(写后真态复查)/ bt_connected_text /
bt_status.connected_n 全部经 bt_acl_parse,无第二套。
单测用真机原始字节(od -c 校对):
入站 0.8.50 设备 10.58.215.50 2026-11-11 20:53 "\t> ACL 33:DE:2E:21:AD:30
handle 11 state 1 lm MASTER AUTH ENCRYPT \n"
出站 devdocs/evidence/m50_bt_result_20261111.log:8 "\t< ACL ... handle 0
state 5 lm MASTER \n"
覆盖 >/</>SCO/<SCO/表头/空/NULL/无方向符/截断(行中与行尾)/伪 MAC/大小写/CRLF/
max 裁剪,另加端到端:入站时 connected、status connected=1、connect 不报未生效、
SCO-only 不计连接、disconnect 假成功对入站 ACL 也报「仍有该 ACL」。
用旧解析器跑同一套测试 18 条 FAIL(证明用例不是自证)。
真机验证:同一时刻 0.8.50 旧二进制 bt status connected=0,而按新源码交叉编译
(mipsel 静态)的探针在设备上对**实时 > ACL** 链路报 connected=1 且
bt_connected_text 列出 "Device 33:DE:2E:21:AD:30 MOONDROP Pudding"。
This commit is contained in:
1 parent
26cab3a0ed
commit
6e2774ff5c
2 files changed
+118
-27
No files matched your search
@@ -970,8 +970,14 @@ static int bt_scan_line_parse(const char *raw, char *mac, size_t mcap, char *nam
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 解析 `hcitool con` 输出 → ACL MAC 列表(返回数量 ≤max)。
|
/* 解析 `hcitool con` 输出 → ACL MAC 列表(返回数量 ≤max)。
|
||||||
* 行样本:"\t< ACL 33:DE:2E:21:AD:30 handle 12 state 1 lm MASTER";只认 "< ACL" 行,
|
* 真机行样本(od -c 校对,行首是 TAB):
|
||||||
* MAC 不完整(截断)跳过;表头 "Connections:" 自然不匹配。 */
|
* "\t> ACL 33:DE:2E:21:AD:30 handle 11 state 1 lm MASTER AUTH ENCRYPT " ← 远端发起(耳机自己回连)
|
||||||
|
* "\t< ACL 33:DE:2E:21:AD:30 handle 0 state 5 lm MASTER " ← 本端发起
|
||||||
|
* 方向符 `>`/`<` 必须**都认**:耳机自动回连是 `>`(M54 真机 0.8.50 实测,只认 `<` 时
|
||||||
|
* `bt connected` 空、`bt status connected=0`,而声音其实正常从耳机出)。
|
||||||
|
* 白名单 = 行首(可带空白)→ '<' 或 '>' → 空格 → "ACL" → 空格 → 17 字符 BD_ADDR;
|
||||||
|
* 不写成裸 "ACL" 子串匹配,所以 "< SCO ..."(SCO 链路不是 ACL)、"Connections:" 表头、
|
||||||
|
* 截断行都不收。MAC 统一大写(与 audiod bt_dev_acl_macs / bluetoothctl 输出一致)。 */
|
||||||
static int bt_acl_parse(const char *raw, char macs[][24], int max)
|
static int bt_acl_parse(const char *raw, char macs[][24], int max)
|
||||||
{
|
{
|
||||||
int n = 0;
|
int n = 0;
|
||||||
@@ -980,18 +986,26 @@ static int bt_acl_parse(const char *raw, char macs[][24], int max)
|
|||||||
while (line && *line && n < max) {
|
while (line && *line && n < max) {
|
||||||
const char *nl = strchr(line, '\n');
|
const char *nl = strchr(line, '\n');
|
||||||
const char *e = nl ? nl : line + strlen(line);
|
const char *e = nl ? nl : line + strlen(line);
|
||||||
const char *acl = NULL;
|
const char *q = line;
|
||||||
for (const char *q = line; q + 6 <= e; q++) {
|
while (q < e && (*q == ' ' || *q == '\t' || *q == '\r')) q++;
|
||||||
if (!strncmp(q, "< ACL ", 6)) { acl = q + 6; break; }
|
if (q + 6 <= e && (q[0] == '<' || q[0] == '>') && !memcmp(q + 1, " ACL ", 5)) {
|
||||||
}
|
const char *acl = q + 6;
|
||||||
if (acl) {
|
int ok = (acl + 17 <= e);
|
||||||
int ok = 1;
|
for (int i = 0; ok && i < 17; i++) {
|
||||||
for (int i = 0; i < 17 && ok; i++) {
|
|
||||||
if (i % 3 == 2) { if (acl[i] != ':') ok = 0; }
|
if (i % 3 == 2) { if (acl[i] != ':') ok = 0; }
|
||||||
else if (bt_hex((unsigned char)acl[i]) < 0) ok = 0;
|
else if (bt_hex((unsigned char)acl[i]) < 0) ok = 0;
|
||||||
}
|
}
|
||||||
if (ok && acl + 17 <= e) {
|
/* MAC 必须在此断词:后面还跟 hex/':' 说明不是完整 BD_ADDR */
|
||||||
memcpy(macs[n], acl, 17); macs[n][17] = 0; n++;
|
if (ok && acl + 17 < e) {
|
||||||
|
unsigned char c = (unsigned char)acl[17];
|
||||||
|
if (c == ':' || bt_hex(c) >= 0) ok = 0;
|
||||||
|
}
|
||||||
|
if (ok) {
|
||||||
|
for (int i = 0; i < 17; i++) {
|
||||||
|
char c = acl[i];
|
||||||
|
macs[n][i] = (c >= 'a' && c <= 'f') ? (char)(c - 'a' + 'A') : c;
|
||||||
|
}
|
||||||
|
macs[n][17] = 0; n++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
line = nl ? nl + 1 : NULL;
|
line = nl ? nl + 1 : NULL;
|
||||||
|
|||||||
@@ -44,6 +44,17 @@ static const char *S_PLAIN_BLOCK = "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\n"
|
|||||||
static const char *S_TRUNC_MAC =
|
static const char *S_TRUNC_MAC =
|
||||||
"\033[0;94m[bluetooth]\033[0m# \r\033[K[NEW] Device 33:DE:2E:21:A";
|
"\033[0;94m[bluetooth]\033[0m# \r\033[K[NEW] Device 33:DE:2E:21:A";
|
||||||
|
|
||||||
|
/* ---- 真机 `hcitool con` 原始字节(od -c 逐字节校对,行首 TAB、行尾一个空格 + \n)----
|
||||||
|
* S_CON_INBOUND:0.8.50 设备 10.58.215.50,2026-11-11 20:53,**耳机自主回连**(M54 复现现场):
|
||||||
|
* od -c → "Connections:\n\t> ACL 33:DE:2E:21:AD:30 handle 11 state 1 lm MASTER AUTH ENCRYPT \n"
|
||||||
|
* S_CON_OUTBOUND:devdocs/evidence/m50_bt_result_20261111.log:8(同机同耳机,本端发起) */
|
||||||
|
static const char *S_CON_INBOUND =
|
||||||
|
"Connections:\n"
|
||||||
|
"\t> ACL 33:DE:2E:21:AD:30 handle 11 state 1 lm MASTER AUTH ENCRYPT \n";
|
||||||
|
static const char *S_CON_OUTBOUND =
|
||||||
|
"Connections:\n"
|
||||||
|
"\t< ACL 33:DE:2E:21:AD:30 handle 0 state 5 lm MASTER \n";
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
/* 宿主临时目录:Termux/Android 上 /tmp 不可写,优先 $TMPDIR(mktemp 同规则) */
|
/* 宿主临时目录:Termux/Android 上 /tmp 不可写,优先 $TMPDIR(mktemp 同规则) */
|
||||||
@@ -102,7 +113,10 @@ int main(void)
|
|||||||
" connect)\n"
|
" connect)\n"
|
||||||
" case \"$2\" in CC:*) sleep 5;; esac\n"
|
" case \"$2\" in CC:*) sleep 5;; esac\n"
|
||||||
" case \"$2\" in AA:*) echo 'Failed to connect: org.bluez.Error.Failed br-connection-refused'; exit 1;; esac\n"
|
" case \"$2\" in AA:*) echo 'Failed to connect: org.bluez.Error.Failed br-connection-refused'; exit 1;; esac\n"
|
||||||
" echo yes > \"$S/conn_$2\"; echo yes > \"$S/paired_$2\"\n"
|
/* 已入站连接时 connect 是幂等 no-op:真机上内核 ACL 仍是 `>` 行,别把方向改成 `<` */
|
||||||
|
" prev=; [ -f \"$S/conn_$2\" ] && IFS= read -r prev < \"$S/conn_$2\"\n"
|
||||||
|
" [ \"$prev\" = in ] || echo yes > \"$S/conn_$2\"\n"
|
||||||
|
" echo yes > \"$S/paired_$2\"\n"
|
||||||
" echo 'Attempting to connect'; echo 'Connection successful'; exit 0;;\n"
|
" echo 'Attempting to connect'; echo 'Connection successful'; exit 0;;\n"
|
||||||
" disconnect)\n"
|
" disconnect)\n"
|
||||||
" case \"$2\" in AA:*) echo 'Failed to disconnect: not connected'; exit 1;; esac\n"
|
" case \"$2\" in AA:*) echo 'Failed to disconnect: not connected'; exit 1;; esac\n"
|
||||||
@@ -118,10 +132,13 @@ int main(void)
|
|||||||
"exit 0\n", T);
|
"exit 0\n", T);
|
||||||
wf(p, sh); chmod(p, 0755);
|
wf(p, sh); chmod(p, 0755);
|
||||||
}
|
}
|
||||||
/* 桩:hcitool con —— 按 conn_<mac>=yes 标记生成 ACL 行 */
|
/* 桩:hcitool con —— 按 conn_<mac> 文件内容选方向/链路类型(真机字节形态):
|
||||||
|
* in → "\t> ACL … "(远端发起,耳机自主回连,M54 真机 od -c 原文)
|
||||||
|
* sco → "\t< SCO … "(SCO 链路,不是 ACL)
|
||||||
|
* 其它非空值(yes)→ "\t< ACL … "(本端发起) */
|
||||||
snprintf(p, sizeof p, "%s/hcitool", T);
|
snprintf(p, sizeof p, "%s/hcitool", T);
|
||||||
{
|
{
|
||||||
char sh[900];
|
char sh[1200];
|
||||||
snprintf(sh, sizeof sh,
|
snprintf(sh, sizeof sh,
|
||||||
"#!/bin/sh\n"
|
"#!/bin/sh\n"
|
||||||
"S='%s/state'\n"
|
"S='%s/state'\n"
|
||||||
@@ -129,9 +146,13 @@ int main(void)
|
|||||||
"for f in \"$S\"/conn_*; do\n"
|
"for f in \"$S\"/conn_*; do\n"
|
||||||
" [ -f \"$f\" ] || continue\n"
|
" [ -f \"$f\" ] || continue\n"
|
||||||
" v=no; IFS= read -r v < \"$f\"\n"
|
" v=no; IFS= read -r v < \"$f\"\n"
|
||||||
" [ \"$v\" = yes ] || continue\n"
|
" case \"$v\" in yes|in|sco) ;; *) continue;; esac\n"
|
||||||
" m=\"${f##*/}\"; m=\"${m#conn_}\"\n"
|
" m=\"${f##*/}\"; m=\"${m#conn_}\"\n"
|
||||||
" echo \"\t< ACL $m handle 12 state 1 lm MASTER\"\n"
|
" case \"$v\" in\n"
|
||||||
|
" in) echo \"\t> ACL $m handle 11 state 1 lm MASTER AUTH ENCRYPT \";;\n"
|
||||||
|
" sco) echo \"\t< SCO $m handle 13 state 1 lm MASTER \";;\n"
|
||||||
|
" *) echo \"\t< ACL $m handle 12 state 1 lm MASTER \";;\n"
|
||||||
|
" esac\n"
|
||||||
"done\n"
|
"done\n"
|
||||||
"exit 0\n", T);
|
"exit 0\n", T);
|
||||||
wf(p, sh); chmod(p, 0755);
|
wf(p, sh); chmod(p, 0755);
|
||||||
@@ -297,21 +318,49 @@ int main(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ================= 3. hcitool con 解析 ================= */
|
/* ================= 3. hcitool con 解析 ================= */
|
||||||
printf("== bt_acl_parse(hcitool con 原始输出/空/截断) ==\n");
|
printf("== bt_acl_parse(hcitool con 真机字节:> / < / SCO / 表头 / 空 / 截断 / 大小写) ==\n");
|
||||||
{
|
{
|
||||||
char macs[8][24];
|
char macs[8][24];
|
||||||
CHECK(bt_acl_parse("Connections:\n", macs, 8) == 0);
|
/* 入站(远端发起):M54 的 bug 现场 —— 只认 "< ACL" 时这里返回 0 */
|
||||||
CHECK(bt_acl_parse("", macs, 8) == 0);
|
CHECK(bt_acl_parse(S_CON_INBOUND, macs, 8) == 1);
|
||||||
|
CHECK(strcmp(macs[0], "33:DE:2E:21:AD:30") == 0);
|
||||||
|
/* 出站(本端发起):M49 起就该过,修入站时不许回归 */
|
||||||
|
CHECK(bt_acl_parse(S_CON_OUTBOUND, macs, 8) == 1);
|
||||||
|
CHECK(strcmp(macs[0], "33:DE:2E:21:AD:30") == 0);
|
||||||
|
/* 两副耳机混排(一副自主回连、一副本端发起)→ 按输出顺序全收 */
|
||||||
int n = bt_acl_parse("Connections:\n"
|
int n = bt_acl_parse("Connections:\n"
|
||||||
"\t< ACL 33:DE:2E:21:AD:30 handle 12 state 1 lm MASTER\n"
|
"\t> ACL 33:DE:2E:21:AD:30 handle 11 state 1 lm MASTER AUTH ENCRYPT \n"
|
||||||
"\t< ACL 11:22:33:44:55:66 handle 13 state 1 lm SLAVE\n", macs, 8);
|
"\t< ACL 11:22:33:44:55:66 handle 12 state 1 lm SLAVE \n", macs, 8);
|
||||||
CHECK(n == 2);
|
CHECK(n == 2);
|
||||||
CHECK(strcmp(macs[0], "33:DE:2E:21:AD:30") == 0);
|
CHECK(strcmp(macs[0], "33:DE:2E:21:AD:30") == 0);
|
||||||
CHECK(strcmp(macs[1], "11:22:33:44:55:66") == 0);
|
CHECK(strcmp(macs[1], "11:22:33:44:55:66") == 0);
|
||||||
/* SCO 行不是 ACL;截断 MAC 丢掉 */
|
/* SCO 链路不是 ACL:两个方向都不收 */
|
||||||
n = bt_acl_parse("\t< SCO 33:DE:2E:21:AD:30 handle 12 state 1\n"
|
CHECK(bt_acl_parse("\t< SCO 33:DE:2E:21:AD:30 handle 13 state 1 lm MASTER \n", macs, 8) == 0);
|
||||||
"\t< ACL 33:DE:2E:21:A\n", macs, 8);
|
CHECK(bt_acl_parse("\t> SCO 33:DE:2E:21:AD:30 handle 13 state 1 lm MASTER \n", macs, 8) == 0);
|
||||||
CHECK(n == 0);
|
/* 表头 / 空 / NULL / 无方向符(裸 "ACL" 不能被当连接) */
|
||||||
|
CHECK(bt_acl_parse("Connections:\n", macs, 8) == 0);
|
||||||
|
CHECK(bt_acl_parse("", macs, 8) == 0);
|
||||||
|
CHECK(bt_acl_parse(NULL, macs, 8) == 0);
|
||||||
|
CHECK(bt_acl_parse("\tACL 33:DE:2E:21:AD:30 handle 1 state 1 lm MASTER \n", macs, 8) == 0);
|
||||||
|
CHECK(bt_acl_parse("no ACL connection\n", macs, 8) == 0);
|
||||||
|
/* 截断 MAC(行中 / 行尾都算)与「MAC 后面还跟 hex/':'」的伪 MAC */
|
||||||
|
CHECK(bt_acl_parse("\t> ACL 33:DE:2E:21:A\n", macs, 8) == 0);
|
||||||
|
CHECK(bt_acl_parse("\t> ACL 33:DE:2E:21:AD:3", macs, 8) == 0);
|
||||||
|
CHECK(bt_acl_parse("\t> ACL 33:DE:2E:21:AD:30:99 handle 1 state 1\n", macs, 8) == 0);
|
||||||
|
/* 大小写归一(hcitool 理论上恒大写,别让下游 join 因大小写错过) */
|
||||||
|
n = bt_acl_parse("\t> ACL 33:de:2e:21:ad:30 handle 11 state 1 lm MASTER \n", macs, 8);
|
||||||
|
CHECK(n == 1);
|
||||||
|
CHECK(strcmp(macs[0], "33:DE:2E:21:AD:30") == 0);
|
||||||
|
/* CRLF 行尾(终端/串口转发形态) */
|
||||||
|
n = bt_acl_parse("\t> ACL 33:DE:2E:21:AD:30 handle 11 state 1 lm MASTER \r\n", macs, 8);
|
||||||
|
CHECK(n == 1);
|
||||||
|
CHECK(strcmp(macs[0], "33:DE:2E:21:AD:30") == 0);
|
||||||
|
/* max 上限裁剪 */
|
||||||
|
n = bt_acl_parse("Connections:\n"
|
||||||
|
"\t> ACL 33:DE:2E:21:AD:30 handle 11 state 1 lm MASTER \n"
|
||||||
|
"\t< ACL 11:22:33:44:55:66 handle 12 state 1 lm SLAVE \n"
|
||||||
|
"\t> ACL 22:33:44:55:66:77 handle 13 state 1 lm SLAVE \n", macs, 2);
|
||||||
|
CHECK(n == 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ================= 4. list-pcms / info 解析 ================= */
|
/* ================= 4. list-pcms / info 解析 ================= */
|
||||||
@@ -653,6 +702,30 @@ int main(void)
|
|||||||
CHECK(g_bt_tmo_streak == 0);
|
CHECK(g_bt_tmo_streak == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
printf("== 入站回连(hcitool con 的 `> ACL`):connected / status / connect 复查都必须认 ==\n");
|
||||||
|
{ /* 真机复现场景(M54):耳机自己回连,行首是 '>'。修前:connected 空、connected=0、
|
||||||
|
* connect 报「未生效(hcitool con 无该 ACL)」。桩按 conn 文件内容生成方向。 */
|
||||||
|
snprintf(p, sizeof p, "%s/state/conn_33:DE:2E:21:AD:30", T); wf(p, "in\n");
|
||||||
|
bt_ctl_execute("connected", NULL, &r);
|
||||||
|
CHECK(r.code == MEOW_OK);
|
||||||
|
CHECK(strstr(r.text, "Device 33:DE:2E:21:AD:30 MOONDROP Pudding") != NULL);
|
||||||
|
bt_ctl_execute("status", NULL, &r);
|
||||||
|
CHECK(strstr(r.text, "connected=1") != NULL);
|
||||||
|
CHECK(bt_acl_has_tri("33:DE:2E:21:AD:30", 1500) == 1); /* 写后复查走的也是这个解析器 */
|
||||||
|
/* 已连(入站)时再点连接:ACL 仍是 '>' 行,必须报已连接,不许报「未生效」 */
|
||||||
|
bt_ctl_execute("connect", "33:DE:2E:21:AD:30", &r);
|
||||||
|
CHECK(r.code == MEOW_OK);
|
||||||
|
CHECK(strstr(r.text, "已连接") != NULL);
|
||||||
|
CHECK(strstr(r.text, "未生效") == NULL);
|
||||||
|
/* 只改了链路类型(SCO 行)→ 不算连接:防止「把 SCO 当 ACL」的宽匹配回归 */
|
||||||
|
wf(p, "sco\n");
|
||||||
|
bt_ctl_execute("status", NULL, &r);
|
||||||
|
CHECK(strstr(r.text, "connected=0") != NULL);
|
||||||
|
bt_ctl_execute("connected", NULL, &r);
|
||||||
|
CHECK(r.text[0] == 0);
|
||||||
|
unlink(p);
|
||||||
|
}
|
||||||
|
|
||||||
printf("== bt connect:成功路径以 hcitool con 复查 ==\n");
|
printf("== bt connect:成功路径以 hcitool con 复查 ==\n");
|
||||||
bt_ctl_execute("connect", "33:DE:2E:21:AD:30", &r);
|
bt_ctl_execute("connect", "33:DE:2E:21:AD:30", &r);
|
||||||
CHECK(r.code == MEOW_OK);
|
CHECK(r.code == MEOW_OK);
|
||||||
@@ -674,9 +747,13 @@ int main(void)
|
|||||||
|
|
||||||
printf("== bt disconnect 假成功:命令 exit 0 但 ACL 仍在 → 仍报 ERR_NODEV ==\n");
|
printf("== bt disconnect 假成功:命令 exit 0 但 ACL 仍在 → 仍报 ERR_NODEV ==\n");
|
||||||
/* BB:* 的 disconnect 桩打印成功但不删 ACL(模拟「命令说成功、链路其实还在」)。 */
|
/* BB:* 的 disconnect 桩打印成功但不删 ACL(模拟「命令说成功、链路其实还在」)。 */
|
||||||
snprintf(p, sizeof p, "%s/state/conn_BB:BB:BB:BB:BB:BB", T); wf(p, "yes\n");
|
snprintf(p, sizeof p, "%s/state/conn_BB:BB:BB:BB:BB:BB", T); wf(p, "in\n");
|
||||||
snprintf(p, sizeof p, "%s/state/paired_BB:BB:BB:BB:BB:BB", T); wf(p, "yes\n");
|
snprintf(p, sizeof p, "%s/state/paired_BB:BB:BB:BB:BB:BB", T); wf(p, "yes\n");
|
||||||
bt_ctl_execute("disconnect", "BB:BB:BB:BB:BB:BB", &r);
|
bt_ctl_execute("disconnect", "BB:BB:BB:BB:BB:BB", &r); /* 入站 ACL 残留也必须被看见 */
|
||||||
|
CHECK(r.code == MEOW_ERR_NODEV);
|
||||||
|
CHECK(strstr(r.text, "仍有该 ACL") != NULL);
|
||||||
|
snprintf(p, sizeof p, "%s/state/conn_BB:BB:BB:BB:BB:BB", T); wf(p, "yes\n");
|
||||||
|
bt_ctl_execute("disconnect", "BB:BB:BB:BB:BB:BB", &r); /* 出站形态同样要看见 */
|
||||||
CHECK(r.code == MEOW_ERR_NODEV);
|
CHECK(r.code == MEOW_ERR_NODEV);
|
||||||
CHECK(strstr(r.text, "仍有该 ACL") != NULL);
|
CHECK(strstr(r.text, "仍有该 ACL") != NULL);
|
||||||
bt_ctl_execute("disconnect", "AA:AA:AA:AA:AA:AA", &r); /* 本来就没 ACL:真态=已断开 */
|
bt_ctl_execute("disconnect", "AA:AA:AA:AA:AA:AA", &r); /* 本来就没 ACL:真态=已断开 */
|
||||||
|
|||||||
Reference in new issue
Block a user