Files
MeowOS-For-R3P2/patch_v2/meow_launcherd/test_bt_page.c
T
AskaEth b20e3c43e9 fix(launcher): M47 设置页蓝牙/WiFi 开关只认真实态 — 三态查询 + 待确认收敛 + 未知态呈现
用户原话:「开关都存在显示问题 我都不能判断它是不是真的开启还是关闭(包括 WiFi 页也是)」。
根因不是「没读到硬件态」,而是「读到/没读到都被拿去当用户意图的装饰」:
- bt_page.c:103 开关初值来自 settings 布尔;:112-114 下发后立刻 g_sys.bt_on = on(显示意图);
  :129 c->bt_on = nm_bt_powered() 赋值后全文无人引用(M45 也独立发现)。
- wifi_page.c:40 读不到 settings 时默认 1(显示已开)。
- netmgrd 的 bt/wifi 查询本身把「问不到」塌缩成 0(nm_bt_powered/nm_wifi_connected)。

本单只解「状态可信」,不碰扫描/配对/音频(另有 M45/M46 单)。

1) 真实态三态(-1 未知 / 0 关 / 1 开)
- net.c 新增 net_bt_power_probe()(hciconfig hci0 UP/DOWN;无输出=未知)与
  net_wifi_power_probe()(rfkill wlan soft/hard 汇总,回退 wlan0 IFF_UP;读不到=未知)。
- netmgr_ipc.c 新增 nm_bt_power_state()(daemon bt status 的 available/powered 权威,不可达
  回退直连探测)与 nm_wifi_power_state()(netmgrd 的 wifi status 只有 wpa_state、不报射频态,
  故取本地 rfkill——daemon 的 wifi on/off 改的就是同一份内核状态)。
- 两个子页的 top_sw/rows 只读这两个函数;settings 布尔降级为「意图持久化」,不再决定外观。
- 「读不到默认 1」删除:未知就是未知。

2) 点击 → 待确认 → 真实态定稿
- 纯函数 meow_toggle_{display,pending,rollback,error}(bt_page.c,无 I/O/UI):
  只有 probe 确认才算收敛;probe 未知继续等,绝不因 last_real 恰好等于意图就宣布成功。
- 页面:落盘意图 → 下发 → 250ms 轮询(BT 上限 4s、WiFi 上限 5s;hciconfig 0.3~2s、
  wifi_up.sh 起 supplicant/DHCP 更慢,故不用 20ms 级忙轮询)→ 真实态定稿;失败/超时把
  settings 回滚到已确认真实态 + s_dd_modal 可见报错,不静默、不乐观。
- subpage.c:切换前先画「正在开启/关闭…(等待设备确认)」条(待确认态与稳定态可区分);
  切换后回读 top_sw 定稿(取消原「点了就翻面」),live tick 也回读,外部改动驻留期间可见。

3) 第三态呈现(未知 ≠ 已关)
- SDK meow_switch 只有两态:未知时开关停「关」并在左侧画灰色「?」;行内另有
  「蓝牙状态未知 / WiFi 开关状态未知」文字说明。选「?」是因为它不占用开关控件本身、
  且「关+?」不会被读成「已关」。

4) 顺带修 net.c 三条开关失真(塔追加的 M45 盘点,同一病根)
- s_net_toggle_bt() 补 rfkill soft(与 netmgrd 的 bt on/off 同一语义;开:先 unblock 再 up;
  关:先 down 再 block)。此前只发 hciconfig → 「BT 页关过再从头开」会被 rfkill 拦下。
- s_net_sync_from_sys() WiFi 判据从 wlan_connected()(已关联)改为射频态;BT 用三态,
  读不到不写(旧写法把「网卡 up 未关联」写成 0,把未知写成已关)。
- rfkill 同类多节点(本机 2 wlan + 2 bluetooth)按「任一 block 即 block」读、全部写入。

测试(`bash patch_v2/meow_launcherd/build_launcher.sh test`,新增 test 入口)
- test_bt_page.c:P4 三态与未知占位、P5 收敛四例(达成/下发失败/超时/未知)、P6 收敛矩阵
  12 组(真实开/关 × 下发成功/失败/超时 × probe 未知),全 PASS(64 条断言)。
