From 6e2774ff5caebc84464dc704bb63c1a8644d5f71 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Wed, 11 Nov 2026 21:21:59 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(bt):=20M54=20=E5=85=A5=E7=AB=99=20ACL?= =?UTF-8?q?=EF=BC=88`>=20ACL`=EF=BC=89=E8=A7=A3=E6=9E=90=20=E2=80=94=20?= =?UTF-8?q?=E8=80=B3=E6=9C=BA=E8=87=AA=E4=B8=BB=E5=9B=9E=E8=BF=9E=E5=90=8E?= =?UTF-8?q?=20connected=3D0=20=E8=80=8C=E5=A3=B0=E9=9F=B3=E6=AD=A3?= =?UTF-8?q?=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/ ACL** 链路报 connected=1 且 bt_connected_text 列出 "Device 33:DE:2E:21:AD:30 MOONDROP Pudding"。 --- patch_v2/meow_netmgrd/meow_netmgrd.c | 36 ++++++--- patch_v2/meow_netmgrd/test_bt_ctl.c | 109 +++++++++++++++++++++++---- 2 files changed, 118 insertions(+), 27 deletions(-) diff --git a/patch_v2/meow_netmgrd/meow_netmgrd.c b/patch_v2/meow_netmgrd/meow_netmgrd.c index 6ba2d6c..bee91c1 100644 --- a/patch_v2/meow_netmgrd/meow_netmgrd.c +++ b/patch_v2/meow_netmgrd/meow_netmgrd.c @@ -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)。 - * 行样本:"\t< ACL 33:DE:2E:21:AD:30 handle 12 state 1 lm MASTER";只认 "< ACL" 行, - * MAC 不完整(截断)跳过;表头 "Connections:" 自然不匹配。 */ + * 真机行样本(od -c 校对,行首是 TAB): + * "\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) { 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) { const char *nl = strchr(line, '\n'); const char *e = nl ? nl : line + strlen(line); - const char *acl = NULL; - for (const char *q = line; q + 6 <= e; q++) { - if (!strncmp(q, "< ACL ", 6)) { acl = q + 6; break; } - } - if (acl) { - int ok = 1; - for (int i = 0; i < 17 && ok; i++) { + const char *q = line; + while (q < e && (*q == ' ' || *q == '\t' || *q == '\r')) q++; + if (q + 6 <= e && (q[0] == '<' || q[0] == '>') && !memcmp(q + 1, " ACL ", 5)) { + const char *acl = q + 6; + int ok = (acl + 17 <= e); + for (int i = 0; ok && i < 17; i++) { if (i % 3 == 2) { if (acl[i] != ':') ok = 0; } else if (bt_hex((unsigned char)acl[i]) < 0) ok = 0; } - if (ok && acl + 17 <= e) { - memcpy(macs[n], acl, 17); macs[n][17] = 0; n++; + /* MAC 必须在此断词:后面还跟 hex/':' 说明不是完整 BD_ADDR */ + 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; diff --git a/patch_v2/meow_netmgrd/test_bt_ctl.c b/patch_v2/meow_netmgrd/test_bt_ctl.c index c575e7f..f39c223 100644 --- a/patch_v2/meow_netmgrd/test_bt_ctl.c +++ b/patch_v2/meow_netmgrd/test_bt_ctl.c @@ -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 = "\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) { /* 宿主临时目录:Termux/Android 上 /tmp 不可写,优先 $TMPDIR(mktemp 同规则) */ @@ -102,7 +113,10 @@ int main(void) " connect)\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" - " 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" " disconnect)\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); wf(p, sh); chmod(p, 0755); } - /* 桩:hcitool con —— 按 conn_=yes 标记生成 ACL 行 */ + /* 桩:hcitool con —— 按 conn_ 文件内容选方向/链路类型(真机字节形态): + * in → "\t> ACL … "(远端发起,耳机自主回连,M54 真机 od -c 原文) + * sco → "\t< SCO … "(SCO 链路,不是 ACL) + * 其它非空值(yes)→ "\t< ACL … "(本端发起) */ snprintf(p, sizeof p, "%s/hcitool", T); { - char sh[900]; + char sh[1200]; snprintf(sh, sizeof sh, "#!/bin/sh\n" "S='%s/state'\n" @@ -129,9 +146,13 @@ int main(void) "for f in \"$S\"/conn_*; do\n" " [ -f \"$f\" ] || continue\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" - " 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" "exit 0\n", T); wf(p, sh); chmod(p, 0755); @@ -297,21 +318,49 @@ int main(void) } /* ================= 3. hcitool con 解析 ================= */ - printf("== bt_acl_parse(hcitool con 原始输出/空/截断) ==\n"); + printf("== bt_acl_parse(hcitool con 真机字节:> / < / SCO / 表头 / 空 / 截断 / 大小写) ==\n"); { char macs[8][24]; - CHECK(bt_acl_parse("Connections:\n", macs, 8) == 0); - CHECK(bt_acl_parse("", macs, 8) == 0); + /* 入站(远端发起):M54 的 bug 现场 —— 只认 "< ACL" 时这里返回 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" - "\t< ACL 33:DE:2E:21:AD:30 handle 12 state 1 lm MASTER\n" - "\t< ACL 11:22:33:44:55:66 handle 13 state 1 lm SLAVE\n", macs, 8); + "\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 12 state 1 lm SLAVE \n", macs, 8); CHECK(n == 2); CHECK(strcmp(macs[0], "33:DE:2E:21:AD:30") == 0); CHECK(strcmp(macs[1], "11:22:33:44:55:66") == 0); - /* SCO 行不是 ACL;截断 MAC 丢掉 */ - n = bt_acl_parse("\t< SCO 33:DE:2E:21:AD:30 handle 12 state 1\n" - "\t< ACL 33:DE:2E:21:A\n", macs, 8); - CHECK(n == 0); + /* SCO 链路不是 ACL:两个方向都不收 */ + CHECK(bt_acl_parse("\t< SCO 33:DE:2E:21:AD:30 handle 13 state 1 lm MASTER \n", macs, 8) == 0); + CHECK(bt_acl_parse("\t> SCO 33:DE:2E:21:AD:30 handle 13 state 1 lm MASTER \n", macs, 8) == 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 解析 ================= */ @@ -653,6 +702,30 @@ int main(void) 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"); bt_ctl_execute("connect", "33:DE:2E:21:AD:30", &r); CHECK(r.code == MEOW_OK); @@ -674,9 +747,13 @@ int main(void) printf("== bt disconnect 假成功:命令 exit 0 但 ACL 仍在 → 仍报 ERR_NODEV ==\n"); /* 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"); - 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(strstr(r.text, "仍有该 ACL") != NULL); bt_ctl_execute("disconnect", "AA:AA:AA:AA:AA:AA", &r); /* 本来就没 ACL:真态=已断开 */ From 1e2604953d01383925b2b1e07fa1220c44d852c8 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Wed, 11 Nov 2026 21:44:24 +0800 Subject: [PATCH 2/2] =?UTF-8?q?docs(bt):=20M54=20r1=20=E8=AF=84=E5=AE=A1?= =?UTF-8?q?=20P2=20=E2=80=94=20=E5=85=A5=E7=AB=99=20hcitool=20con=20?= =?UTF-8?q?=E6=A0=B7=E6=9C=AC=E5=86=85=E8=81=94=20od=20-c=20=E5=8E=9F?= =?UTF-8?q?=E5=A7=8B=E6=8D=95=E8=8E=B7=EF=BC=88=E4=BB=85=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 评审指出 S_CON_INBOUND 注释声称「od -c 逐字节校对」,但仓库与设备上都找不到 那份捕获件,traceability 不可审计。把 2026-11-11 20:53(0.8.50 / 10.58.215.50 / 耳机自主回连现场)`hcitool con 2>&1 | od -c` 的原样输出内联进注释块,并写清 逐字节读法、80 字节/末偏移 0000120 的自校验、以及复核命令(printf | od -c)。 同时如实注明该次采集未单独落盘(devdocs/evidence/ 只有 m50 那一份),出站样本 仍指 devdocs/evidence/m50_bt_result_20261111.log:7-8 的 cat -A 证据。 仅注释变化:meow_netmgrd.c 一字未动,测试断言未动(宿主单测 206 ok / 0 FAIL, 连跑 3 次 ALL PASS)。 --- patch_v2/meow_netmgrd/test_bt_ctl.c | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/patch_v2/meow_netmgrd/test_bt_ctl.c b/patch_v2/meow_netmgrd/test_bt_ctl.c index f39c223..f4ae9c3 100644 --- a/patch_v2/meow_netmgrd/test_bt_ctl.c +++ b/patch_v2/meow_netmgrd/test_bt_ctl.c @@ -44,10 +44,28 @@ static const char *S_PLAIN_BLOCK = "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\n" static const char *S_TRUNC_MAC = "\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(同机同耳机,本端发起) */ +/* ---- 真机 `hcitool con` 原始字节样本(两条都可从仓库内自证)---- + * + * S_CON_INBOUND —— 0.8.50 / 设备 10.58.215.50 / 2026-11-11 20:53 / **耳机自主回连**现场, + * `hcitool con 2>&1 | od -c` 的原样输出(每行 16 字节;偏移是八进制,末行 0000120 = 80 字节, + * 与本字面量长度一致;每字节右侧的空格是 od 的字段填充,不是数据): + * 0000000 C o n n e c t i o n s : \n \t > + * 0000020 A C L 3 3 : D E : 2 E : 2 1 : + * 0000040 A D : 3 0 h a n d l e 1 1 + * 0000060 s t a t e 1 l m M A S T E + * 0000100 R A U T H E N C R Y P T \n + * 0000120 + * 逐字节读法:`Connections:` + LF + TAB + `>` + SPC + `ACL` + SPC + 17 字符 MAC + + * ` handle 11 ` + `state 1 lm MASTER AUTH ENCRYPT` + SPC + LF ⇒ 字面量见下方 S_CON_INBOUND。 + * 复核方法:`printf '<上面的字面量>' | od -c`,与本块除「行尾字段填充空格」外逐行相同 + * (本块为免源码尾随空格删掉了那点填充;数据以 80 字节 / 末偏移 0000120 为准)。 + * 这次采集没有单独落盘成证据文件(`devdocs/evidence/` 只有 m50 那一份),上面这块 od 输出 + * 就是它的原样记录;同一条命令的「`Connections:` 表头 + 行首 TAB + 行尾 SPC 后 LF」框架字节 + * 可与 S_CON_OUTBOUND 的来源(下面那份 `cat -A` 证据)互相印证。 + * + * S_CON_OUTBOUND —— devdocs/evidence/m50_bt_result_20261111.log:7-8(同机同耳机,本端发起)。 + * `cat -A` 核为 `Connections:$` + `^I< ACL 33:DE:2E:21:AD:30 handle 0 state 5 lm MASTER $`, + * 与下面的字面量逐字节一致(行首 TAB、行尾一个空格再 LF)。 */ 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";