fix(bt): M49 r1 评审 — 写操作预算压回 1500ms 窗口内 + A2DP 总预算/TTL 缓存 + 测试可重复 + rfkill 顺序;rebase 到 main 保住 M43 time32
rebase 到 main(00e7271,M43 time32 已合入) 时解开 build_netmgrd.sh 冲突:**两者都保留**—— M43 的 `../sdk/meow_time32.c` + `-DMEOW_TIME32_LIBC_OVERRIDES`,以及我的 ROOT/ZIGCC/OUT 环境变量化。编译改在 mktemp 目录里做(产物名仍是字面量 `-o meow_netmgrd`,供 sdk/test_time32_abi.sh 的按名定位),编完再落到 OUT —— 设了 OUT 也不会覆盖/删除仓库里 那份被跟踪的二进制(r1 期间 mv 曾把它删掉过)。门禁自证:`--builds` → build meow_netmgrd src=yes macro=yes;`--no-builds` → 4263=3 4403=0 16B-layout=OK ALL PASS。 P1-1(写操作越过客户端 1500ms 窗口):预算模型改成**每一层都受限**—— `BT_ACT_TMO_MS 800 / BT_ACT_SHORT_MS 600 / BT_VERIFY_BUDGET_MS 220 / gap 90`, `bt_poll_probe` 把**剩余预算**传给探测函数(`bt_acl_has_tri`/`bt_info_flag` 新增 tmo 形参), 探测自身那次 run_cmd 也受同一个 deadline 约束;`bt_onoff` 同理(up 失败只在剩余预算内重试 一次);成功文案不再补设备名(省掉 1-2 次 fork,UI 本来就按 MAC join)。 探测改为三态:命令失败/超时 = -1(**未知**),不再被当成「没有 ACL」——否则 disconnect 会在 查不到的情况下报「已断开」,又是一次假成功。 实测(宿主桩驱动 bt_ctl_execute,命令卡死 5s 的形态):connect 1042-1047ms、pair 1042-1045ms、 bt on 831ms,全部 < 1450 且有 400ms+ 余量(r1 实测 1528/2424ms)。设备侧实测 fork 成本 ≈25-30ms/次(bt a2dp 50ms、bt status 100ms),余量足够。 P2-1(A2DP 1+N 子进程塞进 status JSON):`bt_a2dp_query(buf,cap,budget)` 加**总预算**, list-pcms 单命令 ≤400ms、每个 info ≤300ms、剩下端点超预算就不再查;`bt a2dp` 命令用 900ms。 status JSON 不在无连接时查(0 次 fork),有连接时走 10s TTL 缓存 + 500ms 预算; `a2dp_codec`/`a2dp_rate` 字段保留(MeowPanel btCard 依赖,见 docs/plan/FUTURE_MEOWPANEL.md:48)。 实测 4 端点全卡死:911ms(r1 6053ms);status JSON 首次 350-384ms、缓存命中 31-49ms。 P2-2(test_bt_ctl 不可重复):开跑先 `rm -rf` 状态目录、退出再清;连跑 3 次 ALL PASS(贴了输出)。 P2-3(bt_onoff 顺序 + 多节点):开=先 rfkill unblock(**全部**同类型节点)再 hciconfig up, up 未生效时剩余预算内重试一次;关=先 down 再 block。新增 `rfkill_set_block_all()`(旧 `rfkill_set_block` 仍只写首个节点,WiFi 路径行为不变)。桩改成「blocked 时 up 必败」,并加了 反向用例证明桩真的会失败(否则断言空转)。 P3:①info 的 RUNCMD_EMPTY(exit 0 无输出)不再当失败跳过;②`bt_pcm_paths_parse` 的 `/a2dpsrc` 搜索限制在行内 [p,e)(原来会跨 \n 命中下一行);③`bt_devices_text` 注释写明 「本轮扫描见到过的设备」而非「此刻在场」([DEL] 不删除、devices 块含 BlueZ 历史缓存)。 附带(塔 11:06 建议,BT 作用域内先落地):连续 3 次 RUNCMD_TIMEOUT 后熔断 30s,冷却期内 直接回「熔断中」不再 fork(真机 55 个挂死 bluetoothctl 吃光进程槽的事故);任何非超时返回清零。 单测覆盖:第 4 次调用 0ms 返回。范围只限 BT 写操作,WiFi/DNS 调用点未动。 真机(本轮只读,设备已恢复在线):新件 md5 e62e7512 上卡只读验收 status/paired/connected/ devices/a2dp 全通;`ps w | grep -c bluetoothctl` 前后均为 2(≤3);Music du 199164544; 临时目录/独立实例清理干净,/usr/bin 未动。
This commit is contained in:
1 parent
370c290160
commit
239cebae01
4 files changed
+470
-114
No files matched your search
@@ -19,7 +19,7 @@ ROOT="${MEOW_ROOT:-$(cd ../.. && pwd)}"
|
||||
CC="${ZIGCC:-$ROOT/.tools/zig-mipsel-cc}"
|
||||
[ -x "$CC" ] || CC="$HOME/.tools/zig-mipsel-cc"
|
||||
I_HDR="$ROOT/patch_v2/meow_launcherd" # meow_ipc.h(帧/MSG_NET_*/m_net_*)
|
||||
OUT="${OUT:-.}"
|
||||
OUT="${OUT:-.}" # 产物目录:默认脚本目录;设了则在交叉编译后把产物移过去
|
||||
[ -d "$OUT" ] || mkdir -p "$OUT"
|
||||
|
||||
if [ "$1" = "host" ]; then
|
||||
@@ -36,8 +36,16 @@ fi
|
||||
# meow_time32.c:独立 o32 time32 取时/睡眠(netmgrd 不链 meow_ui.a);修 DNS TTL 基准
|
||||
# 与 nanosleep 退避。守护/CLI 同一二进制,一次构建两处安装。(M43 的 time32 接入,必须保留:
|
||||
# 少了它 mono_now()/轮询退避会踩 mipsel o32 的 time64/time32 结构错位。)
|
||||
"$CC" -Os -Wall -Wextra -static -I"$I_HDR" -DMEOW_TIME32_LIBC_OVERRIDES \
|
||||
meow_netmgrd.c ../sdk/meow_time32.c -o "$OUT/meow_netmgrd"
|
||||
# 注意:`-o meow_netmgrd` 保持字面量(不带目录前缀)——sdk/test_time32_abi.sh 的构建门禁正是
|
||||
# 按 `-o <产物名>` 定位编译行来断言 time32 接入;写成 `-o "$OUT/..."` 会让门禁报「未找到」。
|
||||
# 编译在临时目录里做(产物名仍是字面量),编完再落到 OUT:这样设了 OUT 也**不会**覆盖/删除
|
||||
# 仓库里那份被跟踪的 patch_v2/meow_netmgrd/meow_netmgrd(M49 曾因 mv 把它删掉过一次)。
|
||||
HERE="$(pwd)"
|
||||
BUILD_DIR="$(mktemp -d "${TMPDIR:-/tmp}/netmgrd-build.XXXXXX")"
|
||||
( cd "$BUILD_DIR" && "$CC" -Os -Wall -Wextra -static -I"$I_HDR" -DMEOW_TIME32_LIBC_OVERRIDES \
|
||||
"$HERE/meow_netmgrd.c" "$HERE/../sdk/meow_time32.c" -o meow_netmgrd )
|
||||
mv -f "$BUILD_DIR/meow_netmgrd" "$OUT/meow_netmgrd"
|
||||
rmdir "$BUILD_DIR" 2>/dev/null || true
|
||||
echo "== meow_netmgrd (mipsel static) =="
|
||||
ls -la "$OUT/meow_netmgrd"
|
||||
md5sum "$OUT/meow_netmgrd"
|
||||
|
||||
@@ -89,14 +89,30 @@ static char g_net_policy[160]= "/data/meow/netmgrd/net_policy.conf"; /* MEOW_NET
|
||||
* 普通 ~10s;隐藏网络(connecth,主动探测+关联更慢)~15s。env 可调(宿主桩测试缩短)。 */
|
||||
#define WIFI_CONNECT_WAIT_MS 10000 /* MEOW_WIFI_CONNECT_WAIT_MS */
|
||||
#define WIFI_CONNECTH_WAIT_MS 15000 /* MEOW_WIFI_CONNECTH_WAIT_MS */
|
||||
/* 蓝牙写操作(connect/pair)要等 BlueZ 关联/配对收敛,但 launcher 侧 `netmgr_bt()` 的 IPC 超时是
|
||||
* NM_TMO_DFL=1500ms(netmgr_ipc.c:21,非本单 scope)——守护侧一旦超过它,客户端会判「不可达」并
|
||||
* 回退直连再下发一次。所以这里的预算=「命令 1100ms + 复查 350ms」,绝不越过客户端的 1500ms 窗口;
|
||||
* 真态复查拿不到就如实报「未收敛」,不再像 P1 之前那样恒报成功。 */
|
||||
#define BT_ACT_TMO_MS 1100 /* connect/pair 的 bluetoothctl 上限 */
|
||||
#define BT_ACT_SHORT_MS 900 /* disconnect/unpair/on/off */
|
||||
#define BT_VERIFY_BUDGET_MS 350 /* 写后真态复查总预算 */
|
||||
#define BT_VERIFY_GAP_MS 120 /* 每轮间隔(ms) */
|
||||
/* 蓝牙写操作的**总预算**必须压在 launcher 侧 `netmgr_bt()` 的 IPC 超时(NM_TMO_DFL=1500ms,
|
||||
* netmgr_ipc.c,非本单 scope)以内:越线即被客户端判「不可达」→ 直连回退再下发一次 →
|
||||
* 刚写好的「未生效」文案被客户端丢掉,净效果退回「恒报成功」。
|
||||
* 预算模型(每一层都受限,不是只在两次探测之间看 deadline):
|
||||
* 命令 ≤ BT_ACT_TMO_MS / BT_ACT_SHORT_MS
|
||||
* + 复查 ≤ BT_VERIFY_BUDGET_MS,且**每次探测的 run_cmd 也只用剩余预算**
|
||||
* + 收尾 reap(正常/超时路径都先 SIGKILL,不额外等 300ms)
|
||||
* 目标上界 ≈ 1150ms(MIPS 更慢的 fork/exec 留了余量),实测见 test_bt_ctl 的 `budget-*` 用例。 */
|
||||
#define BT_ACT_TMO_MS 800 /* connect/pair 的 bluetoothctl 上限 */
|
||||
#define BT_ACT_SHORT_MS 600 /* disconnect/unpair/on/off */
|
||||
#define BT_VERIFY_BUDGET_MS 220 /* 写后真态复查总预算(含探测内部 run_cmd) */
|
||||
#define BT_VERIFY_GAP_MS 90 /* 每轮间隔(ms) */
|
||||
/* A2DP 查询是 1+N 个子进程(list-pcms + 每端点 info),而守护单线程、DNS 代理同一主循环,
|
||||
* 所以**每条命令与整段查询都设上限**;status JSON 另走 TTL 缓存(见 g_bt_a2dp_*)。 */
|
||||
#define BT_A2DP_TEXT_BUDGET_MS 900 /* `bt a2dp` 命令总预算(客户端 1500ms 窗口内) */
|
||||
#define BT_A2DP_STATUS_BUDGET_MS 500 /* status JSON 刷新缓存时的总预算 */
|
||||
#define BT_A2DP_LIST_TMO_MS 400 /* list-pcms 单命令上限 */
|
||||
#define BT_A2DP_INFO_TMO_MS 300 /* 每个端点 info 单命令上限 */
|
||||
#define BT_A2DP_CACHE_SEC 10 /* status JSON 的 a2dp 摘要缓存 TTL(秒) */
|
||||
/* 防堆积熔断(真机 0.8.49 事故:55 个挂死的 bluetoothctl 吃光进程槽/内存,launcherd 与 sshd
|
||||
* 同时失去 fork 能力)。BT 写操作**连续** NN 次超时后,冷却期内不再 fork 新命令,如实回
|
||||
* 「熔断中」;任何非超时返回清零。范围只限 BT 写操作,不影响 WiFi/DNS 调用点。 */
|
||||
#define BT_TMO_STREAK_MAX 3
|
||||
#define BT_TMO_COOLDOWN_SEC 30
|
||||
#define IFACE_MAX 16
|
||||
#define RFKILL_MAX 8
|
||||
#define DNS_MAX 8
|
||||
@@ -745,8 +761,9 @@ static void wifi_status(wifi_t *w)
|
||||
|
||||
/* ---------------- 蓝牙:hciconfig/bluetoothctl 包装 ---------------- */
|
||||
/* hci0 真实 power 态:1=up 0=down -1=未知。sysfs `/sys/class/bluetooth/hci0/power` 优先,
|
||||
* 回退 `hciconfig hci0` 行首 UP/DOWN(P1 之后从 bt_status 抽出,写操作复查也用它)。 */
|
||||
static int bt_powered_now(void)
|
||||
* 回退 `hciconfig hci0` 行首 UP/DOWN(P1 之后从 bt_status 抽出,写操作复查也用它)。
|
||||
* tmo_ms 只管回退那次 run_cmd:写后复查要能把它压进总预算,读路径传 RUNCMD_TMO_MS。 */
|
||||
static int bt_powered_now(int tmo_ms)
|
||||
{
|
||||
char p[192];
|
||||
snprintf(p, sizeof p, "%s/hci0/power", g_bt_dir);
|
||||
@@ -758,7 +775,7 @@ static int bt_powered_now(void)
|
||||
char cmd[160];
|
||||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s hci0 2>/dev/null", g_hciconfig);
|
||||
char buf[256];
|
||||
if (run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd) <= 0) return -1;
|
||||
if (run_cmd(buf, sizeof buf, tmo_ms, cmd) <= 0) return -1;
|
||||
char *ln = buf;
|
||||
while (ln && *ln) {
|
||||
char *nl = strchr(ln, '\n');
|
||||
@@ -773,7 +790,7 @@ static int bt_powered_now(void)
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int bt_acl_macs(char macs[][24], int max); /* 前向声明(bt_status 早于其定义) */
|
||||
static int bt_acl_macs(char macs[][24], int max, int tmo_ms); /* 前向声明(bt_status 早于其定义) */
|
||||
static void bt_status(bt_t *b)
|
||||
{
|
||||
memset(b, 0, sizeof *b);
|
||||
@@ -783,7 +800,7 @@ static void bt_status(bt_t *b)
|
||||
snprintf(p, sizeof p, "%s/hci0", g_bt_dir);
|
||||
if (access(p, F_OK) == 0) b->available = 1;
|
||||
if (!b->available) return;
|
||||
{ int pw = bt_powered_now(); b->powered = pw > 0 ? 1 : 0; }
|
||||
{ int pw = bt_powered_now(RUNCMD_TMO_MS); b->powered = pw > 0 ? 1 : 0; }
|
||||
/* address:/sys/.../address 优先,回退 hciconfig "BD Address:" */
|
||||
snprintf(p, sizeof p, "%s/hci0/address", g_bt_dir);
|
||||
read_file_txt(p, b->address, sizeof b->address);
|
||||
@@ -825,7 +842,7 @@ static void bt_status(bt_t *b)
|
||||
/* 已连接数:内核 ACL(hcitool con)权威;快速,不受 bluetoothctl 过滤/慢启动影响。 */
|
||||
{
|
||||
char macs[8][24];
|
||||
b->connected_n = bt_acl_macs(macs, 8);
|
||||
b->connected_n = bt_acl_macs(macs, 8, RUNCMD_TMO_MS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1009,6 +1026,18 @@ static int bt_path_mac(const char *path, char *mac, size_t mcap)
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 在 [s, e) 内找子串 needle(不跨行匹配;行内 strstr 会越过 '\n' 命中下一行)。
|
||||
* 返回 1=命中。纯函数。 */
|
||||
static int bt_mem_has(const char *s, const char *e, const char *needle)
|
||||
{
|
||||
size_t nl = strlen(needle);
|
||||
if (!nl || e <= s) return 0;
|
||||
for (const char *q = s; q + nl <= e; q++) {
|
||||
if (!memcmp(q, needle, nl)) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 解析 `bluealsa-cli list-pcms` → A2DP 端点路径(每行一个 /org/bluealsa/... 路径)。
|
||||
* 只收 a2dpsrc/a2dpsnk(SCO/HFP 端点不算 A2DP);行尾空白/CR 去掉。返回数量(≤max)。 */
|
||||
static int bt_pcm_paths_parse(const char *raw, char paths[][80], int max)
|
||||
@@ -1022,8 +1051,7 @@ static int bt_pcm_paths_parse(const char *raw, char paths[][80], int max)
|
||||
const char *p = line;
|
||||
while (p < e && (*p == ' ' || *p == '\t')) p++;
|
||||
while (e > p && (e[-1] == ' ' || e[-1] == '\t' || e[-1] == '\r')) e--;
|
||||
if (e - p > 14 && !strncmp(p, "/org/bluealsa/", 14) &&
|
||||
(strstr(p, "/a2dpsrc") || strstr(p, "/a2dpsnk"))) {
|
||||
if (e - p > 14 && (bt_mem_has(p, e, "/a2dpsrc") || bt_mem_has(p, e, "/a2dpsnk"))) {
|
||||
size_t l = (size_t)(e - p);
|
||||
if (l > 79) l = 79;
|
||||
memcpy(paths[n], p, l); paths[n][l] = 0; n++;
|
||||
@@ -1082,7 +1110,11 @@ static void bt_pcm_info_parse(const char *raw, bt_pcm_info_t *o)
|
||||
char *p = clean;
|
||||
while (*p == ' ' || *p == '\t') p++;
|
||||
char v[64];
|
||||
if (!o->codec[0] && bt_kv(p, "Codec", v, sizeof v) && v[0]) {
|
||||
/* 真机 v4.1.1 的键名(M50 原文):Selected codec / Available codecs / Format: S16_LE /
|
||||
* Running: true|false / Channels / Sampling / Transport / Mode / Delay / Volume / Muted。
|
||||
* 注意 Available codecs 是空格分隔的**列表**,绝不能当 codec;Selected codec 才是当前值。 */
|
||||
if (!o->codec[0] &&
|
||||
(bt_kv(p, "Selected codec", v, sizeof v) || bt_kv(p, "Codec", v, sizeof v)) && v[0]) {
|
||||
snprintf(o->codec, sizeof o->codec, "%s", v); o->has = 1;
|
||||
} else if (!o->rate[0] && bt_kv(p, "Sampling", v, sizeof v) && v[0]) {
|
||||
snprintf(o->rate, sizeof o->rate, "%s", v); o->has = 1;
|
||||
@@ -1094,6 +1126,8 @@ static void bt_pcm_info_parse(const char *raw, bt_pcm_info_t *o)
|
||||
snprintf(o->bits, sizeof o->bits, "%s", v); o->has = 1;
|
||||
} else if (bt_kv(p, "Format", v, sizeof v) && v[0]) {
|
||||
snprintf(fmt, sizeof fmt, "%s", v); o->has = 1;
|
||||
} else if (bt_kv(p, "Mode", v, sizeof v) && v[0]) {
|
||||
o->has = 1; /* sink/source:本机侧 PCM 模式(不是 codec/不是 MAC) */
|
||||
} else if (bt_kv(p, "State", v, sizeof v) && v[0]) {
|
||||
snprintf(o->state, sizeof o->state, "%s", v); o->has = 1;
|
||||
if (strstr(v, "RUNNING") || strstr(v, "ACTIVE") || strstr(v, "PLAYING")) o->running = 1;
|
||||
@@ -1107,9 +1141,16 @@ static void bt_pcm_info_parse(const char *raw, bt_pcm_info_t *o)
|
||||
}
|
||||
line = nl ? nl + 1 : NULL;
|
||||
}
|
||||
/* Format 兜底:如 "44100 Hz, 2 channels, 16 bits" / "44100 Hz 2 channels 16 bits" */
|
||||
/* Format 兜底:真机是 ALSA 格式串(S16_LE / S24_3LE),也可能是
|
||||
* "44100 Hz, 2 channels, 16 bits" 这种打包写法。 */
|
||||
if (fmt[0]) {
|
||||
char *q;
|
||||
if (!o->bits[0] && (fmt[0] == 'S' || fmt[0] == 'U')) {
|
||||
char *n = fmt + 1;
|
||||
int bw = 0;
|
||||
while (*n >= '0' && *n <= '9') { bw = bw * 10 + (*n - '0'); n++; }
|
||||
if (bw > 0 && bw <= 32) snprintf(o->bits, sizeof o->bits, "%d", bw);
|
||||
}
|
||||
if (!o->rate[0] && (q = strstr(fmt, "Hz")) != NULL) {
|
||||
char *b = q;
|
||||
while (b > fmt && (b[-1] == ' ' || b[-1] == '\t')) b--; /* 跳过 "Hz" 前的空格 */
|
||||
@@ -1144,34 +1185,44 @@ static void bt_pcm_info_parse(const char *raw, bt_pcm_info_t *o)
|
||||
}
|
||||
}
|
||||
|
||||
/* 内核 ACL 连接 MAC(hcitool con)。返回数(≤max)。 */
|
||||
static int bt_acl_macs(char macs[][24], int max)
|
||||
/* 内核 ACL 连接 MAC(hcitool con)。返回数(≤max)。
|
||||
* tmo_ms 由调用方给:读路径用 RUNCMD_TMO_MS,写后复查只给剩余预算。 */
|
||||
static int bt_acl_macs(char macs[][24], int max, int tmo_ms)
|
||||
{
|
||||
char cmd[160];
|
||||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s con 2>/dev/null", g_hcitool);
|
||||
char buf[1024];
|
||||
if (run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd) <= 0) return 0;
|
||||
if (run_cmd(buf, sizeof buf, tmo_ms, cmd) <= 0) return 0;
|
||||
return bt_acl_parse(buf, macs, max);
|
||||
}
|
||||
|
||||
/* 指定 MAC 是否有内核 ACL(hcitool con)。1=有 0=没有。 */
|
||||
static int bt_acl_has(const char *mac)
|
||||
/* 写后复查专用三态:1=有 ACL,0=确实没有,-1=**查不到**(命令失败/超时/被 kill)。
|
||||
* 必须和「没有」分开:探测超时被当成「没有连接」的话,disconnect 就会在查不到的情况下
|
||||
* 报「已断开」——那正是本单要消灭的「假成功」。 */
|
||||
static int bt_acl_has_tri(const char *mac, int tmo_ms)
|
||||
{
|
||||
char cmd[160];
|
||||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s con 2>/dev/null", g_hcitool);
|
||||
char buf[1024];
|
||||
runcmd_res_t rr;
|
||||
int n = run_cmd_ex(buf, sizeof buf, tmo_ms, cmd, &rr);
|
||||
if (n < 0 || rr.status == RUNCMD_TIMEOUT || rr.status == RUNCMD_EXEC ||
|
||||
rr.status == RUNCMD_EXIT) return -1;
|
||||
char macs[8][24];
|
||||
int n = bt_acl_macs(macs, 8);
|
||||
for (int i = 0; i < n; i++) if (bt_mac_eq(macs[i], mac)) return 1;
|
||||
int cnt = bt_acl_parse(buf, macs, 8);
|
||||
for (int i = 0; i < cnt; i++) if (bt_mac_eq(macs[i], mac)) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* `bluetoothctl info <mac>` 的布尔字段真态(Paired / Connected / Trusted):
|
||||
* 1=yes 0=no -1=未知(设备不在 BlueZ 列表里 / 后端失败)。 */
|
||||
static int bt_info_flag(const char *mac, const char *key)
|
||||
static int bt_info_flag(const char *mac, const char *key, int tmo_ms)
|
||||
{
|
||||
char cmd[224];
|
||||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s info %s 2>&1", g_bluetoothctl, mac);
|
||||
char buf[1024];
|
||||
runcmd_res_t rr;
|
||||
if (run_cmd_ex(buf, sizeof buf, RUNCMD_TMO_MS, cmd, &rr) <= 0) return -1;
|
||||
if (run_cmd_ex(buf, sizeof buf, tmo_ms, cmd, &rr) <= 0) return -1;
|
||||
char keyp[32];
|
||||
snprintf(keyp, sizeof keyp, "%s:", key);
|
||||
const char *p = strstr(buf, keyp);
|
||||
@@ -1218,7 +1269,7 @@ static void bt_connected_text(char *buf, int cap)
|
||||
{
|
||||
int off = 0;
|
||||
char macs[8][24];
|
||||
int n = bt_acl_macs(macs, 8);
|
||||
int n = bt_acl_macs(macs, 8, RUNCMD_TMO_MS);
|
||||
for (int i = 0; i < n; i++) {
|
||||
char name[64] = "";
|
||||
bt_name_for_mac(macs[i], name, sizeof name);
|
||||
@@ -1240,19 +1291,24 @@ static void bt_connected_text(char *buf, int cap)
|
||||
* 所以:本函数是「此刻快照」;没有端点 → "A2DP none"(= 当前无 A2DP 活动,不是错误)。
|
||||
* 字段缺失写 "-"(info 输出认得但字段名对不上时也照样出行,至少让 UI 看见端点在);info 查询
|
||||
* 失败(端点刚好消失)就跳过该端点;一个都没有 → "A2DP none"。 */
|
||||
static void bt_a2dp_text(char *buf, int cap)
|
||||
static void bt_a2dp_query(char *buf, int cap, int budget_ms)
|
||||
{
|
||||
if (cap <= 0) return;
|
||||
buf[0] = 0;
|
||||
long deadline = mono_ms() + (budget_ms > 0 ? budget_ms : 1);
|
||||
char cmd[176];
|
||||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s list-pcms 2>&1", g_bluealsa_cli);
|
||||
char raw[1024];
|
||||
runcmd_res_t rr;
|
||||
run_cmd_ex(raw, sizeof raw, RUNCMD_TMO_MS, cmd, &rr);
|
||||
long remain = deadline - mono_ms();
|
||||
if (remain <= 0) { snprintf(buf, (size_t)cap, "A2DP none\n"); return; }
|
||||
run_cmd_ex(raw, sizeof raw, (int)(remain > BT_A2DP_LIST_TMO_MS ? BT_A2DP_LIST_TMO_MS : remain), cmd, &rr);
|
||||
char paths[4][80];
|
||||
int np = bt_pcm_paths_parse(raw, paths, 4);
|
||||
int off = 0;
|
||||
for (int i = 0; i < np; i++) {
|
||||
remain = deadline - mono_ms();
|
||||
if (remain <= 0) break; /* 总预算用完:剩下的端点不再查(宁缺不卡) */
|
||||
char mac[24] = "-";
|
||||
if (!bt_path_mac(paths[i], mac, sizeof mac)) continue;
|
||||
const char *role = strstr(paths[i], "a2dpsnk") ? "snk" : "src";
|
||||
@@ -1261,8 +1317,10 @@ static void bt_a2dp_text(char *buf, int cap)
|
||||
g_bluealsa_cli, paths[i]);
|
||||
char ibuf[1024];
|
||||
runcmd_res_t ir;
|
||||
run_cmd_ex(ibuf, sizeof ibuf, RUNCMD_TMO_MS, cmd2, &ir);
|
||||
if (ir.status != RUNCMD_OK) continue; /* 端点消失/查询失败:跳过,不编造 */
|
||||
run_cmd_ex(ibuf, sizeof ibuf, (int)(remain > BT_A2DP_INFO_TMO_MS ? BT_A2DP_INFO_TMO_MS : remain),
|
||||
cmd2, &ir);
|
||||
/* 端点消失/查询失败:跳过,不编造。EMPTY(exit 0 无输出)不算失败——端点确实在。 */
|
||||
if (ir.status != RUNCMD_OK && ir.status != RUNCMD_EMPTY) continue;
|
||||
bt_pcm_info_t info;
|
||||
bt_pcm_info_parse(ibuf, &info);
|
||||
/* info 成功但字段名一个都没命中(版本差异)→ 仍出行,字段写 "-":
|
||||
@@ -1279,13 +1337,24 @@ static void bt_a2dp_text(char *buf, int cap)
|
||||
else buf[off] = 0;
|
||||
}
|
||||
|
||||
/* A2DP 摘要(首个带 codec/rate 的端点)给 overview JSON;失败置空。 */
|
||||
static void bt_a2dp_summary(char *codec, int clen, char *rate, int rlen)
|
||||
/* `bt a2dp` 命令入口:总预算 BT_A2DP_TEXT_BUDGET_MS(客户端 1500ms 窗口内)。 */
|
||||
static void bt_a2dp_text(char *buf, int cap)
|
||||
{
|
||||
bt_a2dp_query(buf, cap, BT_A2DP_TEXT_BUDGET_MS);
|
||||
}
|
||||
|
||||
/* status JSON 用的 a2dp 摘要缓存:TTL + 总预算,且**无连接时直接不查**(没 ACL 就没有 A2DP
|
||||
* 端点,一次 fork 都不必花)。守护单线程、DNS 代理同一主循环,status 是高频路径。 */
|
||||
static long g_bt_a2dp_cache_ms = 0;
|
||||
static char g_bt_a2dp_codec[16] = "", g_bt_a2dp_rate[16] = "";
|
||||
|
||||
/* A2DP 摘要(首个带 codec/rate 的端点);budget_ms 为总预算,失败置空。纯读,不改缓存。 */
|
||||
static void bt_a2dp_summary(char *codec, int clen, char *rate, int rlen, int budget_ms)
|
||||
{
|
||||
if (codec && clen > 0) codec[0] = 0;
|
||||
if (rate && rlen > 0) rate[0] = 0;
|
||||
char buf[MEOW_IPC_MAX - 16];
|
||||
bt_a2dp_text(buf, sizeof buf);
|
||||
bt_a2dp_query(buf, sizeof buf, budget_ms);
|
||||
char *line = buf;
|
||||
while (line && *line) {
|
||||
char *nl = strchr(line, '\n'); if (nl) *nl = 0;
|
||||
@@ -1304,7 +1373,10 @@ static void bt_a2dp_summary(char *codec, int clen, char *rate, int rlen)
|
||||
|
||||
/* 附近设备文本:netmgrd 权威扫描缓存(/tmp/meow_bt_scan.txt),行 "Device <MAC> <Name>"(去重)。
|
||||
* M49:实时 `[NEW] Device` 行以 ANSI ESC 开头(见 bt_scan_line_parse 注释里的字节证据),
|
||||
* 旧实现只认行首 "Device "/"[NEW] Device " → 实时行全丢,只能靠末尾 `devices` 块。 */
|
||||
* 旧实现只认行首 "Device "/"[NEW] Device " → 实时行全丢,只能靠末尾 `devices` 块。
|
||||
* `[DEL]` 的处理是「不新增」而不是「删掉已有」:bt_scan_on 每次扫描都用 `>` 整文件覆盖缓存,
|
||||
* 所以跨会话不会累积;但**同一次扫描窗口内**先出现后离开的设备仍会留在列表里,且末尾
|
||||
* `devices` 块本身含 BlueZ 历史缓存——所以这份列表是「本轮扫描见到过的设备」,不是「此刻在场」。 */
|
||||
static void bt_devices_text(char *buf, int cap)
|
||||
{
|
||||
int off = 0;
|
||||
@@ -1377,7 +1449,9 @@ static void rfkill_list(void)
|
||||
}
|
||||
|
||||
/* 找某类型 rfkill 设备并写 soft=block(1)/unblock(0)。返回 0=ok;-1=无该类型/写失败。 */
|
||||
static int rfkill_set_block(const char *type, int block)
|
||||
/* rfkill 软件开关。all=0 只写第一个同类型节点(旧行为),all=1 写**全部**同类型节点:
|
||||
* 真机有 rfkill0/rfkill3 两个 bluetooth 节点,只写首个会让另一个仍处于 block。 */
|
||||
static int rfkill_set_block_ex(const char *type, int block, int all)
|
||||
{
|
||||
char norm[16] = "";
|
||||
if (!strcmp(type, "wifi")) snprintf(norm, sizeof norm, "%s", "wlan");
|
||||
@@ -1398,12 +1472,15 @@ static int rfkill_set_block(const char *type, int block)
|
||||
char one = block ? '1' : '0';
|
||||
if (write(fd, &one, 1) == 1) rc = 0;
|
||||
close(fd);
|
||||
break;
|
||||
if (!all) break;
|
||||
}
|
||||
closedir(d);
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
static int rfkill_set_block(const char *type, int block) { return rfkill_set_block_ex(type, block, 0); }
|
||||
/* 蓝牙用「全节点」版本:任一节点仍 block 就等于没开(M47 finding 的第二个点)。 */
|
||||
static int rfkill_set_block_all(const char *type, int block) { return rfkill_set_block_ex(type, block, 1); }
|
||||
|
||||
/* ---------------- udhcpc:续租/释放(SIGUSR1/SIGUSR2,不改启动路径) ---------------- */
|
||||
static int udhcpc_ctl(const char *op, const char *ifname)
|
||||
@@ -1654,10 +1731,23 @@ static void build_status_json(char *out, size_t cap)
|
||||
jadd_str_or_null(&w, "address", bt.address);
|
||||
jadd_int_or_null(&w, "paired", bt.paired_n);
|
||||
jadd_int_or_null(&w, "connected", bt.connected_n);
|
||||
{ char ac[16] = "", ar[16] = "";
|
||||
bt_a2dp_summary(ac, sizeof ac, ar, sizeof ar);
|
||||
jadd_str_or_null(&w, "a2dp_codec", ac);
|
||||
jadd_str_or_null(&w, "a2dp_rate", ar); }
|
||||
/* a2dp_codec/a2dp_rate 是 MeowPanel btCard 的字段(docs/plan/FUTURE_MEOWPANEL.md:48),
|
||||
* 但 status 是高频路径且守护单线程(DNS 代理同环)——所以:无连接直接不查(0 次 fork),
|
||||
* 有连接时走 TTL 缓存,刷新也只给 BT_A2DP_STATUS_BUDGET_MS。`bt a2dp` 命令另有独立调用。 */
|
||||
if (bt.connected_n > 0) {
|
||||
long now_s = mono_now();
|
||||
if (!g_bt_a2dp_cache_ms || now_s - g_bt_a2dp_cache_ms >= BT_A2DP_CACHE_SEC) {
|
||||
g_bt_a2dp_cache_ms = now_s;
|
||||
bt_a2dp_summary(g_bt_a2dp_codec, sizeof g_bt_a2dp_codec,
|
||||
g_bt_a2dp_rate, sizeof g_bt_a2dp_rate, BT_A2DP_STATUS_BUDGET_MS);
|
||||
}
|
||||
} else {
|
||||
g_bt_a2dp_cache_ms = 0;
|
||||
g_bt_a2dp_codec[0] = 0;
|
||||
g_bt_a2dp_rate[0] = 0;
|
||||
}
|
||||
jadd_str_or_null(&w, "a2dp_codec", g_bt_a2dp_codec);
|
||||
jadd_str_or_null(&w, "a2dp_rate", g_bt_a2dp_rate);
|
||||
jadd(&w, "\"dns\":[");
|
||||
{
|
||||
char ns[DNS_MAX][40];
|
||||
@@ -3384,45 +3474,101 @@ static void bt_snip(const char *s, char *dst, size_t cap)
|
||||
dst[o] = 0;
|
||||
}
|
||||
|
||||
typedef int (*bt_probe_fn)(const char *);
|
||||
static int bt_probe_acl(const char *mac) { return bt_acl_has(mac); }
|
||||
static int bt_probe_paired(const char *mac) { return bt_info_flag(mac, "Paired"); }
|
||||
/* 探测函数带 tmo:写后复查的**总**预算必须传到每一次 run_cmd 里,否则「两次探测之间看
|
||||
* deadline」挡不住探测自身那 1500ms(r1 评审 P1:实测 connect 1528ms / bt on 2424ms,
|
||||
* 越过客户端 NM_TMO_DFL=1500 的 IPC 窗口 → 客户端判不可达 → 二次下发 + 假成功)。 */
|
||||
typedef int (*bt_probe_fn)(const char *mac, int tmo_ms);
|
||||
static int bt_probe_acl(const char *mac, int tmo_ms) { return bt_acl_has_tri(mac, tmo_ms); }
|
||||
static int bt_probe_paired(const char *mac, int tmo_ms) { return bt_info_flag(mac, "Paired", tmo_ms); }
|
||||
|
||||
/* 写后真态复查:在 BT_VERIFY_BUDGET_MS 内轮询 probe,一旦命中 want 立刻返回;
|
||||
* 返回最后一次探测值(1/0/-1=未知)。总耗时受限于客户端 1500ms 的 IPC 预算。 */
|
||||
static int bt_poll_probe(bt_probe_fn fn, const char *mac, int want)
|
||||
/* 熔断器状态(只服务 BT 写操作;守护单线程,无需加锁)。 */
|
||||
static int g_bt_tmo_streak = 0;
|
||||
static long g_bt_tmo_until = 0; /* 冷却截止(mono 秒),0=未熔断 */
|
||||
|
||||
static int bt_circuit_open(void)
|
||||
{
|
||||
long deadline = mono_ms() + BT_VERIFY_BUDGET_MS;
|
||||
int v = fn(mac);
|
||||
while (v != want && mono_ms() < deadline) {
|
||||
return (g_bt_tmo_streak >= BT_TMO_STREAK_MAX && g_bt_tmo_until && mono_now() < g_bt_tmo_until);
|
||||
}
|
||||
static void bt_circuit_note(runcmd_status_t st)
|
||||
{
|
||||
if (st == RUNCMD_TIMEOUT) {
|
||||
if (g_bt_tmo_streak < BT_TMO_STREAK_MAX) g_bt_tmo_streak++;
|
||||
if (g_bt_tmo_streak >= BT_TMO_STREAK_MAX) g_bt_tmo_until = mono_now() + BT_TMO_COOLDOWN_SEC;
|
||||
} else {
|
||||
g_bt_tmo_streak = 0;
|
||||
g_bt_tmo_until = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 写后真态复查:总预算 budget_ms,**每次探测只拿剩余预算**,一旦命中 want 立刻返回。
|
||||
* 返回最后一次探测值(1/0/-1=未知)。总耗时 ≤ budget_ms + 一次探测的收尾。 */
|
||||
static int bt_poll_probe(bt_probe_fn fn, const char *mac, int want, int budget_ms)
|
||||
{
|
||||
long deadline = mono_ms() + budget_ms;
|
||||
long remain = budget_ms;
|
||||
int v = fn(mac, (int)(remain > 0 ? remain : 1));
|
||||
while (v != want) {
|
||||
remain = deadline - mono_ms();
|
||||
if (remain <= 0) break;
|
||||
struct timespec ts = { 0, BT_VERIFY_GAP_MS * 1000000L };
|
||||
nanosleep(&ts, NULL);
|
||||
v = fn(mac);
|
||||
remain = deadline - mono_ms();
|
||||
if (remain <= 0) break;
|
||||
v = fn(mac, (int)remain);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
/* bt on/off:hciconfig + rfkill,然后按真实 power 态定稿(不再「下发即成功」)。 */
|
||||
/* bt on/off:顺序按 M47 finding 走 —— 开:先解 rfkill(全部节点)再 up(blocked 时 up 必败);
|
||||
* 关:先 down 再 block。up 第一次没生效时在**剩余预算内**重试一次(射频刚放开常差一拍)。 */
|
||||
static void bt_onoff(const char *cmd, ctl_result_t *r)
|
||||
{
|
||||
int up = strcmp(cmd, "on") == 0;
|
||||
int want = up ? 1 : 0;
|
||||
long deadline = mono_ms() + BT_ACT_SHORT_MS + BT_VERIFY_BUDGET_MS;
|
||||
char cmd2[192];
|
||||
snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s hci0 %s",
|
||||
g_hciconfig, up ? "up" : "down");
|
||||
char buf[256];
|
||||
buf[0] = 0;
|
||||
runcmd_res_t res;
|
||||
memset(&res, 0, sizeof res);
|
||||
res.exit_code = -1;
|
||||
res.status = RUNCMD_EXEC;
|
||||
|
||||
if (bt_circuit_open()) {
|
||||
snprintf(r->text, sizeof r->text,
|
||||
"bt %s: 熔断中(连续 %d 次命令超时,%ld 秒内不再下发;蓝牙栈可能卡住)",
|
||||
cmd, g_bt_tmo_streak, g_bt_tmo_until - mono_now());
|
||||
r->code = MEOW_ERR_NODEV;
|
||||
return;
|
||||
}
|
||||
if (up) rfkill_set_block_all("bluetooth", 0); /* 先解 block,否则 up 会被拦下 */
|
||||
run_cmd_ex(buf, sizeof buf, BT_ACT_SHORT_MS, cmd2, &res);
|
||||
rfkill_set_block("bluetooth", up ? 0 : 1);
|
||||
int want = up ? 1 : 0;
|
||||
int pw = -1;
|
||||
long deadline = mono_ms() + BT_VERIFY_BUDGET_MS;
|
||||
bt_circuit_note(res.status);
|
||||
if (!up) rfkill_set_block_all("bluetooth", 1); /* 先 down 再 block */
|
||||
|
||||
int pw = -1, retried = 0;
|
||||
for (;;) {
|
||||
pw = bt_powered_now();
|
||||
if (pw == want || mono_ms() >= deadline) break;
|
||||
long remain = deadline - mono_ms();
|
||||
if (remain <= 0) break;
|
||||
pw = bt_powered_now((int)remain);
|
||||
if (pw == want) break;
|
||||
if (up && !retried) { /* up 未生效:剩余预算内重试一次 */
|
||||
retried = 1;
|
||||
remain = deadline - mono_ms();
|
||||
if (remain > 0) {
|
||||
run_cmd_ex(buf, sizeof buf, (int)(remain > BT_ACT_SHORT_MS ? BT_ACT_SHORT_MS : remain),
|
||||
cmd2, &res);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
remain = deadline - mono_ms();
|
||||
if (remain <= 0) break;
|
||||
struct timespec ts = { 0, BT_VERIFY_GAP_MS * 1000000L };
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
char msg[240];
|
||||
char msg[280];
|
||||
if (pw == want) {
|
||||
snprintf(msg, sizeof msg, "bt %s: hci0 %s + rfkill %s(power 已复查)",
|
||||
cmd, up ? "up" : "down", up ? "unblocked" : "blocked");
|
||||
@@ -3442,19 +3588,29 @@ static void bt_onoff(const char *cmd, ctl_result_t *r)
|
||||
}
|
||||
|
||||
/* connect/disconnect/pair/unpair:下发 + 按真态(hcitool con / bluetoothctl info)定稿。
|
||||
* 旧实现 `n >= 0 → MEOW_OK` 恒报成功(命令失败也返回 0 字节),这是「点配对提示成功但没配上」的根因。 */
|
||||
* 旧实现 `n >= 0 → MEOW_OK` 恒报成功(命令失败也返回 0 字节),这是「点配对提示成功但没配上」的根因。
|
||||
* 成功文案不再补设备名:那次 bluetoothctl 查询是 1-2 个额外子进程,会吃掉 1500ms 窗口;
|
||||
* UI 本来就按 MAC join 名字(nm_parse_devices)。 */
|
||||
static void bt_write_execute(const char *cmd, const char *arg, ctl_result_t *r)
|
||||
{
|
||||
int is_conn = !strcmp(cmd, "connect"), is_disc = !strcmp(cmd, "disconnect");
|
||||
int is_pair = !strcmp(cmd, "pair"), is_unpair = !strcmp(cmd, "unpair");
|
||||
const char *sub = is_unpair ? "remove" : cmd; /* bluetoothctl 取消配对=remove */
|
||||
int tmo = (is_conn || is_pair) ? BT_ACT_TMO_MS : BT_ACT_SHORT_MS;
|
||||
if (bt_circuit_open()) { /* 连续超时:不再 fork,如实回熔断 */
|
||||
snprintf(r->text, sizeof r->text,
|
||||
"bt %s: 熔断中(连续 %d 次命令超时,%ld 秒内不再下发;蓝牙栈可能卡住)",
|
||||
cmd, g_bt_tmo_streak, g_bt_tmo_until - mono_now());
|
||||
r->code = MEOW_ERR_NODEV;
|
||||
return;
|
||||
}
|
||||
char cmd2[224];
|
||||
snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s %s %s",
|
||||
g_bluetoothctl, sub, arg);
|
||||
char out[512];
|
||||
runcmd_res_t res;
|
||||
run_cmd_ex(out, sizeof out, tmo, cmd2, &res);
|
||||
bt_circuit_note(res.status);
|
||||
int fail = 0;
|
||||
const char *why = run_cmd_why(&res, &fail);
|
||||
char snip[121];
|
||||
@@ -3471,12 +3627,10 @@ static void bt_write_execute(const char *cmd, const char *arg, ctl_result_t *r)
|
||||
|
||||
if (is_conn || is_disc) {
|
||||
int want = is_conn ? 1 : 0;
|
||||
int v = bt_poll_probe(bt_probe_acl, arg, want);
|
||||
int v = bt_poll_probe(bt_probe_acl, arg, want, BT_VERIFY_BUDGET_MS);
|
||||
if (v == want) {
|
||||
char name[64] = "";
|
||||
if (is_conn) bt_name_for_mac(arg, name, sizeof name);
|
||||
snprintf(msg, sizeof msg, "bt %s: %s %s%s%s(hcitool con 已复查)", cmd,
|
||||
is_conn ? "已连接" : "已断开", arg, name[0] ? " " : "", name);
|
||||
snprintf(msg, sizeof msg, "bt %s: %s %s(hcitool con 已复查)", cmd,
|
||||
is_conn ? "已连接" : "已断开", arg);
|
||||
ctl_sets(r, MEOW_OK, msg);
|
||||
return;
|
||||
}
|
||||
@@ -3488,7 +3642,7 @@ static void bt_write_execute(const char *cmd, const char *arg, ctl_result_t *r)
|
||||
|
||||
/* pair / unpair:以 bluetoothctl info 的 Paired: 字段为准 */
|
||||
int want = is_pair ? 1 : 0;
|
||||
int v = bt_poll_probe(bt_probe_paired, arg, want);
|
||||
int v = bt_poll_probe(bt_probe_paired, arg, want, BT_VERIFY_BUDGET_MS);
|
||||
int ok = (v == want);
|
||||
if (!ok && is_unpair && v < 0 && !fail) ok = 1; /* remove 后设备整体消失(info 查不到)也算已解除 */
|
||||
if (ok) {
|
||||
|
||||
@@ -54,17 +54,23 @@ int main(void)
|
||||
const char *T = Tbuf;
|
||||
char p[256];
|
||||
char cmd[512];
|
||||
/* 可重复运行(r1 评审 P2):上一轮遗留的 conn_ / paired_ 状态文件会把「无连接」断言推翻。
|
||||
* 每次开跑先整棵删掉重来,退出时也清(见文件末尾)。 */
|
||||
{ char rm[320]; snprintf(rm, sizeof rm, "rm -rf '%s'", Tbuf); (void)!system(rm); }
|
||||
mkdir(Tbuf, 0755);
|
||||
/* 假 /sys/class/bluetooth/hci0(available/powered/address) */
|
||||
{ snprintf(p, sizeof p, "%s/bt", T); mkdir(p, 0755);
|
||||
snprintf(p, sizeof p, "%s/bt/hci0", T); mkdir(p, 0755);
|
||||
snprintf(p, sizeof p, "%s/bt/hci0/address", T); wf(p, "4c:bc:98:b0:98:db\n");
|
||||
snprintf(p, sizeof p, "%s/bt/hci0/power", T); wf(p, "off\n"); }
|
||||
/* 假 rfkill(bt on/off 会写 soft) */
|
||||
/* 假 rfkill:真机有两个 bluetooth 节点(rfkill0/rfkill3),任一仍 block 就等于没开 */
|
||||
{ snprintf(p, sizeof p, "%s/rfkill", T); mkdir(p, 0755);
|
||||
snprintf(p, sizeof p, "%s/rfkill/rfkill1", T); mkdir(p, 0755);
|
||||
snprintf(p, sizeof p, "%s/rfkill/rfkill1/type", T); wf(p, "bluetooth\n");
|
||||
snprintf(p, sizeof p, "%s/rfkill/rfkill1/soft", T); wf(p, "1\n"); }
|
||||
for (int i = 0; i < 2; i++) {
|
||||
char d[300];
|
||||
snprintf(d, sizeof d, "%s/rfkill/rfkill%d", T, i ? 3 : 0); mkdir(d, 0755);
|
||||
snprintf(p, sizeof p, "%s/type", d); wf(p, "bluetooth\n");
|
||||
snprintf(p, sizeof p, "%s/soft", d); wf(p, "1\n");
|
||||
} }
|
||||
/* 有状态桩的状态目录 */
|
||||
snprintf(p, sizeof p, "%s/state", T); mkdir(p, 0755);
|
||||
|
||||
@@ -94,6 +100,7 @@ int main(void)
|
||||
" echo \" Connected: $cv\"\n"
|
||||
" exit 0;;\n"
|
||||
" 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"
|
||||
" echo 'Attempting to connect'; echo 'Connection successful'; exit 0;;\n"
|
||||
@@ -102,6 +109,7 @@ int main(void)
|
||||
" case \"$2\" in BB:*) echo 'Successful disconnected'; exit 0;; esac\n"
|
||||
" echo no > \"$S/conn_$2\"; echo 'Successful disconnected'; exit 0;;\n"
|
||||
" pair)\n"
|
||||
" case \"$2\" in CC:*) sleep 5;; esac\n"
|
||||
" case \"$2\" in AA:*) echo 'Failed to pair: org.bluez.Error.AuthenticationFailed'; exit 1;; esac\n"
|
||||
" echo yes > \"$S/paired_$2\"; echo 'Pairing successful'; exit 0;;\n"
|
||||
" remove)\n"
|
||||
@@ -135,8 +143,15 @@ int main(void)
|
||||
snprintf(sh, sizeof sh,
|
||||
"#!/bin/sh\n"
|
||||
"S='%s'\n"
|
||||
"if [ -f \"$S/state/hang_hciconfig\" ]; then sleep 5; fi\n"
|
||||
"blocked() { for t in \"$S\"/rfkill/*; do\n"
|
||||
" [ -f \"$t/type\" ] || continue\n"
|
||||
" v=\"\"; IFS= read -r v < \"$t/type\"; [ \"$v\" = bluetooth ] || continue\n"
|
||||
" b=0; IFS= read -r b < \"$t/soft\"; [ \"$b\" = 1 ] && return 0\n"
|
||||
" done; return 1; }\n"
|
||||
"case \"$2\" in\n"
|
||||
" up) echo on > \"$S/bt/hci0/power\"; echo 'hci0: Type: Primary Bus: UART'; echo ' UP RUNNING';;\n"
|
||||
" up) if blocked; then echo 'hci0: Operation not possible due to RF-kill' >&2; exit 1; fi\n"
|
||||
" echo on > \"$S/bt/hci0/power\"; echo 'hci0: Type: Primary Bus: UART'; echo ' UP RUNNING';;\n"
|
||||
" down) echo off > \"$S/bt/hci0/power\"; echo 'hci0: Type: Primary Bus: UART'; echo ' DOWN';;\n"
|
||||
" *) echo 'hci0: Type: Primary Bus: UART'\n"
|
||||
" v=off; if [ -f \"$S/bt/hci0/power\" ]; then IFS= read -r v < \"$S/bt/hci0/power\"; fi\n"
|
||||
@@ -156,26 +171,34 @@ int main(void)
|
||||
"case \"$1\" in\n"
|
||||
" list-pcms) catf \"$S/pcms.txt\"; exit 0;;\n"
|
||||
" info)\n"
|
||||
" if [ -f \"$S/hang_info\" ]; then sleep 5; fi\n"
|
||||
" path=\"$2\"; found=0\n"
|
||||
" if [ -f \"$S/pcms.txt\" ]; then\n"
|
||||
" while IFS= read -r l || [ -n \"$l\" ]; do\n"
|
||||
" if [ \"$l\" = \"$path\" ]; then found=1; fi\n"
|
||||
" done < \"$S/pcms.txt\"\n"
|
||||
" fi\n"
|
||||
" case \"$path\" in *33_DE_2E_21_AD_30*) ;; *) found=0;; esac\n"
|
||||
" case \"$path\" in *dev_33_DE_2E_21_AD_30*) ;; *) found=0;; esac\n"
|
||||
" if [ \"$found\" != 1 ]; then\n"
|
||||
" echo \"bluealsa-cli: E: CMD \\\"info\\\": Couldn't get BlueALSA PCM: Object path not found: '$path'\" >&2\n"
|
||||
" exit 1\n"
|
||||
" fi\n"
|
||||
" echo \"BlueALSA PCM: $path\"\n"
|
||||
" echo ' Transport: A2DP-source'\n"
|
||||
" echo ' Codec: SBC'\n"
|
||||
" echo ' CodecConfiguration: 0x3f'\n"
|
||||
" echo ' Sampling: 44100 Hz'\n"
|
||||
" echo ' Channels: 2'\n"
|
||||
" echo ' Bits per Sample: 16'\n"
|
||||
" echo ' Running: true'\n"
|
||||
" echo ' State: RUNNING'\n"
|
||||
" case \"$path\" in *dev_33_DE_2E_21_AD_30*) ;; *) exit 1;; esac\n"
|
||||
" if [ -f \"$S/running\" ]; then run=true; else run=false; fi\n"
|
||||
" echo \"Device: /org/bluez/hci0/dev_33_DE_2E_21_AD_30\"\n" /* ← 真机原文(M50 2026-11-11 18:26) */
|
||||
" echo 'Sequence: 0'\n"
|
||||
" echo 'Transport: A2DP-source'\n"
|
||||
" echo 'Mode: sink'\n"
|
||||
" echo \"Running: $run\"\n"
|
||||
" echo 'Format: S16_LE'\n"
|
||||
" echo 'Channels: 2'\n"
|
||||
" echo 'Sampling: 48000 Hz'\n"
|
||||
" echo 'Available codecs: SBC AAC'\n"
|
||||
" echo 'Selected codec: AAC'\n"
|
||||
" echo 'Delay: 15.0 ms'\n"
|
||||
" echo 'SoftVolume: false'\n"
|
||||
" echo 'Volume: L: 127 R: 127'\n"
|
||||
" echo 'Muted: L: false R: false'\n"
|
||||
" exit 0;;\n"
|
||||
"esac\n"
|
||||
"exit 0\n", T);
|
||||
@@ -327,20 +350,59 @@ int main(void)
|
||||
CHECK(bt_path_mac("/org/bluealsa/hci0/dev_33_DE_2E_21_AD_3", mac, sizeof mac) == 0);
|
||||
CHECK(bt_path_mac("/org/bluealsa/hci0/nothing", mac, sizeof mac) == 0);
|
||||
|
||||
/* 真机原文(M50 2026-11-11 18:26 实测,idle 与 running 各一份),逐字照抄 */
|
||||
static const char *REAL_IDLE =
|
||||
"Device: /org/bluez/hci0/dev_33_DE_2E_21_AD_30\n"
|
||||
"Sequence: 0\n"
|
||||
"Transport: A2DP-source\n"
|
||||
"Mode: sink\n"
|
||||
"Running: false\n"
|
||||
"Format: S16_LE\n"
|
||||
"Channels: 2\n"
|
||||
"Sampling: 48000 Hz\n"
|
||||
"Available codecs: SBC AAC\n"
|
||||
"Selected codec: AAC\n"
|
||||
"Delay: 15.0 ms\n"
|
||||
"SoftVolume: false\n"
|
||||
"Volume: L: 127 R: 127\n"
|
||||
"Muted: L: false R: false\n";
|
||||
static const char *REAL_RUNNING =
|
||||
"Device: /org/bluez/hci0/dev_33_DE_2E_21_AD_30\n"
|
||||
"Sequence: 3\n"
|
||||
"Transport: A2DP-source\n"
|
||||
"Mode: sink\n"
|
||||
"Running: true\n"
|
||||
"Format: S16_LE\n"
|
||||
"Channels: 2\n"
|
||||
"Sampling: 48000 Hz\n"
|
||||
"Available codecs: SBC AAC\n"
|
||||
"Selected codec: AAC\n"
|
||||
"Delay: 17.2 ms\n"
|
||||
"SoftVolume: false\n"
|
||||
"Volume: L: 127 R: 127\n"
|
||||
"Muted: L: false R: false\n";
|
||||
bt_pcm_info_t info;
|
||||
bt_pcm_info_parse("BlueALSA PCM: /org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc\n"
|
||||
" Transport: A2DP-source\n"
|
||||
bt_pcm_info_parse(REAL_IDLE, &info);
|
||||
CHECK(info.has == 1);
|
||||
CHECK(strcmp(info.codec, "AAC") == 0); /* Selected codec,不是 Available 列表 */
|
||||
CHECK(strcmp(info.rate, "48000 Hz") == 0);
|
||||
CHECK(strcmp(info.channels, "2") == 0);
|
||||
CHECK(strcmp(info.bits, "16") == 0); /* 从 Format: S16_LE 推出来 */
|
||||
CHECK(info.running == 0); /* Running: false */
|
||||
bt_pcm_info_parse(REAL_RUNNING, &info);
|
||||
CHECK(info.running == 1); /* Running: true 才代表真在传 */
|
||||
CHECK(strcmp(info.codec, "AAC") == 0);
|
||||
CHECK(strcmp(info.bits, "16") == 0);
|
||||
/* 兼容旧/其它形态(有人给的是 Codec:/Bits per Sample:/State: RUNNING) */
|
||||
bt_pcm_info_parse(" Transport: A2DP-source\n"
|
||||
" Codec: SBC\n"
|
||||
" CodecConfiguration: 0x3f\n"
|
||||
" Sampling: 44100 Hz\n"
|
||||
" Channels: 2\n"
|
||||
" Bits per Sample: 16\n"
|
||||
" Running: true\n"
|
||||
" State: RUNNING\n", &info);
|
||||
CHECK(info.has == 1);
|
||||
CHECK(strcmp(info.codec, "SBC") == 0); /* 不被 CodecConfiguration 抢走 */
|
||||
CHECK(strcmp(info.codec, "SBC") == 0); /* 不被 CodecConfiguration 抢走 */
|
||||
CHECK(strcmp(info.rate, "44100 Hz") == 0);
|
||||
CHECK(strcmp(info.channels, "2") == 0);
|
||||
CHECK(strcmp(info.bits, "16") == 0);
|
||||
CHECK(info.running == 1);
|
||||
/* 只有 Format 一行(属性打包)时的兜底 */
|
||||
@@ -399,13 +461,20 @@ int main(void)
|
||||
bt_ctl_execute("a2dp", NULL, &r);
|
||||
CHECK(r.code == MEOW_OK);
|
||||
CHECK(strstr(r.text, "A2DP 33:DE:2E:21:AD:30") != NULL);
|
||||
CHECK(strstr(r.text, "codec=SBC") != NULL);
|
||||
CHECK(strstr(r.text, "rate=44100 Hz") != NULL);
|
||||
CHECK(strstr(r.text, "codec=AAC") != NULL); /* 真机 Selected codec */
|
||||
CHECK(strstr(r.text, "rate=48000 Hz") != NULL);
|
||||
CHECK(strstr(r.text, "channels=2") != NULL);
|
||||
CHECK(strstr(r.text, "bits=16") != NULL);
|
||||
CHECK(strstr(r.text, "acquired=1") != NULL);
|
||||
CHECK(strstr(r.text, "acquired=0") != NULL); /* Running: false(空闲,端点还在) */
|
||||
CHECK(strstr(r.text, "role=src") != NULL);
|
||||
{ /* Running: true → acquired=1(权威布尔) */
|
||||
snprintf(p, sizeof p, "%s/state/running", T); wf(p, "1\n");
|
||||
bt_ctl_execute("a2dp", NULL, &r);
|
||||
CHECK(strstr(r.text, "acquired=1") != NULL);
|
||||
unlink(p);
|
||||
}
|
||||
/* 列表里有端点但 info 查不到(刚断开)→ 跳过,不编造 */
|
||||
snprintf(p, sizeof p, "%s/state/pcms.txt", T);
|
||||
wf(p, "/org/bluealsa/hci0/dev_00_00_00_00_00_00/a2dpsrc\n");
|
||||
bt_ctl_execute("a2dp", NULL, &r);
|
||||
CHECK(strcmp(r.text, "A2DP none\n") == 0);
|
||||
@@ -413,6 +482,47 @@ int main(void)
|
||||
bt_ctl_execute("a2dp", NULL, &r);
|
||||
CHECK(strcmp(r.text, "A2DP none\n") == 0);
|
||||
|
||||
printf("== A2DP 预算:端点全卡死时不许把守护拖住(r1 评审 P2-1) ==\n");
|
||||
{
|
||||
char pcms[512];
|
||||
snprintf(pcms, sizeof pcms,
|
||||
"/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink\n"
|
||||
"/org/bluealsa/hci0/dev_11_22_33_44_55_66/a2dpsrc/sink\n"
|
||||
"/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_FF/a2dpsrc/sink\n"
|
||||
"/org/bluealsa/hci0/dev_12_34_56_78_9A_BC/a2dpsnk/source\n");
|
||||
snprintf(p, sizeof p, "%s/state/pcms.txt", T); wf(p, pcms);
|
||||
snprintf(p, sizeof p, "%s/state/hang_info", T); wf(p, "1\n");
|
||||
long t0 = mono_ms();
|
||||
bt_ctl_execute("a2dp", NULL, &r);
|
||||
long dt = mono_ms() - t0;
|
||||
printf(" budget-a2dp(4 端点全卡死) = %ld ms\n", dt);
|
||||
CHECK(dt < 1200); /* 旧实现实测 6053ms */
|
||||
CHECK(r.code == MEOW_OK);
|
||||
unlink(p);
|
||||
snprintf(p, sizeof p, "%s/state/pcms.txt", T); wf(p, "");
|
||||
|
||||
/* status JSON 的 a2dp 摘要:无连接→0 次 fork;有连接→TTL 缓存(第二次几乎免费) */
|
||||
snprintf(p, sizeof p, "%s/state/conn_33:DE:2E:21:AD:30", T); wf(p, "yes\n");
|
||||
snprintf(p, sizeof p, "%s/state/pcms.txt", T);
|
||||
wf(p, "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink\n");
|
||||
snprintf(p, sizeof p, "%s/state/hang_info", T); wf(p, "1\n");
|
||||
static char js[8192];
|
||||
g_bt_a2dp_cache_ms = 0;
|
||||
long t1 = mono_ms();
|
||||
build_status_json(js, sizeof js);
|
||||
long d1 = mono_ms() - t1;
|
||||
long t2 = mono_ms();
|
||||
build_status_json(js, sizeof js);
|
||||
long d2 = mono_ms() - t2;
|
||||
printf(" budget-status-json 1st = %ld ms, 2nd(cache) = %ld ms\n", d1, d2);
|
||||
CHECK(d1 < 700); /* 单次刷新受 BT_A2DP_STATUS_BUDGET_MS 约束 */
|
||||
CHECK(d2 < 60); /* 缓存命中:不再 fork */
|
||||
CHECK(strstr(js, "\"a2dp_codec\"") != NULL); /* 字段保留(MeowPanel btCard 依赖) */
|
||||
unlink(p);
|
||||
unlink(snprintf(p, sizeof p, "%s/state/conn_33:DE:2E:21:AD:30", T) ? p : p);
|
||||
snprintf(p, sizeof p, "%s/state/pcms.txt", T); wf(p, "");
|
||||
}
|
||||
|
||||
printf("== 扫描缓存 → bt devices(真机 ANSI 字节 + devices 尾块 + 去重) ==\n");
|
||||
{
|
||||
char cache[4096];
|
||||
@@ -456,27 +566,105 @@ int main(void)
|
||||
}
|
||||
|
||||
/* ================= 7. 写级:on/off + connect/disconnect/pair/unpair ================= */
|
||||
printf("== bt on/off:按 power 真态定稿 ==\n");
|
||||
printf("== bt on/off:按 power 真态定稿 + rfkill 顺序(先 unblock 再 up / 先 down 再 block) ==\n");
|
||||
/* 初始两个 bluetooth rfkill 节点 soft=1(真机形态)。旧实现在这里必败:
|
||||
* 先 hciconfig up 被 rfkill 拦下,再 unblock 也不会重试 → 报「未生效」(假失败)。 */
|
||||
bt_ctl_execute("on", NULL, &r);
|
||||
CHECK(r.code == MEOW_OK);
|
||||
CHECK(strstr(r.text, "已复查") != NULL);
|
||||
CHECK(bt_powered_now() == 1);
|
||||
CHECK(bt_powered_now(1500) == 1);
|
||||
{ /* 两个节点都必须被解 block(只写首个会让另一个仍 block) */
|
||||
int all_unblocked = 1;
|
||||
for (int i = 0; i < 2; i++) {
|
||||
char f[300], v[8] = "1";
|
||||
snprintf(f, sizeof f, "%s/rfkill/rfkill%d/soft", T, i ? 3 : 0);
|
||||
FILE *fp = fopen(f, "r");
|
||||
if (fp) { if (fscanf(fp, "%7s", v) != 1) v[0] = 0; fclose(fp); }
|
||||
if (strcmp(v, "0") != 0) all_unblocked = 0;
|
||||
}
|
||||
CHECK(all_unblocked == 1);
|
||||
}
|
||||
bt_ctl_execute("off", NULL, &r);
|
||||
CHECK(r.code == MEOW_OK);
|
||||
CHECK(bt_powered_now() == 0);
|
||||
CHECK(bt_powered_now(1500) == 0);
|
||||
{ /* 关也要写全部节点 */
|
||||
int all_blocked = 1;
|
||||
for (int i = 0; i < 2; i++) {
|
||||
char f[300], v[8] = "";
|
||||
snprintf(f, sizeof f, "%s/rfkill/rfkill%d/soft", T, i ? 3 : 0);
|
||||
FILE *fp = fopen(f, "r");
|
||||
if (fp) { if (fscanf(fp, "%7s", v) != 1) v[0] = 0; fclose(fp); }
|
||||
if (strcmp(v, "1") != 0) all_blocked = 0;
|
||||
}
|
||||
CHECK(all_blocked == 1);
|
||||
}
|
||||
/* 反向验证:桩确实会在 blocked 时拒绝 up(否则上面的断言是空转) */
|
||||
{ char f[300]; snprintf(f, sizeof f, "%s/rfkill/rfkill0/soft", T); wf(f, "1\n");
|
||||
snprintf(f, sizeof f, "%s/rfkill/rfkill3/soft", T); wf(f, "1\n");
|
||||
char out[128]; snprintf(out, sizeof out, "%s/hciconfig hci0 up 2>&1", sb_hc);
|
||||
runcmd_res_t rr; run_cmd_ex(out, sizeof out, 800, out, &rr);
|
||||
CHECK(rr.status != RUNCMD_OK); }
|
||||
{ char f[300]; snprintf(f, sizeof f, "%s/bt/hci0/power", T); wf(f, "off\n");
|
||||
for (int i = 0; i < 2; i++) { snprintf(f, sizeof f, "%s/rfkill/rfkill%d/soft", T, i ? 3 : 0); wf(f, "0\n"); } }
|
||||
|
||||
printf("== 预算上界:命令卡死时写操作必须落在客户端 1500ms IPC 窗口内(r1 评审 P1) ==\n");
|
||||
{ /* connect:bluetoothctl 卡住 5s(耳机没上电的真实形态)→ 总耗时必须 < 1450ms */
|
||||
long t0 = mono_ms();
|
||||
bt_ctl_execute("connect", "CC:CC:CC:CC:CC:CC", &r);
|
||||
long dt = mono_ms() - t0;
|
||||
printf(" budget-connect = %ld ms (code=%d)\n", dt, r.code);
|
||||
CHECK(dt < 1450);
|
||||
CHECK(r.code == MEOW_ERR_NODEV); /* 未连上就必须报未生效 */
|
||||
CHECK(strstr(r.text, "未生效") != NULL);
|
||||
}
|
||||
{ long t0 = mono_ms();
|
||||
bt_ctl_execute("pair", "CC:CC:CC:CC:CC:CC", &r);
|
||||
long dt = mono_ms() - t0;
|
||||
printf(" budget-pair = %ld ms (code=%d)\n", dt, r.code);
|
||||
CHECK(dt < 1450);
|
||||
CHECK(r.code == MEOW_ERR_NODEV);
|
||||
}
|
||||
{ /* bt on:hciconfig 卡住 5s → 同样必须 < 1450ms(旧实现实测 2424ms) */
|
||||
char f[300]; snprintf(f, sizeof f, "%s/state/hang_hciconfig", T); wf(f, "1\n");
|
||||
long t0 = mono_ms();
|
||||
bt_ctl_execute("on", NULL, &r);
|
||||
long dt = mono_ms() - t0;
|
||||
printf(" budget-bt-on = %ld ms (code=%d)\n", dt, r.code);
|
||||
CHECK(dt < 1450);
|
||||
CHECK(r.code == MEOW_ERR_NODEV); /* 卡住未生效:如实报 */
|
||||
unlink(f);
|
||||
}
|
||||
{ /* 熔断:连续 3 次超时后,第 4 次必须**立刻**拒绝、不再 fork(防 55 个挂死 bluetoothctl
|
||||
* 吃光进程槽的事故)。冷却 30s,测试里手工复位继续后续用例。 */
|
||||
CHECK(g_bt_tmo_streak >= BT_TMO_STREAK_MAX);
|
||||
long t0 = mono_ms();
|
||||
bt_ctl_execute("connect", "CC:CC:CC:CC:CC:CC", &r);
|
||||
long dt = mono_ms() - t0;
|
||||
printf(" circuit-breaker 4th = %ld ms, code=%d\n", dt, r.code);
|
||||
CHECK(dt < 100); /* 没再起子进程 */
|
||||
CHECK(r.code == MEOW_ERR_NODEV);
|
||||
CHECK(strstr(r.text, "熔断中") != NULL);
|
||||
g_bt_tmo_streak = 0; /* 测试专用复位(生产靠 30s 冷却+成功清零) */
|
||||
g_bt_tmo_until = 0;
|
||||
}
|
||||
{ /* 恢复:正常 on,供后面的用例使用;同时验证「非超时返回会清零熔断计数」 */
|
||||
bt_ctl_execute("on", NULL, &r);
|
||||
CHECK(r.code == MEOW_OK);
|
||||
CHECK(g_bt_tmo_streak == 0);
|
||||
}
|
||||
|
||||
printf("== bt connect:成功路径以 hcitool con 复查 ==\n");
|
||||
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, "hcitool con") != NULL);
|
||||
CHECK(bt_acl_has("33:DE:2E:21:AD:30") == 1);
|
||||
CHECK(bt_acl_has_tri("33:DE:2E:21:AD:30", 1500) == 1);
|
||||
|
||||
printf("== bt disconnect:成功路径以 hcitool con 复查 ==\n");
|
||||
bt_ctl_execute("disconnect", "33:DE:2E:21:AD:30", &r);
|
||||
CHECK(r.code == MEOW_OK);
|
||||
CHECK(strstr(r.text, "已断开") != NULL);
|
||||
CHECK(bt_acl_has("33:DE:2E:21:AD:30") == 0);
|
||||
CHECK(bt_acl_has_tri("33:DE:2E:21:AD:30", 1500) == 0);
|
||||
|
||||
printf("== bt connect 失败:bluetoothctl 报错 + 无 ACL → ERR_NODEV(旧实现恒报成功) ==\n");
|
||||
bt_ctl_execute("connect", "AA:AA:AA:AA:AA:AA", &r);
|
||||
@@ -501,11 +689,11 @@ int main(void)
|
||||
bt_ctl_execute("pair", "11:22:33:44:55:66", &r);
|
||||
CHECK(r.code == MEOW_OK);
|
||||
CHECK(strstr(r.text, "已配对") != NULL);
|
||||
CHECK(bt_info_flag("11:22:33:44:55:66", "Paired") == 1);
|
||||
CHECK(bt_info_flag("11:22:33:44:55:66", "Paired", 1500) == 1);
|
||||
bt_ctl_execute("unpair", "11:22:33:44:55:66", &r); /* remove → info 查不到也算解除 */
|
||||
CHECK(r.code == MEOW_OK);
|
||||
CHECK(strstr(r.text, "已解除配对") != NULL);
|
||||
CHECK(bt_info_flag("11:22:33:44:55:66", "Paired") == -1);
|
||||
CHECK(bt_info_flag("11:22:33:44:55:66", "Paired", 1500) == -1);
|
||||
bt_ctl_execute("pair", "AA:AA:AA:AA:AA:AA", &r);
|
||||
CHECK(r.code == MEOW_ERR_NODEV);
|
||||
CHECK(strstr(r.text, "未生效") != NULL);
|
||||
@@ -529,6 +717,7 @@ int main(void)
|
||||
CHECK(bt_write_authorize(&other, 1, "status") == MEOW_OK); /* 读级任意 → 放 */
|
||||
|
||||
(void)cmd;
|
||||
{ char rm[320]; snprintf(rm, sizeof rm, "rm -rf '%s'", T); (void)!system(rm); } /* 退出即清,保证可重复 */
|
||||
if (g_fail) { printf("\nBT_CTL TEST: %d FAIL\n", g_fail); return 1; }
|
||||
printf("\nBT_CTL TEST: ALL PASS\n");
|
||||
return 0;
|
||||
|
||||
@@ -104,16 +104,21 @@ case "${1:-}" in
|
||||
# /org/bluealsa/hci0/dev_XX/a2dpsrc/sink
|
||||
echo "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink";;
|
||||
info)
|
||||
# 真实 info 是按 PCM 路径查属性;桩固定回同一组字段
|
||||
echo "BlueALSA PCM: ${2:-/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc}"
|
||||
echo " Transport: A2DP-source"
|
||||
echo " Codec: SBC"
|
||||
echo " CodecConfiguration: 0x3f"
|
||||
echo " Sampling: 44100 Hz"
|
||||
echo " Channels: 2"
|
||||
echo " Bits per Sample: 16"
|
||||
echo " Running: true"
|
||||
echo " State: RUNNING";;
|
||||
# 真机 v4.1.1 原文(M50 2026-11-11 18:26 抓,PCM 被 audiod 撑住时);此处取「正在传音频」态
|
||||
echo "Device: /org/bluez/hci0/dev_33_DE_2E_21_AD_30"
|
||||
echo "Sequence: 3"
|
||||
echo "Transport: A2DP-source"
|
||||
echo "Mode: sink"
|
||||
echo "Running: true"
|
||||
echo "Format: S16_LE"
|
||||
echo "Channels: 2"
|
||||
echo "Sampling: 48000 Hz"
|
||||
echo "Available codecs: SBC AAC"
|
||||
echo "Selected codec: AAC"
|
||||
echo "Delay: 17.2 ms"
|
||||
echo "SoftVolume: false"
|
||||
echo "Volume: L: 127 R: 127"
|
||||
echo "Muted: L: false R: false";;
|
||||
esac
|
||||
exit 0
|
||||
EOF
|
||||
@@ -191,10 +196,10 @@ ck T5e2 "Device 11:22:33:44:55:66 Speaker" "$OUT"
|
||||
# v4.1.1 没有 `a2dp list` 子命令,桩只认 list-pcms/info;acquired 由 `Running: true` 推出。
|
||||
OUT=$(env $ENVS $Q "$T/meow_net" bt a2dp 2>&1)
|
||||
ck T5f "A2DP 33:DE:2E:21:AD:30" "$OUT"
|
||||
ck T5f2 "codec=SBC" "$OUT"
|
||||
ck T5f3 "rate=44100 Hz" "$OUT"
|
||||
ck T5f2 "codec=AAC" "$OUT" # 真机 Selected codec(不是 Available codecs 列表)
|
||||
ck T5f3 "rate=48000 Hz" "$OUT"
|
||||
ck T5f4 "channels=2" "$OUT"
|
||||
ck T5f5 "bits=16" "$OUT"
|
||||
ck T5f5 "bits=16" "$OUT" # 从 Format: S16_LE 推出
|
||||
ck T5f6 "acquired=1" "$OUT"
|
||||
ck T5f7 "role=src" "$OUT"
|
||||
|
||||
|
||||
Reference in new issue
Block a user