- test_netmgr_ipc.c:新增 nm_bt_power_state 解析(powered=1/0、available=0、缺字段→-1)、
  nm_wifi_power_state 透传、不可达回退=未知、wifi_rows 未知态占位;并修掉两处既有测试缺陷
  (行内 UI 桩缺失导致本文件此前根本链不过;直连连接桩统一返回 "0\n" 导致等满 10s 必失败)。
- 交叉编译 0 warning(-Wall -Wextra,逐文件单独编 + 整链);launcherd 899388 B
  md5 1c88661a9410ac1a4139304362281cdd,同 commit 同步 scripts/build/fw_content.conf 钉值。

真机(10.58.215.50,仅只读自检,未替换 /usr/bin/meow_launcherd、未刷机)
- 二进制放 /usr/data/mnt/sd_0/MeowOS/tmp/ 跑 --sysdump:exit 0,输出 wifi_on=1 / bt_seen=1
  bt_on=0,与设备现状一致(rfkill 全 0 + wlan0 up + hci0 DOWN + netmgrd powered=0)。
- 卡上 du -s /usr/data/mnt/sd_0/Music 前后恒 199164544;临时文件已删;settings 未改。
- launcherd 在 squashfs 里 → 本改动需刷机生效。

已知不在本单:输出目标只写 settings(audiod 仅启动读一次)→ M45 finding / M46;
netmgrd 侧四处失真(未知当关、up 与 unblock 顺序、rfkill 只写首节点、wifi status 无射频字段)
已报 finding;设置主页/通知中心的开关显示仍是 settings(另立单)。
2026-11-11 17:16:28 +08:00

358 lines
18 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* test_bt_page.c — MeowOS launcher 蓝牙子页(bt_page.c)sub_page_t 逻辑宿主自测(非生产代码)。
*
* 通过 #include bt_page.c 把其 FILE-STATIC 的 bt_top_sw/bt_rows/bt_act/bt_detail 平移进本 TU,
* 桩掉 netmgr IPC 客户端(nm_bt_*)+ 设置后端(meow_settings_*)+ UI(s_dd_modal/logmsg 等),
* 直接验证:
* P1 bt_rows 行骨架(plan §六 8 分组:②A2DP 摘要 ③输出目标 ④已连接 ⑤已配对 ⑥附近 ⑦音质 ⑧功耗)
* 与「已连接设备」数据(权威 netmgrd 状态 → mac/name/codec 渲染);
* P2 bt_act 命中 → 正确下发 netmgrd 命令(已连接→disconnect、已配对未连→connect、输出目标
* 切换、编解码器循环);
* P3 附近设备仅列未配对(已配对者归「已配对设备」)。
* P4 M47 开关三态:真实态未知(-1) → top_sw 回 -1 + 行内「蓝牙状态未知」(不当已关);
* P5 M47 点击收敛:下发成功且真实态达成 → 定稿、不弹窗;下发失败/超时/未知 → 回滚 + 弹窗;
* P6 M47 收敛矩阵(纯函数 meow_toggle_*,真实态×下发结果×探测 全组合)。
*
* 编译(在 patch_v2/meow_launcherd 下):
* gcc -I../sdk -I. -Wall -Wextra -fsanitize=undefined test_bt_page.c -o test_bt_page
* 运行:./test_bt_page(或 `bash build_launcher.sh test`)
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <strings.h>
#include <unistd.h>
#include <time.h>
/* M47:把收敛等待改成宿主可接受的尺度(生产 4000ms/250ms,见 bt_page.c)。 */
#define BT_TOGGLE_SETTLE_MS 80
#define BT_TOGGLE_POLL_MS 10
#include "meow_launcher_internal.h"
#include "bt_page.c" /* bt_rows/bt_act/bt_top_sw/bt_detail 平移到本 TU */
/* ---------- 测试态:netmgrd BT 状态桩(launcher 客户端视角) ---------- */
static meow_net_t g_conn[8], g_paired[8], g_near[8];
static int g_conn_n, g_paired_n, g_near_n;
static bt_ep_t g_eps[8]; static int g_eps_n;
static char g_last_cmd[128]; /* 记录 act 下发命令,如 "disconnect:<mac>" */
static int g_output_bt; /* audio.output_bt 桩值 */
static char g_codec[16] = "auto"; /* bt.codec 桩值 */
static int g_bt_state = 1; /* M47:真实态三态桩(-1 未知 / 0 / 1) */
static int g_onoff_rc = MEOW_OK; /* M47:nm_bt_onoff 下发结果桩 */
static int g_bt_en_setting = 1; /* M47:network.bluetooth_enabled 桩值(意图/回滚观察点) */
int nm_bt_power_state(void) { return g_bt_state; }
int nm_bt_onoff(int on) { (void)on; return g_onoff_rc; }
int nm_bt_connected(meow_net_t *out, int max) {
if (max > g_conn_n) max = g_conn_n;
memcpy(out, g_conn, (size_t)max * sizeof *out); return max;
}
int nm_bt_paired(meow_net_t *out, int max) {
if (max > g_paired_n) max = g_paired_n;
memcpy(out, g_paired, (size_t)max * sizeof *out); return max;
}
int nm_bt_devices(meow_net_t *out, int max) {
if (max > g_near_n) max = g_near_n;
memcpy(out, g_near, (size_t)max * sizeof *out); return max;
}
int nm_bt_a2dp(bt_ep_t *eps, int max) {
if (max > g_eps_n) max = g_eps_n;
memcpy(eps, g_eps, (size_t)max * sizeof *eps); return max;
}
static void set_cmd(const char *cmd, const char *mac)
{
snprintf(g_last_cmd, sizeof g_last_cmd, "%s:%s", cmd, mac ? mac : "");
}
int nm_bt_connect(const char *mac) { set_cmd("connect", mac); return MEOW_OK; }
int nm_bt_disconnect(const char *mac) { set_cmd("disconnect", mac); return MEOW_OK; }
int nm_bt_pair(const char *mac) { set_cmd("pair", mac); return MEOW_OK; }
int nm_bt_unpair(const char *mac) { set_cmd("unpair", mac); return MEOW_OK; }
void nm_bt_scan_on(void) {}
void nm_bt_scan_off(void) {}
int netmgr_ok(void) { return 1; }
void net_bt_mode_str(char *buf, int len) { snprintf(buf, (size_t)len, "主模式·已连 Pudding"); }
/* ---------- 设置/UI 桩 ---------- */
int meow_settings_get_bool(const char *k, int *v)
{
if (v) *v = 0;
if (!strcmp(k, "network.bluetooth_enabled")) { if (v) *v = g_bt_en_setting; return 0; }
if (!strcmp(k, "audio.output_bt")) { if (v) *v = g_output_bt; return 0; }
return -1;
}
int meow_settings_set_bool(const char *k, int v)
{
if (!strcmp(k, "audio.output_bt")) { g_output_bt = v; return 0; }
if (!strcmp(k, "network.bluetooth_enabled")) { g_bt_en_setting = v; return 0; }
return -1;
}
int meow_settings_get_str(const char *k, char *b, int n)
{
if (!strcmp(k, "bt.codec")) { snprintf(b, (size_t)n, "%s", g_codec); return 0; }
if (b && n > 0) b[0] = 0;
return -1;
}
int meow_settings_set_str(const char *k, const char *v)
{
if (!strcmp(k, "bt.codec")) { snprintf(g_codec, sizeof g_codec, "%s", v); return 0; }
return -1;
}
int meow_settings_save(void) { return 0; }
void paint_sb_right(void) {}
void logmsg(const char *fmt, ...) { (void)fmt; }
void bt_role_refresh(int force) { (void)force; }
meow_sys_state g_sys;
static int g_modal_n = 0; /* M47:可见错误提示(弹窗)计数 */
static char g_modal_line[128] = "";
int s_dd_modal(const char *title, const char *item[S_NET_MAX], int n, int cur)
{
(void)title; (void)cur;
g_modal_n++;
g_modal_line[0] = 0;
if (item && n > 0 && item[0]) snprintf(g_modal_line, sizeof g_modal_line, "%s", item[0]);
return -1;
}
int meow_subpage_run(sub_page_t *p) { (void)p; return 0; }
/* M47:真实时钟(收敛等待用),毫秒单调。 */
unsigned long long now_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (unsigned long long)ts.tv_sec * 1000ULL + (unsigned long long)(ts.tv_nsec / 1000000);
}
static int g_fail = 0;
#define CHECK(cond, ...) do { if (cond) { printf(" PASS: " __VA_ARGS__); printf("\n"); } \
else { printf(" FAIL: " __VA_ARGS__); printf("\n"); g_fail++; } } while (0)
/* 在 rows 里找下标:kind==HDR 且 label==lbl;返回该 HDR 的下一行下标起查找 */
static int find_hdr(const sub_row_t *r, int n, const char *lbl)
{
for (int i = 0; i < n; i++) if (r[i].kind == SUB_HDR && r[i].label && !strcmp(r[i].label, lbl)) return i;
return -1;
}
/* 在 [from,n) 找首个 SUB_ACT 且 label 前缀 == pref,返回下标 */
static int find_act(const sub_row_t *r, int n, int from, const char *pref)
{
for (int i = from; i < n; i++)
if (r[i].kind == SUB_ACT && r[i].label && !strncmp(r[i].label, pref, strlen(pref))) return i;
return -1;
}
/* 按 label 精确找任意行(M47 未知态占位行用) */
static int row_label(const sub_row_t *r, int n, const char *lbl)
{
for (int i = 0; i < n; i++)
if (r[i].label && !strcmp(r[i].label, lbl)) return i;
return -1;
}
static void fill_state(void)
{
/* 已连接:Pudding(a2dp 播放中)+ Speaker */
g_conn_n = 2;
snprintf(g_conn[0].mac, sizeof g_conn[0].mac, "33:DE:2E:21:AD:30");
snprintf(g_conn[0].name, sizeof g_conn[0].name, "Pudding");
snprintf(g_conn[1].mac, sizeof g_conn[1].mac, "11:22:33:44:55:66");
snprintf(g_conn[1].name, sizeof g_conn[1].name, "Speaker");
/* 已配对:+ 17OP15(未连) */
g_paired_n = 3;
memcpy(&g_paired[0], &g_conn[0], sizeof g_conn[0]);
memcpy(&g_paired[1], &g_conn[1], sizeof g_conn[1]);
snprintf(g_paired[2].mac, sizeof g_paired[2].mac, "E0:42:6D:79:C5:98");
snprintf(g_paired[2].name, sizeof g_paired[2].name, "17OP15");
/* 附近:Earbuds(未配对 → 附近列出) */
g_near_n = 2;
snprintf(g_near[0].mac, sizeof g_near[0].mac, "AA:BB:CC:DD:EE:FF");
snprintf(g_near[0].name, sizeof g_near[0].name, "Earbuds");
snprintf(g_near[1].mac, sizeof g_near[1].mac, "33:DE:2E:21:AD:30"); /* 已配对 → 附近应过滤 */
snprintf(g_near[1].name, sizeof g_near[1].name, "Pudding");
/* A2DP:Pudding SBC/44100 acquired */
g_eps_n = 1;
snprintf(g_eps[0].mac, sizeof g_eps[0].mac, "33:DE:2E:21:AD:30");
snprintf(g_eps[0].codec, sizeof g_eps[0].codec, "SBC");
snprintf(g_eps[0].rate, sizeof g_eps[0].rate, "44100");
snprintf(g_eps[0].chn, sizeof g_eps[0].chn, "2");
snprintf(g_eps[0].bits, sizeof g_eps[0].bits, "16");
g_eps[0].acquired = 1;
snprintf(g_eps[0].role, sizeof g_eps[0].role, "src");
g_output_bt = 0; /* DAC */
snprintf(g_codec, sizeof g_codec, "auto");
}
int main(void)
{
setbuf(stdout, NULL);
fill_state();
bt_ctx_t c;
memset(&c, 0, sizeof c);
c.full = 1;
c.output_bt = 0;
sub_row_t rows[SUB_ROWS_MAX];
int n = bt_rows(&c, rows, SUB_ROWS_MAX);
printf("== P1 bt_rows 分组骨架 ==\n");
CHECK(find_hdr(rows, n, "输出目标") >= 0, "分组③输出目标存在");
CHECK(find_hdr(rows, n, "已连接设备") > 0, "分组④已连接设备存在");
CHECK(find_hdr(rows, n, "已配对设备") > 0, "分组⑤已配对设备存在");
CHECK(find_hdr(rows, n, "附近设备") > 0, "分组⑥附近设备存在");
CHECK(find_hdr(rows, n, "音质 / 连接管理") > 0, "分组⑦音质/连接管理存在");
CHECK(find_hdr(rows, n, "功耗") > 0, "分组⑧功耗存在");
/* A2DP 摘要(首行 SUB_ROW,主=Pudding,副含 codec/采样率/输出=DAC) */
CHECK(rows[0].kind == SUB_ROW, "行0=A2DP摘要(SUB_ROW)");
CHECK(rows[0].label && !strcmp(rows[0].label, "Pudding"), "摘要主=Pudding (实际=%s)",
rows[0].label ? rows[0].label : "(null)");
CHECK(rows[0].sub && strstr(rows[0].sub, "SBC") && strstr(rows[0].sub, "44100") &&
strstr(rows[0].sub, "输出=DAC"), "摘要副含 codec/采样率/输出=DAC (%s)",
rows[0].sub ? rows[0].sub : "(null)");
printf("== P1 已连接设备(权威状态,含 codec join)==\n");
int hdr = find_hdr(rows, n, "已连接设备");
CHECK(hdr >= 0, "定位已连接 HDR");
int ci = find_act(rows, n, hdr + 1, "Pudding");
CHECK(ci >= 0, "已连接列出 Pudding");
CHECK(ci >= 0 && rows[ci].sub && strstr(rows[ci].sub, "SBC"), "已连接 Pudding sub 含 codec SBC");
CHECK(find_act(rows, n, hdr + 1, "Speaker") >= 0, "已连接列出 Speaker");
printf("== P1 已配对设备(已连者标「已连接」on=1)==\n");
hdr = find_hdr(rows, n, "已配对设备");
CHECK(hdr >= 0, "定位已配对 HDR");
ci = find_act(rows, n, hdr + 1, "Pudding");
CHECK(ci >= 0 && rows[ci].on == 1 && rows[ci].sub && strstr(rows[ci].sub, "已连接"),
"已配对 Pudding 标已连接(on=1)");
ci = find_act(rows, n, hdr + 1, "17OP15");
CHECK(ci >= 0 && rows[ci].on == 0, "已配对 17OP15 未连(on=0)");
printf("== P3 附近设备仅列未配对(已配对者过滤)==\n");
hdr = find_hdr(rows, n, "附近设备");
CHECK(hdr >= 0, "定位附近 HDR");
ci = find_act(rows, n, hdr + 1, "Earbuds");
CHECK(ci >= 0, "附近列出 Earbuds");
ci = find_act(rows, n, hdr + 1, "Pudding");
CHECK(ci < 0, "已配对 Pudding 不在附近列表");
printf("== P2 bt_act 命中 → netmgrd 命令 ==\n");
/* 已连接 Pudding tap → disconnect */
memset(&c, 0, sizeof c); c.full = 1; fill_state();
n = bt_rows(&c, rows, SUB_ROWS_MAX);
hdr = find_hdr(rows, n, "已连接设备");
ci = find_act(rows, n, hdr + 1, "Pudding");
g_last_cmd[0] = 0;
bt_act(&c, ci, rows[ci].act);
CHECK(strcmp(g_last_cmd, "disconnect:33:DE:2E:21:AD:30") == 0, "已连接 tap → disconnect (实际=%s)", g_last_cmd);
/* 已配对 17OP15(未连)tap → connect */
memset(&c, 0, sizeof c); c.full = 1; fill_state();
n = bt_rows(&c, rows, SUB_ROWS_MAX);
hdr = find_hdr(rows, n, "已配对设备");
ci = find_act(rows, n, hdr + 1, "17OP15");
g_last_cmd[0] = 0;
bt_act(&c, ci, rows[ci].act);
CHECK(strcmp(g_last_cmd, "connect:E0:42:6D:79:C5:98") == 0, "已配对未连 tap → connect (实际=%s)", g_last_cmd);
/* 输出目标 toggle → audio.output_bt 翻转为 1;bt_rows 摘要改输出=蓝牙 */
memset(&c, 0, sizeof c); c.full = 1; fill_state();
n = bt_rows(&c, rows, SUB_ROWS_MAX);
hdr = find_hdr(rows, n, "输出目标");
ci = find_act(rows, n, hdr + 1, "输出目标");
bt_act(&c, ci, rows[ci].act);
CHECK(g_output_bt == 1, "输出目标 tap → output_bt=1 (蓝牙)");
n = bt_rows(&c, rows, SUB_ROWS_MAX);
CHECK(rows[0].sub && strstr(rows[0].sub, "输出=蓝牙"), "切蓝牙后 A2DP 摘要=输出=蓝牙");
/* 编解码器循环:auto → SBC */
memset(&c, 0, sizeof c); c.full = 1; fill_state();
n = bt_rows(&c, rows, SUB_ROWS_MAX);
hdr = find_hdr(rows, n, "音质 / 连接管理");
ci = find_act(rows, n, hdr + 1, "编解码器");
bt_act(&c, ci, rows[ci].act);
CHECK(strcmp(g_codec, "SBC") == 0, "编解码器 auto → SBC (实际=%s)", g_codec);
printf("== P4 M47 开关三态(未知 ≠ 已关)==\n");
int sw = 0;
g_bt_state = -1; /* 真实态未知 */
memset(&c, 0, sizeof c); c.full = 1; fill_state();
CHECK(bt_top_sw(&c, &sw) == 1 && sw == -1, "真实态未知 → top_sw *on=-1 (实际=%d)", sw);
n = bt_rows(&c, rows, SUB_ROWS_MAX);
CHECK(row_label(rows, n, "蓝牙状态未知") >= 0, "未知态 → 行内含「蓝牙状态未知」");
g_bt_state = 0; /* 真实态为关 */
memset(&c, 0, sizeof c); c.full = 1;
sw = -2;
bt_top_sw(&c, &sw);
CHECK(sw == 0, "真实态关 → top_sw *on=0 (实际=%d)", sw);
n = bt_rows(&c, rows, SUB_ROWS_MAX);
CHECK(row_label(rows, n, "蓝牙状态未知") < 0, "真实态已知 → 无未知占位行");
printf("== P5 M47 点击收敛:定稿 / 回滚 + 可见错误 ==\n");
/* 5a 下发成功且真实态已达成 → 立即定稿,无弹窗,settings/状态栏=真实态 */
g_bt_state = 1; g_onoff_rc = MEOW_OK; g_modal_n = 0; g_bt_en_setting = 0;
memset(&c, 0, sizeof c);
unsigned long long t0 = now_ms();
bt_top_sw_set(&c, 1);
unsigned long long dt = now_ms() - t0;
CHECK(g_modal_n == 0, "已达成 → 无错误弹窗 (弹窗数=%d)", g_modal_n);
CHECK(g_bt_en_setting == 1, "已达成 → settings 定稿为 1");
CHECK(g_sys.bt_on == 1, "已达成 → 状态栏 g_sys.bt_on=1");
CHECK(dt < BT_TOGGLE_SETTLE_MS, "已达成 → 不必等满超时 (%llums)", dt);
/* 5b 下发失败、真实态仍为开(要求关)→ 回滚到开 + 可见弹窗 */
g_bt_state = 1; g_onoff_rc = MEOW_ERR_NODEV; g_modal_n = 0; g_bt_en_setting = 1;
memset(&c, 0, sizeof c);
bt_top_sw_set(&c, 0);
CHECK(g_modal_n == 1, "下发失败 → 有可见错误弹窗");
CHECK(g_bt_en_setting == 1, "下发失败 → settings 回滚为真实态 1 (实际=%d)", g_bt_en_setting);
CHECK(g_sys.bt_on == 1, "下发失败 → 状态栏仍为真实态 1");
/* 5c 下发成功但真实态不收敛(要求开、真实一直关)→ 等满上限后回滚 + 弹窗 */
g_bt_state = 0; g_onoff_rc = MEOW_OK; g_modal_n = 0; g_bt_en_setting = 1;
memset(&c, 0, sizeof c);
t0 = now_ms();
bt_top_sw_set(&c, 1);
dt = now_ms() - t0;
CHECK(g_modal_n == 1, "超时未收敛 → 有可见错误弹窗(%s)", g_modal_line);
CHECK(g_bt_en_setting == 0, "超时未收敛 → settings 回滚为真实态 0");
CHECK(g_sys.bt_on == 0, "超时未收敛 → 状态栏=真实态 0");
CHECK(dt >= BT_TOGGLE_SETTLE_MS, "确实等满了上限 (%llums)", dt);
/* 5d 真实态未知 → 等满上限后仍是未知 + 弹窗(绝不把未知显示成成功) */
g_bt_state = -1; g_onoff_rc = MEOW_OK; g_modal_n = 0; g_bt_en_setting = 1;
memset(&c, 0, sizeof c);
bt_top_sw_set(&c, 1);
CHECK(g_modal_n == 1 && strstr(g_modal_line, "无法确认") != NULL,
"未知 → 弹窗明说无法确认 (%s)", g_modal_line);
CHECK(c.bt_on == -1, "未知 → 定稿态仍为 -1 (实际=%d)", c.bt_on);
CHECK(g_bt_en_setting == 0, "未知 → settings 不留意图值 1(回滚为 0)");
printf("== P6 M47 收敛矩阵(真实态 × 下发结果 × 探测 × 超时)==\n");
{
static const struct {
int last, want, ok, probe, to; /* 输入 */
int disp, pend, rb, err; /* 期望输出 */
const char *what;
} m[] = {
{ 1, 1, 1, 1, 0, 1, 0, 0, 0, "真开·下发成功·探测开" },
{ 1, 0, 1, 0, 0, 0, 0, 0, 0, "真开·下发成功·探测关" },
{ 1, 0, 1, 1, 1, 1, 0, 1, 1, "真开·下发成功·未收敛(超时)" },
{ 1, 0, 0, 1, 0, 1, 0, 1, 1, "真开·下发失败·探测仍开" },
{ 1, 0, 0, 0, 0, 0, 0, 0, 0, "真开·下发失败但探测已关(不误报)" },
{ 0, 1, 1, 1, 0, 1, 0, 0, 0, "真关·下发成功·探测开" },
{ 0, 1, 1, 0, 1, 0, 0, 1, 1, "真关·下发成功·未收敛(超时)" },
{ 0, 1, 0, 0, 0, 0, 0, 1, 1, "真关·下发失败·仍关" },
{ 1, 0, 1, -1, 0, 1, 1, 1, 0, "真开·下发成功·探测未知(继续等)" },
{ 1, 0, 1, -1, 1, 1, 0, 1, 1, "真开·下发成功·探测未知(超时)" },
{-1, 1, 1, -1, 1, -1, 0, 1, 1, "未知·下发成功·探测仍未知(超时)" },
{-1, 1, 1, -1, 0, -1, 1, 1, 0, "未知·下发成功·探测仍未知(继续等)" },
};
for (size_t i = 0; i < sizeof m / sizeof m[0]; i++) {
int d = meow_toggle_display(m[i].want, m[i].last, m[i].ok, m[i].probe, m[i].to);
int p = meow_toggle_pending(m[i].want, m[i].last, m[i].ok, m[i].probe, m[i].to);
int r = meow_toggle_rollback(m[i].want, m[i].last, m[i].ok, m[i].probe, m[i].to);
int e = meow_toggle_error(m[i].want, m[i].last, m[i].ok, m[i].probe, m[i].to);
CHECK(d == m[i].disp && p == m[i].pend && r == m[i].rb && e == m[i].err,
"%s → disp=%d pend=%d rb=%d err=%d (期望 %d/%d/%d/%d)",
m[i].what, d, p, r, e, m[i].disp, m[i].pend, m[i].rb, m[i].err);
}
}
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
return g_fail;
}