Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0da68e4972 | ||
|
|
eb07415f7b | ||
|
|
8c677f127f | ||
|
|
2e5b814059 | ||
|
|
7ca3e5bd84 |
No files matched your search
@@ -1,46 +1,98 @@
|
|||||||
/* bt_page.c — MeowOS 系统设置「蓝牙独立子页面」(安卓风,M2.76)。
|
/* bt_page.c — MeowOS 系统设置「蓝牙独立子页面」(安卓风,M2.76;P1 权威收口重做)。
|
||||||
*
|
*
|
||||||
* 页面(sub_page_t 脚手架 subpage.c 驱动):
|
* 【P1 背景】BLUETOOTH_FULL_OVERHAUL_PLAN §六/§八:连接/断开/配对/取消配对/扫描/A2DP 状态
|
||||||
* - 顶栏:返回 + 标题「蓝牙」 + 右上开关(network.bluetooth_enabled)
|
* 查询全部收口进 netmgrd(BT_CTL 消息扩展),本子页**只做客户端**——经 netmgrd socket/IPC 查询
|
||||||
* - 已连接设备:tap → bluetoothctl disconnect(角色经 bt_role_refresh 判定)
|
* 与下发,不再 bluetoothctl popen 直连(直连仅作 netmgrd 不可达时的回退保留)。
|
||||||
* - 已配对设备:tap → bluetoothctl connect(已连者标「已连接」)
|
|
||||||
* - 附近设备:扫描发现(net_bt_scan,后台有界 bluetoothctl 写 /tmp 缓存,异步不阻塞)——
|
|
||||||
* 未配对者 tap → pair;已配对者 tap → connect。实机配对成功率/超时需真机验证。
|
|
||||||
*
|
*
|
||||||
* 与网络线共用 net.c(net_bt_connected/paired/scan/scan_trigger/hci_powered/bt_role)。
|
* 【已连接设备显示不全根因修复】旧实现用 `bluetoothctl devices Connected`(慢 + 只反映 agent
|
||||||
* 开关写 network.bluetooth_enabled + hciconfig;连接/断开/配对走 bluetoothctl(带超时)。
|
* 缓存视图,popen 1500ms 易截断)→ 已连接列表不全。P1 改权威 = netmgrd 读**内核 ACL**
|
||||||
|
* (hcitool con,快速/权威/不受 bluetoothctl 过滤影响),名经 paired/devices 缓存兜底,A2DP 详情
|
||||||
|
* 按 MAC join(codec/采样率/acquired/role)。见 netmgrd `bt connected`/`bt a2dp`。
|
||||||
|
*
|
||||||
|
* 【页面结构(sub_page_t,plan §六)】顶栏「蓝牙」+ 右上开关,分组列表:
|
||||||
|
* ① 蓝牙开关(顶栏)② A2DP 状态摘要(设备名·codec·采样率·输出=蓝牙/DAC)③ 输出目标
|
||||||
|
* (蓝牙/本地,切下发 audio.output_bt,P2 接 audiod)④ 已连接设备(tap 断开/详情)⑤ 已配对
|
||||||
|
* 设备(tap 连接/断开)⑥ 附近设备(扫描节流触发)⑦ 音质/连接管理(codec 选择、P2 参数说明)
|
||||||
|
* ⑧ 功耗档(扫描节流/发现超时/后台保持说明)。
|
||||||
|
*
|
||||||
|
* 【扫描节流(§七)】进页 nm_bt_scan_on、退页/超时 nm_bt_scan_off;7s 节流由 netmgrd 统一控。
|
||||||
|
*
|
||||||
|
* 与网络线共用 net.c(net_bt_* 直连回退 + bt_role 角色判定)。开关写 network.bluetooth_enabled +
|
||||||
|
* netmgrd on/off;连接/断开/配对经 netmgrd(netmgr_ipc.c nm_bt_* 封装)。
|
||||||
*/
|
*/
|
||||||
#include "meow_launcher_internal.h"
|
#include "meow_launcher_internal.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <strings.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
#define BT_ACT_CONN_BASE 2000 /* 已连接 idx(tap→disconnect) */
|
#define BT_ACT_CONN_BASE 2000 /* 已连接 idx(tap→断开) */
|
||||||
#define BT_ACT_PAIRED_BASE 3000 /* 已配对 idx(tap→connect) */
|
#define BT_ACT_PAIRED_BASE 3000 /* 已配对 idx(tap→连接/断开) */
|
||||||
#define BT_ACT_NEAR_BASE 4000 /* 附近 idx(tap→pair/connect) */
|
#define BT_ACT_NEAR_BASE 4000 /* 附近 idx(tap→配对/连接) */
|
||||||
|
#define BT_ACT_OUTPUT 5000 /* 输出目标(tap→蓝牙/本地切换) */
|
||||||
|
#define BT_ACT_CODEC 5001 /* 编解码器(tap→循环选择) */
|
||||||
|
|
||||||
|
#define BT_CODEC_AUTO 0
|
||||||
|
#define BT_CODEC_N 5 /* auto/SBC/AAC/aptX/LDAC */
|
||||||
|
static const char *bt_codec_names[BT_CODEC_N] = { "auto", "SBC", "AAC", "aptX", "LDAC" };
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
meow_net_t conn[S_NET_MAX];
|
meow_net_t conn[S_NET_MAX];
|
||||||
meow_net_t paired[S_NET_MAX];
|
meow_net_t paired[S_NET_MAX];
|
||||||
meow_net_t near[S_NET_MAX];
|
meow_net_t near[S_NET_MAX];
|
||||||
int conn_n, paired_n, near_n;
|
bt_ep_t a2dp[S_NET_MAX];
|
||||||
|
int conn_n, paired_n, near_n, a2dp_n;
|
||||||
int bt_on;
|
int bt_on;
|
||||||
int full; /* 1=本次 rows() 全量重扫(conn/paired popen);0=仅附近(file)+角色(节流) */
|
int output_bt; /* 0=DAC/本地,1=蓝牙 */
|
||||||
|
int full; /* 1=本次 rows() 全量重扫(conn/paired/a2dp);0=仅附近(缓存)+角色(节流) */
|
||||||
char mode[48]; /* bt_role 中文描述(次行摘要) */
|
char mode[48]; /* bt_role 中文描述(次行摘要) */
|
||||||
|
/* 行副文本池:sub_page_t 行 label/sub 为 const char*,跨 rows() 返回后仍须有效(直到下次
|
||||||
|
* rows() 重填)。本地栈缓冲会悬垂(agent-396 根因同类),故格式化 sub 一律写进本池。 */
|
||||||
|
char pool[512];
|
||||||
|
int pool_off;
|
||||||
} bt_ctx_t;
|
} bt_ctx_t;
|
||||||
|
|
||||||
|
|
||||||
/* 设备 MAC(net_bt_* 已填 mac) */
|
/* 格式化并拷入 ctx 行副文本池(返回稳定指针;溢出则绕回——512B 远超单页行数所需)。 */
|
||||||
|
static char *bt_pool(bt_ctx_t *c, const char *fmt, ...)
|
||||||
|
{
|
||||||
|
va_list ap; char tmp[128];
|
||||||
|
va_start(ap, fmt); vsnprintf(tmp, sizeof tmp, fmt, ap); va_end(ap);
|
||||||
|
int l = (int)strlen(tmp);
|
||||||
|
if (c->pool_off + l + 1 > (int)sizeof c->pool) c->pool_off = 0;
|
||||||
|
char *p = &c->pool[c->pool_off];
|
||||||
|
memcpy(p, tmp, (size_t)l + 1);
|
||||||
|
c->pool_off += l + 1;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ---- 设备 MAC 相关 ---- */
|
||||||
static int bt_is_conn(const bt_ctx_t *c, const char *mac)
|
static int bt_is_conn(const bt_ctx_t *c, const char *mac)
|
||||||
{
|
{
|
||||||
if (!mac[0]) return 0;
|
if (!mac[0]) return 0;
|
||||||
for (int i = 0; i < c->conn_n; i++) if (!strcmp(c->conn[i].mac, mac)) return 1;
|
for (int i = 0; i < c->conn_n; i++) if (!strcmp(c->conn[i].mac, mac)) return 1;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int bt_is_paired(const bt_ctx_t *c, const char *mac)
|
static int bt_is_paired(const bt_ctx_t *c, const char *mac)
|
||||||
{
|
{
|
||||||
if (!mac[0]) return 0;
|
if (!mac[0]) return 0;
|
||||||
for (int i = 0; i < c->paired_n; i++) if (!strcmp(c->paired[i].mac, mac)) return 1;
|
for (int i = 0; i < c->paired_n; i++) if (!strcmp(c->paired[i].mac, mac)) return 1;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
/* 在 a2dp 端点表找 MAC(返回 idx;-1=无) */
|
||||||
|
static int bt_ep_idx(const bt_ctx_t *c, const char *mac)
|
||||||
|
{
|
||||||
|
if (!mac[0]) return -1;
|
||||||
|
for (int i = 0; i < c->a2dp_n; i++) if (!strcmp(c->a2dp[i].mac, mac)) return i;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
/* 从 conn 表找设备名(返回指针到 c->conn[i].name,未命中返回 mac) */
|
||||||
|
static const char *bt_conn_name(const bt_ctx_t *c, const char *mac)
|
||||||
|
{
|
||||||
|
if (mac[0]) for (int i = 0; i < c->conn_n; i++) if (!strcmp(c->conn[i].mac, mac)) return c->conn[i].name;
|
||||||
|
return mac;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 顶栏开关当前值 */
|
/* 顶栏开关当前值 */
|
||||||
@@ -53,7 +105,7 @@ static int bt_top_sw(void *ctx, int *on)
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 顶栏开关切换:写设置 + on/off(netmgr IPC,回退直连 hciconfig)+ 触发扫描 + 同步状态栏 */
|
/* 顶栏开关切换:写设置 + on/off(netmgrd IPC,回退直连 hciconfig)+ 触发扫描 + 同步状态栏 */
|
||||||
static void bt_top_sw_set(void *ctx, int on)
|
static void bt_top_sw_set(void *ctx, int on)
|
||||||
{
|
{
|
||||||
(void)ctx;
|
(void)ctx;
|
||||||
@@ -63,48 +115,91 @@ static void bt_top_sw_set(void *ctx, int on)
|
|||||||
bt_role_refresh(1);
|
bt_role_refresh(1);
|
||||||
paint_sb_right();
|
paint_sb_right();
|
||||||
meow_settings_save();
|
meow_settings_save();
|
||||||
if (on) net_bt_scan_trigger();
|
if (on) nm_bt_scan_on();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 填行:刷新数据后构建分组列表。
|
/* 填行:刷新数据后构建分组列表(权威=netmgrd;仅全量(full)时重扫 conn/paired/a2dp,
|
||||||
* conn/paired 走 bluetoothctl(慢);仅全量(full)时重扫,避开子页 live tick 反复 spawn。 */
|
* 避开子页 live tick 反复 IPC)。 */
|
||||||
static int bt_rows(void *ctx, sub_row_t *rbuf, int max)
|
static int bt_rows(void *ctx, sub_row_t *rbuf, int max)
|
||||||
{
|
{
|
||||||
bt_ctx_t *c = ctx;
|
bt_ctx_t *c = ctx;
|
||||||
int full = c->full; c->full = 0; /* 消费一次全量标记 */
|
int full = c->full; c->full = 0; /* 消费一次全量标记 */
|
||||||
|
c->pool_off = 0; /* 行副文本池绕回(上轮行字符串已被重填替换) */
|
||||||
c->bt_on = nm_bt_powered(); /* IPC status;回退直连 hci_powered() */
|
c->bt_on = nm_bt_powered(); /* IPC status;回退直连 hci_powered() */
|
||||||
if (full || c->conn_n < 0) { /* 首次/动作后:全量重扫 conn/paired */
|
c->output_bt = 0;
|
||||||
/* conn 列表 netmgrd 无子命令(BT_CTL=status/on/off/paired),保持直连 bluetoothctl;
|
if (meow_settings_get_bool("audio.output_bt", &c->output_bt) != 0) c->output_bt = 0;
|
||||||
* paired 走 IPC(权威),回退直连。 */
|
if (full || c->conn_n < 0) { /* 首次/动作后:全量重扫 conn/paired/a2dp(全权威) */
|
||||||
c->conn_n = net_bt_connected(c->conn, S_NET_MAX);
|
c->conn_n = nm_bt_connected(c->conn, S_NET_MAX);
|
||||||
c->paired_n = nm_bt_paired(c->paired, S_NET_MAX);
|
c->paired_n = nm_bt_paired(c->paired, S_NET_MAX);
|
||||||
|
c->a2dp_n = nm_bt_a2dp(c->a2dp, S_NET_MAX);
|
||||||
if (c->conn_n < 0) c->conn_n = 0;
|
if (c->conn_n < 0) c->conn_n = 0;
|
||||||
if (c->paired_n < 0) c->paired_n = 0;
|
if (c->paired_n < 0) c->paired_n = 0;
|
||||||
|
if (c->a2dp_n < 0) c->a2dp_n = 0;
|
||||||
}
|
}
|
||||||
c->near_n = net_bt_scan(c->near, S_NET_MAX); /* 附近:读 /tmp 缓存(快,实测无 popen) */
|
c->near_n = nm_bt_devices(c->near, S_NET_MAX); /* 附近:扫描缓存(netmgrd 读 /tmp,快) */
|
||||||
if (c->near_n < 0) c->near_n = 0;
|
if (c->near_n < 0) c->near_n = 0;
|
||||||
/* 角色描述:节流(≤5s)重探,读缓存,避免子页驻留期间反复 popen */
|
/* 角色描述:节流(≤5s)重探,读缓存,避免子页驻留期间反复 popen */
|
||||||
net_bt_mode_str(c->mode, sizeof c->mode);
|
net_bt_mode_str(c->mode, sizeof c->mode);
|
||||||
|
|
||||||
int n = 0;
|
int n = 0;
|
||||||
/* 降级占位:netmgrd 不可达 → 提示(直连仍在跑,保功能) */
|
/* 降级占位:netmgrd 不可达 → 提示(直连回退仍在跑,保功能) */
|
||||||
if (!netmgr_ok() && n < max) {
|
if (!netmgr_ok() && n < max) {
|
||||||
rbuf[n].kind = SUB_HDR; rbuf[n].label = "网络管理服务未运行(直连模式)";
|
rbuf[n].kind = SUB_HDR; rbuf[n].label = "网络管理服务未运行(直连模式)";
|
||||||
rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++;
|
rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++;
|
||||||
}
|
}
|
||||||
/* 已连接设备 */
|
|
||||||
|
/* ② A2DP 状态摘要(设备名·codec·采样率·输出=蓝牙/DAC) */
|
||||||
|
{
|
||||||
|
const bt_ep_t *ep = NULL;
|
||||||
|
for (int i = 0; i < c->a2dp_n; i++) if (c->a2dp[i].acquired) { ep = &c->a2dp[i]; break; }
|
||||||
|
if (!ep && c->a2dp_n > 0) ep = &c->a2dp[0];
|
||||||
|
const char *sub;
|
||||||
|
if (c->conn_n > 0 && n < max) {
|
||||||
|
const char *nm = bt_conn_name(c, ep ? ep->mac : c->conn[0].mac);
|
||||||
|
if (ep && (ep->codec[0] || ep->rate[0]))
|
||||||
|
sub = bt_pool(c, "%s%s%s · 输出=%s",
|
||||||
|
ep->codec[0] ? ep->codec : "?", ep->codec[0] && ep->rate[0] ? " " : "",
|
||||||
|
ep->rate[0] ? ep->rate : "", c->output_bt ? "蓝牙" : "DAC");
|
||||||
|
else
|
||||||
|
sub = bt_pool(c, "%s · 输出=%s", c->mode, c->output_bt ? "蓝牙" : "DAC");
|
||||||
|
rbuf[n].kind = SUB_ROW; rbuf[n].label = nm ? nm : c->conn[0].mac;
|
||||||
|
rbuf[n].sub = sub; rbuf[n].on = 0; rbuf[n].act = -1; n++;
|
||||||
|
} else if (n < max) {
|
||||||
|
sub = bt_pool(c, "%s · 输出=%s", c->mode, c->output_bt ? "蓝牙" : "DAC");
|
||||||
|
rbuf[n].kind = SUB_ROW; rbuf[n].label = "未连接蓝牙设备";
|
||||||
|
rbuf[n].sub = sub; rbuf[n].on = 0; rbuf[n].act = -1; n++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ③ 输出目标(蓝牙/本地) */
|
||||||
|
if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "输出目标"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; }
|
||||||
|
if (n < max) {
|
||||||
|
rbuf[n].kind = SUB_ACT; rbuf[n].label = "输出目标";
|
||||||
|
rbuf[n].sub = c->output_bt ? "蓝牙(耳机/远端)" : "本地(DAC)";
|
||||||
|
rbuf[n].on = c->output_bt; rbuf[n].act = BT_ACT_OUTPUT; n++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ④ 已连接设备 */
|
||||||
if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "已连接设备"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; }
|
if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "已连接设备"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; }
|
||||||
if (c->conn_n == 0) {
|
if (c->conn_n == 0) {
|
||||||
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "无已连接设备"; rbuf[n].sub = ""; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
|
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "无已连接设备"; rbuf[n].sub = ""; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
|
||||||
} else for (int i = 0; i < c->conn_n && n < max; i++) {
|
} else for (int i = 0; i < c->conn_n && n < max; i++) {
|
||||||
|
const bt_ep_t *ep = bt_ep_idx(c, c->conn[i].mac) >= 0 ? &c->a2dp[bt_ep_idx(c, c->conn[i].mac)] : NULL;
|
||||||
|
const char *sub;
|
||||||
|
if (ep && ep->codec[0])
|
||||||
|
sub = bt_pool(c, "%s%s%s%s",
|
||||||
|
ep->codec[0] ? ep->codec : "", ep->rate[0] ? " " : "", ep->rate[0] ? "· " : "",
|
||||||
|
ep->acquired ? "播放中" : "");
|
||||||
|
else sub = c->conn[i].mac; /* mac 拷在 ctx(稳定) */
|
||||||
rbuf[n].kind = SUB_ACT;
|
rbuf[n].kind = SUB_ACT;
|
||||||
rbuf[n].label = c->conn[i].name[0] ? c->conn[i].name : c->conn[i].mac;
|
rbuf[n].label = c->conn[i].name[0] ? c->conn[i].name : c->conn[i].mac;
|
||||||
rbuf[n].sub = c->conn[i].mac;
|
rbuf[n].sub = sub[0] ? sub : c->conn[i].mac;
|
||||||
rbuf[n].on = 1; rbuf[n].act = BT_ACT_CONN_BASE + i;
|
rbuf[n].on = 1; rbuf[n].act = BT_ACT_CONN_BASE + i;
|
||||||
n++;
|
n++;
|
||||||
}
|
}
|
||||||
/* 已配对设备 */
|
|
||||||
|
/* ⑤ 已配对设备 */
|
||||||
if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "已配对设备"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; }
|
if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "已配对设备"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; }
|
||||||
if (c->paired_n == 0) {
|
if (c->paired_n == 0) {
|
||||||
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "无已配对设备"; rbuf[n].sub = ""; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
|
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "无已配对设备"; rbuf[n].sub = ""; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
|
||||||
@@ -117,7 +212,8 @@ static int bt_rows(void *ctx, sub_row_t *rbuf, int max)
|
|||||||
rbuf[n].act = BT_ACT_PAIRED_BASE + i;
|
rbuf[n].act = BT_ACT_PAIRED_BASE + i;
|
||||||
n++;
|
n++;
|
||||||
}
|
}
|
||||||
/* 附近设备(扫描发现,未配对) */
|
|
||||||
|
/* ⑥ 附近设备(扫描发现,未配对;netmgrd 权威扫描缓存) */
|
||||||
if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "附近设备"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; }
|
if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "附近设备"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; }
|
||||||
if (c->near_n == 0) {
|
if (c->near_n == 0) {
|
||||||
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "附近无设备(点顶部开关后自动扫描)"; rbuf[n].sub = ""; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
|
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "附近无设备(点顶部开关后自动扫描)"; rbuf[n].sub = ""; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
|
||||||
@@ -130,30 +226,66 @@ static int bt_rows(void *ctx, sub_row_t *rbuf, int max)
|
|||||||
rbuf[n].on = 0; rbuf[n].act = BT_ACT_NEAR_BASE + i;
|
rbuf[n].on = 0; rbuf[n].act = BT_ACT_NEAR_BASE + i;
|
||||||
n++;
|
n++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ⑦ 音质/连接管理 */
|
||||||
|
if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "音质 / 连接管理"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; }
|
||||||
|
{
|
||||||
|
int idx = BT_CODEC_AUTO;
|
||||||
|
char codec[16] = "auto";
|
||||||
|
if (meow_settings_get_str("bt.codec", codec, sizeof codec) == 0)
|
||||||
|
for (int k = 0; k < BT_CODEC_N; k++) if (!strcasecmp(codec, bt_codec_names[k])) { idx = k; break; }
|
||||||
|
rbuf[n].kind = SUB_ACT; rbuf[n].label = "编解码器";
|
||||||
|
rbuf[n].sub = bt_pool(c, "%s%s", bt_codec_names[idx], idx ? "(偏好,P2 生效)" : "");
|
||||||
|
rbuf[n].on = 0;
|
||||||
|
rbuf[n].act = BT_ACT_CODEC; n++;
|
||||||
|
}
|
||||||
|
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "SBC 质量"; rbuf[n].sub = "P2:bluealsa --sbc-quality 启动参数"; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
|
||||||
|
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "强制 44.1k"; rbuf[n].sub = "P2:--a2dp-force-audio-cd"; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
|
||||||
|
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "音量控制"; rbuf[n].sub = "蓝牙音量由耳机/远端(AVRCP)控制"; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
|
||||||
|
|
||||||
|
/* ⑧ 功耗档 */
|
||||||
|
if (n < max) { rbuf[n].kind = SUB_HDR; rbuf[n].label = "功耗"; rbuf[n].sub = NULL; rbuf[n].on = 0; rbuf[n].act = 0; n++; }
|
||||||
|
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "扫描节流"; rbuf[n].sub = "进页自动扫描,退页/7s 停止"; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
|
||||||
|
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "发现超时"; rbuf[n].sub = "source 无需可发现(有限超时)"; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
|
||||||
|
if (n < max) { rbuf[n].kind = SUB_ROW; rbuf[n].label = "后台保持"; rbuf[n].sub = "未播放时无流零开销"; rbuf[n].on = 0; rbuf[n].act = -2; n++; }
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 执行 bluetoothctl 命令(带超时)+ 结果弹窗反馈 */
|
/* 动作反馈(netmgrd 结果 / 直连回退结果)弹窗反馈 */
|
||||||
static void bt_run(const char *title, const char *cmd)
|
static void bt_feedback(const char *title, int rc)
|
||||||
{
|
{
|
||||||
char *o = popen_read_tmo(8000, cmd);
|
|
||||||
char line[48];
|
char line[48];
|
||||||
const char *txt[4]; txt[0] = line;
|
const char *txt[S_NET_MAX]; txt[0] = line;
|
||||||
int ok = o && (strstr(o, "successful") || strstr(o, "Connected") ||
|
snprintf(line, sizeof line, "%s", rc == MEOW_OK ? "操作成功" : "操作失败/超时");
|
||||||
strstr(o, "already") || strstr(o, "Paired"));
|
|
||||||
snprintf(line, sizeof line, "%s", ok ? "操作成功" : "操作失败/超时");
|
|
||||||
if (o) free(o);
|
|
||||||
s_dd_modal(title, txt, 1, -1);
|
s_dd_modal(title, txt, 1, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 动作行(SUB_ACT)点击:连接/断开/配对 */
|
/* 动作行(SUB_ACT)点击:连接/断开/配对/取消配对/输出目标/编解码器 */
|
||||||
static int bt_act(void *ctx, int idx, int act)
|
static int bt_act(void *ctx, int idx, int act)
|
||||||
{
|
{
|
||||||
(void)idx;
|
(void)idx;
|
||||||
bt_ctx_t *c = ctx;
|
bt_ctx_t *c = ctx;
|
||||||
char mac[24] = "";
|
char mac[24] = "";
|
||||||
|
/* 输出目标/编解码器:与本页设备行 act 段(CONN/PAIRED/NEAR base)不冲突,先处理 */
|
||||||
|
if (act == BT_ACT_OUTPUT) {
|
||||||
|
c->output_bt = !c->output_bt;
|
||||||
|
meow_settings_set_bool("audio.output_bt", c->output_bt);
|
||||||
|
meow_settings_save();
|
||||||
|
logmsg("bt_page: output_target=%s", c->output_bt ? "bt" : "dac");
|
||||||
|
return 1; /* 重绘 + 重填行 */
|
||||||
|
}
|
||||||
|
if (act == BT_ACT_CODEC) {
|
||||||
|
char codec[16] = "auto"; int idx0 = BT_CODEC_AUTO;
|
||||||
|
if (meow_settings_get_str("bt.codec", codec, sizeof codec) == 0)
|
||||||
|
for (int k = 0; k < BT_CODEC_N; k++) if (!strcasecmp(codec, bt_codec_names[k])) { idx0 = k; break; }
|
||||||
|
int ni = (idx0 + 1) % BT_CODEC_N;
|
||||||
|
meow_settings_set_str("bt.codec", bt_codec_names[ni]);
|
||||||
|
meow_settings_save();
|
||||||
|
logmsg("bt_page: codec=%s", bt_codec_names[ni]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
/* 定位 MAC(按 act 段) */
|
/* 定位 MAC(按 act 段) */
|
||||||
if (act >= BT_ACT_NEAR_BASE) {
|
if (act >= BT_ACT_NEAR_BASE) {
|
||||||
int i = act - BT_ACT_NEAR_BASE;
|
int i = act - BT_ACT_NEAR_BASE;
|
||||||
@@ -168,43 +300,75 @@ static int bt_act(void *ctx, int idx, int act)
|
|||||||
if (i < 0 || i >= c->conn_n) return 0;
|
if (i < 0 || i >= c->conn_n) return 0;
|
||||||
snprintf(mac, sizeof mac, "%s", c->conn[i].mac);
|
snprintf(mac, sizeof mac, "%s", c->conn[i].mac);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mac[0]) return 0;
|
if (!mac[0]) return 0;
|
||||||
|
|
||||||
if (act >= BT_ACT_NEAR_BASE) {
|
if (act >= BT_ACT_NEAR_BASE) {
|
||||||
/* 附近未配对 → pair;已配对(理论上过滤了)→ connect */
|
/* 附近:未配对 → pair;已配对(理论上过滤了)→ connect */
|
||||||
char cmd[192];
|
bt_feedback("配对", nm_bt_pair(mac));
|
||||||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin bluetoothctl pair %s 2>/dev/null", mac);
|
|
||||||
bt_run("配对", cmd);
|
|
||||||
} else if (act >= BT_ACT_PAIRED_BASE) {
|
} else if (act >= BT_ACT_PAIRED_BASE) {
|
||||||
/* 已配对:未连 → connect;已连 → disconnect */
|
/* 已配对:未连 → connect;已连 → disconnect */
|
||||||
if (bt_is_conn(c, mac)) {
|
if (bt_is_conn(c, mac)) bt_feedback("断开", nm_bt_disconnect(mac));
|
||||||
char cmd[192];
|
else bt_feedback("连接", nm_bt_connect(mac));
|
||||||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin bluetoothctl disconnect %s 2>/dev/null", mac);
|
|
||||||
bt_run("断开", cmd);
|
|
||||||
} else {
|
|
||||||
char cmd[192];
|
|
||||||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin bluetoothctl connect %s 2>/dev/null", mac);
|
|
||||||
bt_run("连接", cmd);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
/* 已连接 → disconnect */
|
/* 已连接 → disconnect */
|
||||||
char cmd[192];
|
bt_feedback("断开", nm_bt_disconnect(mac));
|
||||||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin bluetoothctl disconnect %s 2>/dev/null", mac);
|
|
||||||
bt_run("断开", cmd);
|
|
||||||
}
|
}
|
||||||
bt_role_refresh(1);
|
bt_role_refresh(1);
|
||||||
net_bt_scan_trigger();
|
nm_bt_scan_on();
|
||||||
c->full = 1; /* 动作后:下一次 rows() 全量重扫 */
|
c->full = 1; /* 动作后:下一次 rows() 全量重扫 */
|
||||||
logmsg("bt_page: act act=%d mac=%s", act, mac);
|
logmsg("bt_page: act act=%d mac=%s", act, mac);
|
||||||
return 1; /* 重绘 + 重填行 */
|
return 1; /* 重绘 + 重填行 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 点普通行(A2DP 摘要等):弹 A2DP/设备详情。仅 A2DP 摘要(act=-1)响应;其余 info 行(act=-2)无操作。 */
|
||||||
|
static int bt_detail(void *ctx, int idx, int act)
|
||||||
|
{
|
||||||
|
(void)idx;
|
||||||
|
if (act != -1) return 0;
|
||||||
|
bt_ctx_t *c = ctx;
|
||||||
|
if (c->a2dp_n > 0) {
|
||||||
|
const bt_ep_t *ep = &c->a2dp[0];
|
||||||
|
char buf[S_NET_MAX][48];
|
||||||
|
const char *item[S_NET_MAX];
|
||||||
|
int n = 0;
|
||||||
|
for (int i = 0; i < c->a2dp_n; i++) if (c->a2dp[i].acquired) { ep = &c->a2dp[i]; break; }
|
||||||
|
snprintf(buf[n], 48, "设备名: %s", bt_conn_name(c, ep->mac)); item[n] = buf[n]; n++;
|
||||||
|
snprintf(buf[n], 48, "MAC: %s", ep->mac[0] ? ep->mac : "-"); item[n] = buf[n]; n++;
|
||||||
|
snprintf(buf[n], 48, "codec: %s", ep->codec[0] ? ep->codec : "-"); item[n] = buf[n]; n++;
|
||||||
|
snprintf(buf[n], 48, "采样率: %s", ep->rate[0] ? ep->rate : "-"); item[n] = buf[n]; n++;
|
||||||
|
snprintf(buf[n], 48, "声道/位深: %s/%s", ep->chn[0] ? ep->chn : "-", ep->bits[0] ? ep->bits : "-");
|
||||||
|
item[n] = buf[n]; n++;
|
||||||
|
snprintf(buf[n], 48, "传输: %s", ep->acquired ? "播放中" : "空闲"); item[n] = buf[n]; n++;
|
||||||
|
snprintf(buf[n], 48, "角色: %s", !strcmp(ep->role, "src") ? "设备=source(送流)" :
|
||||||
|
!strcmp(ep->role, "snk") ? "设备=sink(被连)" : "未知");
|
||||||
|
item[n] = buf[n]; n++;
|
||||||
|
s_dd_modal("A2DP 详情", item, n, -1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (c->conn_n > 0) {
|
||||||
|
char buf[S_NET_MAX][48];
|
||||||
|
const char *item[S_NET_MAX];
|
||||||
|
int n = 0;
|
||||||
|
snprintf(buf[n], 48, "设备名: %s", c->conn[0].name[0] ? c->conn[0].name : c->conn[0].mac);
|
||||||
|
item[n] = buf[n]; n++;
|
||||||
|
snprintf(buf[n], 48, "MAC: %s", c->conn[0].mac); item[n] = buf[n]; n++;
|
||||||
|
snprintf(buf[n], 48, "状态: 已连接"); item[n] = buf[n]; n++;
|
||||||
|
snprintf(buf[n], 48, "输出: %s", c->output_bt ? "蓝牙" : "DAC"); item[n] = buf[n]; n++;
|
||||||
|
s_dd_modal("蓝牙详情", item, n, -1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 运行蓝牙独立子页面(阻塞到返回) */
|
/* 运行蓝牙独立子页面(阻塞到返回) */
|
||||||
int run_bt_page(void)
|
int run_bt_page(void)
|
||||||
{
|
{
|
||||||
static bt_ctx_t c;
|
static bt_ctx_t c;
|
||||||
memset(&c, 0, sizeof c);
|
memset(&c, 0, sizeof c);
|
||||||
|
c.conn_n = -1;
|
||||||
sub_page_t p;
|
sub_page_t p;
|
||||||
memset(&p, 0, sizeof p);
|
memset(&p, 0, sizeof p);
|
||||||
p.title = "蓝牙";
|
p.title = "蓝牙";
|
||||||
@@ -212,11 +376,13 @@ int run_bt_page(void)
|
|||||||
p.top_sw_set = bt_top_sw_set;
|
p.top_sw_set = bt_top_sw_set;
|
||||||
p.rows = bt_rows;
|
p.rows = bt_rows;
|
||||||
p.act = bt_act;
|
p.act = bt_act;
|
||||||
p.detail = NULL; /* 蓝牙动作都走 SUB_ACT(act),无 SUB_ROW 详情 */
|
p.detail = bt_detail; /* SUB_ROW(A2DP 摘要等)→ A2DP 详情弹窗 */
|
||||||
p.live = 1; /* 附近扫描驻留更新(约 2s 重扫行;仅附近/角色,不反复 spawn) */
|
p.live = 1; /* 附近扫描驻留更新(约 2s 重扫行;仅附近/角色,不反复 spawn) */
|
||||||
p.ctx = &c;
|
p.ctx = &c;
|
||||||
c.full = 1; /* 首行全量重扫 */
|
c.full = 1; /* 首行全量重扫 */
|
||||||
net_bt_scan_trigger(); /* 进页立即触发一次附近扫描(异步,不阻塞) */
|
nm_bt_scan_on(); /* 进页:触发一次附近扫描(节流;不进蓝牙不空转 §七) */
|
||||||
bt_role_refresh(1);
|
bt_role_refresh(1);
|
||||||
return meow_subpage_run(&p);
|
int rc = meow_subpage_run(&p);
|
||||||
|
nm_bt_scan_off(); /* 退页:扫描收尾(§七 省电) */
|
||||||
|
return rc;
|
||||||
}
|
}
|
||||||
@@ -29,6 +29,7 @@ SYSROOT="$ROOT/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf"
|
|||||||
font_theme.c keys.c util.c meow_dep.c \
|
font_theme.c keys.c util.c meow_dep.c \
|
||||||
meow_touch.c meow_settings.c meow_notif.c meow_media.c meow_bgplay.c \
|
meow_touch.c meow_settings.c meow_notif.c meow_media.c meow_bgplay.c \
|
||||||
meow_ams_start.c lock_screen.c lock_screen_ui.c lock_settings.c \
|
meow_ams_start.c lock_screen.c lock_screen_ui.c lock_settings.c \
|
||||||
|
"../sdk/meow_lyric.c" "../sdk/meow_lyric_view.c" \
|
||||||
"../sdk/meow_ui.a" "$SYSROOT/lib/libz.a" -lpthread -o meow_launcherd
|
"../sdk/meow_ui.a" "$SYSROOT/lib/libz.a" -lpthread -o meow_launcherd
|
||||||
echo "== meow_launcherd =="
|
echo "== meow_launcherd =="
|
||||||
ls -la meow_launcherd
|
ls -la meow_launcherd
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ enum {
|
|||||||
MSG_PICKER_RESULT = 0x0071, /* ↑ picker 返回值(候选结果回 A;→launcher→amsd) */
|
MSG_PICKER_RESULT = 0x0071, /* ↑ picker 返回值(候选结果回 A;→launcher→amsd) */
|
||||||
MSG_META = 0x0072, /* ↑ 媒体会话上报元数据/进度/状态(阶段 D 附录 B) */
|
MSG_META = 0x0072, /* ↑ 媒体会话上报元数据/进度/状态(阶段 D 附录 B) */
|
||||||
MSG_PLAYCTL = 0x0073, /* ↓ 播放控制下发/↑ 控制器转发到最近活跃媒体会话 */
|
MSG_PLAYCTL = 0x0073, /* ↓ 播放控制下发/↑ 控制器转发到最近活跃媒体会话 */
|
||||||
|
MSG_META_LYRIC = 0x0074, /* ↑ 媒体会话上报当前曲歌词 lrc/tlrc(阶段 D · 批3,分片) */
|
||||||
/* ---- amsd 控制(AMSD_DESIGN §4.2):amsd→launcher,申请拉起/终止某 App ----
|
/* ---- amsd 控制(AMSD_DESIGN §4.2):amsd→launcher,申请拉起/终止某 App ----
|
||||||
* 语义:amsd 为「启动/终止」决策者 → launcher(执行器)执行。payload 见下。 */
|
* 语义:amsd 为「启动/终止」决策者 → launcher(执行器)执行。payload 见下。 */
|
||||||
MSG_AMS_START = 0x00A0, /* → 请求 launcher 拉起某 App(pkg + 可选 activity) */
|
MSG_AMS_START = 0x00A0, /* → 请求 launcher 拉起某 App(pkg + 可选 activity) */
|
||||||
@@ -274,6 +275,29 @@ typedef struct m_playctl {
|
|||||||
int64_t seek_ms; /* cmd=5: 目标位置 ms;否则 0 */
|
int64_t seek_ms; /* cmd=5: 目标位置 ms;否则 0 */
|
||||||
} m_playctl_t; /* 4 (+4 pad) + 8 = 16 B(mipsel int64 8 对齐) */
|
} m_playctl_t; /* 4 (+4 pad) + 8 = 16 B(mipsel int64 8 对齐) */
|
||||||
|
|
||||||
|
/* ---- 媒体会话歌词上报(阶段 D · 批3:live lyric 数据源框架)----
|
||||||
|
* App 播放时把当前曲歌词(lrc 原文 + tlrc 翻译)上报到 meow_media 会话,供桌面歌词页/播放页
|
||||||
|
* 随播放位置实时滚动。约束:launcher 环缓冲 `payload[MEOW_IPC_CARGO(1024)]` 为单帧入队上限,
|
||||||
|
* 故歌词按分片传输(每片 ≤ MEOW_LYRIC_CHUNK 字节)。
|
||||||
|
* - seq = 歌曲代际(换歌/换词自增;launcher 见新 seq 清旧词缓存,同曲分片共用)
|
||||||
|
* - kind = 0 原文 lrc / 1 翻译 tlrc
|
||||||
|
* - total= 该 kind 总字节数(不含 NUL;0=清空该 kind)
|
||||||
|
* - off/len = 本片起始偏移 / 有效字节(首片 off=0;分片按序发送)
|
||||||
|
* 边界:lrc 上限 MEOW_MEDIA_LRC_MAX / tlrc 上限 MEOW_MEDIA_TLRC_MAX(会话内每 App 一份缓冲,
|
||||||
|
* 超限截断)。payload 定长且 == MEOW_IPC_CARGO(即 sizeof(m_lyric_t)=1024)。 */
|
||||||
|
#define MEOW_MEDIA_LRC_MAX 3584u /* 会话内歌词原文缓冲上限 */
|
||||||
|
#define MEOW_MEDIA_TLRC_MAX 1568u /* 会话内歌词翻译缓冲上限 */
|
||||||
|
#define MEOW_LYRIC_CHUNK (MEOW_IPC_CARGO - 44u) /* 单分片数据上限(44=头:32+4+2+2+2+2) */
|
||||||
|
typedef struct m_lyric {
|
||||||
|
char app_id[MEOW_APP_ID_MAX]; /* 32: 上报源头(与 HELLO/META 的 app_id 一致) */
|
||||||
|
uint32_t seq; /* 歌曲代际(新歌/新词自增;分片同 seq) */
|
||||||
|
uint16_t kind; /* 0=原文 lrc;1=翻译 tlrc */
|
||||||
|
uint16_t total; /* 该 kind 总字节数(不含 NUL;0=清空该 kind) */
|
||||||
|
uint16_t off; /* 本片起始偏移(≤ total;首片=0) */
|
||||||
|
uint16_t len; /* 本片有效字节(≤ MEOW_LYRIC_CHUNK) */
|
||||||
|
char data[MEOW_LYRIC_CHUNK]; /* 本片内容(len 后 NUL 结尾,接收侧再截断) */
|
||||||
|
} m_lyric_t; /* 44 + 980 = 1024 B = MEOW_IPC_CARGO */
|
||||||
|
|
||||||
/* MSG_NOTIFY ↑:推送/更新通知(freedesktop 对齐 + MeowOS 扩展;定长 LE)。
|
/* MSG_NOTIFY ↑:推送/更新通知(freedesktop 对齐 + MeowOS 扩展;定长 LE)。
|
||||||
* - summary/body/app_icon/expire_timeout 直通 freedesktop 字段;app_icon 即 SDK `icon`
|
* - summary/body/app_icon/expire_timeout 直通 freedesktop 字段;app_icon 即 SDK `icon`
|
||||||
* (icon name 或 path;映射为空串时 launcher 用该 app 默认图标回退);
|
* (icon name 或 path;映射为空串时 launcher 用该 app 默认图标回退);
|
||||||
|
|||||||
@@ -821,11 +821,32 @@ int nm_wifi_connect(const char *ssid, const char *psk, char *err, int elen); /*
|
|||||||
int nm_wifi_disconnect(char *err, int elen);
|
int nm_wifi_disconnect(char *err, int elen);
|
||||||
int nm_wifi_toggle(int on);
|
int nm_wifi_toggle(int on);
|
||||||
|
|
||||||
/* BT(IPC 优先:on/off/status/paired;连接/断开/配对/附近扫描 netmgrd 无子命令,仍直连) */
|
/* BT(IPC 优先:全量子命令权威收口 P1;netmgrd 不可达回退 net.c 直连)。
|
||||||
|
* P1 收口(BLUETOOTH_FULL_OVERHAUL_PLAN §六/§八):connect/disconnect/pair/unpair/scan 写级
|
||||||
|
* 迁 netmgrd;connected/devices/a2dp 读级也迁 netmgrd(launcher 只做客户端)。 */
|
||||||
int nm_bt_powered(void);
|
int nm_bt_powered(void);
|
||||||
int nm_bt_onoff(int on);
|
int nm_bt_onoff(int on);
|
||||||
int nm_bt_status(int *powered, int *paired_n, char *addr, int alen);
|
int nm_bt_status(int *powered, int *paired_n, char *addr, int alen);
|
||||||
int nm_bt_paired(meow_net_t *out, int max);
|
int nm_bt_paired(meow_net_t *out, int max);
|
||||||
|
int nm_bt_connected(meow_net_t *out, int max);
|
||||||
|
int nm_bt_devices(meow_net_t *out, int max);
|
||||||
|
/* A2DP 单端点详情(codec/采样率/声道/位深/acquired/role;netmgrd bt a2dp 解析) */
|
||||||
|
typedef struct {
|
||||||
|
char mac[24]; /* 端点设备 MAC(与 connected/paired 列表可 join) */
|
||||||
|
char codec[16]; /* SBC/AAC/aptX/LDAC...;空=未知 */
|
||||||
|
char rate[16]; /* 采样率(如 "44100");空=未知 */
|
||||||
|
char chn[8]; /* 声道数 */
|
||||||
|
char bits[8]; /* 位深 */
|
||||||
|
int acquired; /* 1=传输已获取(streaming 中) */
|
||||||
|
char role[8]; /* "src"/"snk"/"-"(设备侧 A2DP 角色) */
|
||||||
|
} bt_ep_t;
|
||||||
|
int nm_bt_a2dp(bt_ep_t *eps, int max); /* 返回端点数 */
|
||||||
|
int nm_bt_connect(const char *mac);
|
||||||
|
int nm_bt_disconnect(const char *mac);
|
||||||
|
int nm_bt_pair(const char *mac);
|
||||||
|
int nm_bt_unpair(const char *mac);
|
||||||
|
void nm_bt_scan_on(void);
|
||||||
|
void nm_bt_scan_off(void);
|
||||||
const meow_ui_theme_t *s_ui_theme(void);
|
const meow_ui_theme_t *s_ui_theme(void);
|
||||||
void sb_blit_icon(int icon_idx, int x, uint32_t fg, int ink_h);
|
void sb_blit_icon(int icon_idx, int x, uint32_t fg, int ink_h);
|
||||||
int scan_apps(struct app apps[MAX_APPS]);
|
int scan_apps(struct app apps[MAX_APPS]);
|
||||||
|
|||||||
@@ -197,13 +197,74 @@ uint32_t meow_media_display_pos(const meow_media_session_t *s)
|
|||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 把一帧歌词分片写入会话缓冲(kind=0 lrc / kind=1 tlrc)。含分片边界截断(不越界)。
|
||||||
|
* 返回 0=写入;-1=参数非法。 */
|
||||||
|
static int lyric_put(meow_media_session_t *s, const m_lyric_t *l)
|
||||||
|
{
|
||||||
|
if (!s || !l) return -1;
|
||||||
|
char *buf;
|
||||||
|
uint32_t cap;
|
||||||
|
uint32_t *lenp;
|
||||||
|
if (l->kind == 0) { buf = s->lyric_lrc; cap = MEOW_MEDIA_LRC_MAX; lenp = &s->lyric_lrc_len; }
|
||||||
|
else if (l->kind == 1) { buf = s->lyric_tlrc; cap = MEOW_MEDIA_TLRC_MAX; lenp = &s->lyric_tlrc_len; }
|
||||||
|
else return -1;
|
||||||
|
uint32_t off = l->off, n = l->len;
|
||||||
|
if (off >= cap) { off = cap; n = 0; } /* 起始越界:整片丢弃 */
|
||||||
|
if (n > cap - off) n = cap - off; /* 末段越界:截断到缓冲上限 */
|
||||||
|
if (n) memcpy(buf + off, l->data, n);
|
||||||
|
buf[off + n] = 0; /* 恒 NUL 结尾(供字符串读取) */
|
||||||
|
uint32_t end = off + n;
|
||||||
|
if (end > *lenp) *lenp = end;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int meow_media_lyric(const m_lyric_t *l)
|
||||||
|
{
|
||||||
|
if (!l || !l->app_id[0]) return -1;
|
||||||
|
meow_media_session_t *s = sess_find(l->app_id);
|
||||||
|
if (!s) {
|
||||||
|
/* 会话须先经 META 注册(否则 fd=0,PLAYCTL 会误发到 0 号描述符);丢弃。 */
|
||||||
|
media_log("media: lyric drop app=%s (no session; report META first)", l->app_id);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
/* 新代际(换歌/换词):清旧词缓冲,重开分片写入。 */
|
||||||
|
if (l->seq != s->lyric_seq) {
|
||||||
|
s->lyric_seq = l->seq;
|
||||||
|
s->lyric_lrc_len = 0;
|
||||||
|
s->lyric_tlrc_len = 0;
|
||||||
|
s->lyric_lrc[0] = 0;
|
||||||
|
s->lyric_tlrc[0] = 0;
|
||||||
|
}
|
||||||
|
lyric_put(s, l);
|
||||||
|
media_log("media: LYRIC app=%s seq=%u kind=%u total=%u off=%u len=%u lrc=%u tlrc=%u",
|
||||||
|
s->app_id, l->seq, l->kind, l->total, l->off, l->len,
|
||||||
|
s->lyric_lrc_len, s->lyric_tlrc_len);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int meow_media_lyric_valid(const meow_media_session_t *s)
|
||||||
|
{
|
||||||
|
return (s && s->used && s->lyric_lrc_len > 0) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *meow_media_lyric_lrc(const meow_media_session_t *s)
|
||||||
|
{
|
||||||
|
return (s && s->used) ? s->lyric_lrc : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *meow_media_lyric_tlrc(const meow_media_session_t *s)
|
||||||
|
{
|
||||||
|
return (s && s->used) ? s->lyric_tlrc : "";
|
||||||
|
}
|
||||||
|
|
||||||
void meow_media_dump(const char *prefix)
|
void meow_media_dump(const char *prefix)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < MEOW_MEDIA_MAX; i++) {
|
for (int i = 0; i < MEOW_MEDIA_MAX; i++) {
|
||||||
if (!g_sess[i].used) continue;
|
if (!g_sess[i].used) continue;
|
||||||
media_log("%s sess%d app=%s state=%u fd=%d last=%llu pos=%u/%u title=\"%s\"",
|
media_log("%s sess%d app=%s state=%u fd=%d last=%llu pos=%u/%u title=\"%s\" lyric=%u",
|
||||||
prefix ? prefix : "media", i, g_sess[i].app_id, g_sess[i].state,
|
prefix ? prefix : "media", i, g_sess[i].app_id, g_sess[i].state,
|
||||||
g_sess[i].fd, (unsigned long long)g_sess[i].last_active_ms,
|
g_sess[i].fd, (unsigned long long)g_sess[i].last_active_ms,
|
||||||
g_sess[i].pos_ms, g_sess[i].dur_ms, g_sess[i].title);
|
g_sess[i].pos_ms, g_sess[i].dur_ms, g_sess[i].title,
|
||||||
|
(unsigned)(g_sess[i].lyric_lrc_len > 0 ? 1 : 0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,15 @@ typedef struct meow_media_session {
|
|||||||
char artist[128];
|
char artist[128];
|
||||||
char album[128];
|
char album[128];
|
||||||
char artwork[192];
|
char artwork[192];
|
||||||
char lyric[192];
|
char lyric[192]; /* 歌词源路径/槽位(沿用 META.lyric) */
|
||||||
|
/* 批3:当前曲 live 歌词(App 经 MSG_META_LYRIC 上报;有界缓冲,新 seq 重置)。
|
||||||
|
* lyric_lrc/lyric_tlrc 为当前曲歌词原文/翻译文本(桌面歌词页 meow_lyric_parse 用);
|
||||||
|
* lyric_seq=歌曲代际(App 新歌/新词自增,launcher 见新 seq 清旧词)。 */
|
||||||
|
uint32_t lyric_seq;
|
||||||
|
uint32_t lyric_lrc_len;
|
||||||
|
uint32_t lyric_tlrc_len;
|
||||||
|
char lyric_lrc[MEOW_MEDIA_LRC_MAX];
|
||||||
|
char lyric_tlrc[MEOW_MEDIA_TLRC_MAX];
|
||||||
} meow_media_session_t;
|
} meow_media_session_t;
|
||||||
|
|
||||||
/* 依赖注入:send=发一帧(launcher 用 ipc_send 包装;宿主单测用 socket 写);
|
/* 依赖注入:send=发一帧(launcher 用 ipc_send 包装;宿主单测用 socket 写);
|
||||||
@@ -91,6 +99,17 @@ void meow_media_tick(void);
|
|||||||
/* 显示用虚拟位置:播放中按墙钟推进 pos_ms(clamp 到 dur);暂停/缓冲/停止原样。 */
|
/* 显示用虚拟位置:播放中按墙钟推进 pos_ms(clamp 到 dur);暂停/缓冲/停止原样。 */
|
||||||
uint32_t meow_media_display_pos(const meow_media_session_t *s);
|
uint32_t meow_media_display_pos(const meow_media_session_t *s);
|
||||||
|
|
||||||
|
/* ---- 歌词(批3 · live lyric 数据源)----
|
||||||
|
* App 上报当前曲歌词(MSG_META_LYRIC 分片):按 app_id 归并到会话,seq 为歌曲代际、
|
||||||
|
* 新代际清旧词;分片按 kind(0=原文/1=翻译)/off 写入会话内有序缓冲(超 MEOW_MEDIA_LRC_MAX/
|
||||||
|
* MEOW_MEDIA_TLRC_MAX 截断)。会话不存在(未先报 META 注册)→ 丢弃返回 -1(防 fd=0 槽)。
|
||||||
|
* meow_media_lyric_valid/lrc/tlrc:桌面歌词页从当前会话取歌词文本,交由批1 meow_lyric_parse
|
||||||
|
* 解析 + meow_lyric_line_at(meow_media_display_pos(s)) 驱动批2 meow_lyric_view 实时滚动。 */
|
||||||
|
int meow_media_lyric(const m_lyric_t *l); /* 0=已写入;-1=会话不存在/参数非法 */
|
||||||
|
int meow_media_lyric_valid(const meow_media_session_t *s); /* 会话携带有效歌词(lrc 非空) */
|
||||||
|
const char *meow_media_lyric_lrc(const meow_media_session_t *s); /* 原文;无则 "" */
|
||||||
|
const char *meow_media_lyric_tlrc(const meow_media_session_t *s); /* 翻译;无则 "" */
|
||||||
|
|
||||||
/* 调试打印(经 ops->log,前缀+每会话一行)。 */
|
/* 调试打印(经 ops->log,前缀+每会话一行)。 */
|
||||||
void meow_media_dump(const char *prefix);
|
void meow_media_dump(const char *prefix);
|
||||||
|
|
||||||
|
|||||||
@@ -140,6 +140,12 @@ static const meow_setting_def g_defs[] = {
|
|||||||
{ "audio.volume_max", SET_INT, "100", 0, 100, NULL, NULL },
|
{ "audio.volume_max", SET_INT, "100", 0, 100, NULL, NULL },
|
||||||
{ "audio.volume_step", SET_INT, "1", 1, 5, NULL, NULL },
|
{ "audio.volume_step", SET_INT, "1", 1, 5, NULL, NULL },
|
||||||
{ "audio.output", SET_STR, "hw:0,0", 0,0, NULL, NULL },
|
{ "audio.output", SET_STR, "hw:0,0", 0,0, NULL, NULL },
|
||||||
|
/* P1 蓝牙 P1:输出目标偏好(0=DAC/本地,1=蓝牙 OUT_BT)。audiod 侧 target 切换在 P2
|
||||||
|
* 实装;P1 仅作为「输出目标」行/摘要的持久化键(蓝牙子页写,DAC 侧不受影响)。 */
|
||||||
|
{ "audio.output_bt", SET_BOOL, "0", 0, 0, NULL, NULL },
|
||||||
|
/* P1 蓝牙:codec 偏好(auto/SBC/AAC/aptX/LDAC;P2 接 alsa.conf pcm.bt/bluealsa 生效)。
|
||||||
|
* 当前实际协商 codec 由 a2dp 状态实时读,本键只是页面「编解码器」选项的持久化。 */
|
||||||
|
{ "bt.codec", SET_STR, "auto", 0, 0, NULL, NULL },
|
||||||
/* 网络(M2.39 设置页 WiFi/蓝牙开关;wifi 默认开=允许 ensure_wifi 自动连网,
|
/* 网络(M2.39 设置页 WiFi/蓝牙开关;wifi 默认开=允许 ensure_wifi 自动连网,
|
||||||
* 用户关=0 则 ensure_wifi 不再自动拉起 / 蓝牙默认关=少耗电) */
|
* 用户关=0 则 ensure_wifi 不再自动拉起 / 蓝牙默认关=少耗电) */
|
||||||
{ "network.wifi_enabled", SET_BOOL, "1", 0, 0, NULL, NULL },
|
{ "network.wifi_enabled", SET_BOOL, "1", 0, 0, NULL, NULL },
|
||||||
|
|||||||
@@ -151,6 +151,15 @@ static void nm_decode_ssid(const char *src, char *dst, int dlen)
|
|||||||
dst[o] = 0;
|
dst[o] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* P1:拷一个「空格分隔的值 token」到 dst(a2dp 字段 extraction 用)。 */
|
||||||
|
static void nm_val_field(char **p, char *dst, int dlen)
|
||||||
|
{
|
||||||
|
while (**p == ' ' || **p == '\t') (*p)++;
|
||||||
|
int o = 0;
|
||||||
|
while (**p && **p != ' ' && **p != '\t' && **p != '\n' && o < dlen - 1) dst[o++] = *(*p)++;
|
||||||
|
dst[o] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* —— netmgrd `wifi status` 单行摘要解析:`wifi state=X connected=N ssid=S ip=I rssi=R`。
|
/* —— netmgrd `wifi status` 单行摘要解析:`wifi state=X connected=N ssid=S ip=I rssi=R`。
|
||||||
* 已知字段以「空格+key=」分隔;ssid 可能含空格,需读到下一个已知字段为止。 —— */
|
* 已知字段以「空格+key=」分隔;ssid 可能含空格,需读到下一个已知字段为止。 —— */
|
||||||
static int nm_is_status_key(const char *p)
|
static int nm_is_status_key(const char *p)
|
||||||
@@ -432,7 +441,9 @@ int nm_wifi_toggle(int on)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ================= BT(IPC 优先:on/off/status/paired;其余子命令 netmgrd 无,直连)================= */
|
/* ================= BT(IPC 优先:全量子命令权威收口 P1;不可达回退直连)================= */
|
||||||
|
|
||||||
|
static int nm_parse_devices(const char *txt, meow_net_t *out, int max); /* 前向声明 */
|
||||||
|
|
||||||
int nm_bt_powered(void)
|
int nm_bt_powered(void)
|
||||||
{
|
{
|
||||||
@@ -478,28 +489,142 @@ int nm_bt_paired(meow_net_t *out, int max)
|
|||||||
int rc = netmgr_bt("paired", NULL, text, sizeof text);
|
int rc = netmgr_bt("paired", NULL, text, sizeof text);
|
||||||
if (rc == NETMGR_UNREACH) return net_bt_paired(out, max);
|
if (rc == NETMGR_UNREACH) return net_bt_paired(out, max);
|
||||||
if (rc != MEOW_OK) return 0;
|
if (rc != MEOW_OK) return 0;
|
||||||
|
return nm_parse_devices(text, out, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- P1 收口:连接/断开/配对/扫描/A2DP 全部迁 netmgrd(权威),不可达回退直连 ---- */
|
||||||
|
|
||||||
|
/* 通用 `Device <MAC> <Name>` 行解析(netmgrd bt {connected,paired,devices} 文本;launcher 只做客户端)。 */
|
||||||
|
static int nm_parse_devices(const char *txt, meow_net_t *out, int max)
|
||||||
|
{
|
||||||
int n = 0;
|
int n = 0;
|
||||||
|
if (!txt || max <= 0) return 0;
|
||||||
if (max > S_NET_MAX) max = S_NET_MAX;
|
if (max > S_NET_MAX) max = S_NET_MAX;
|
||||||
if (text[0]) {
|
char *line = (char *)txt;
|
||||||
char *line = text;
|
while (line && *line && n < max) {
|
||||||
while (line && *line && n < max) {
|
char *eol = strchr(line, '\n'); if (eol) *eol = 0;
|
||||||
char *eol = strchr(line, '\n');
|
char *p = strstr(line, "Device ");
|
||||||
if (eol) *eol = 0;
|
if (p) {
|
||||||
char *p = strstr(line, "Device ");
|
p += 7;
|
||||||
if (p) {
|
char *mac = p; while (*mac && *mac != ' ' && *mac != '\t') mac++;
|
||||||
p += 7;
|
char *nm2 = mac; while (*nm2 == ' ' || *nm2 == '\t') nm2++;
|
||||||
char *mac = p; while (*mac && *mac != ' ' && *mac != '\t') mac++;
|
int ml = (int)(mac - p); if (ml > 47) ml = 47;
|
||||||
char *nm2 = mac; while (*nm2 == ' ' || *nm2 == '\t') nm2++;
|
if (ml > 0 && ml < (int)sizeof out[n].mac) {
|
||||||
int ml = (int)(mac - p); if (ml > 47) ml = 47;
|
memcpy(out[n].mac, p, (size_t)ml); out[n].mac[ml] = 0;
|
||||||
if (ml > 0 && ml < (int)sizeof out[n].mac) {
|
|
||||||
memcpy(out[n].mac, p, (size_t)ml); out[n].mac[ml] = 0;
|
|
||||||
}
|
|
||||||
snprintf(out[n].name, sizeof out[n].name, "%s", *nm2 ? nm2 : p);
|
|
||||||
out[n].id = -1; out[n].sec[0] = 0;
|
|
||||||
n++;
|
|
||||||
}
|
}
|
||||||
line = eol ? eol + 1 : NULL;
|
snprintf(out[n].name, sizeof out[n].name, "%s", *nm2 ? nm2 : p);
|
||||||
|
out[n].id = -1; out[n].sec[0] = 0;
|
||||||
|
n++;
|
||||||
}
|
}
|
||||||
|
line = eol ? eol + 1 : (char *)0;
|
||||||
}
|
}
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int nm_bt_connected(meow_net_t *out, int max)
|
||||||
|
{
|
||||||
|
char text[MEOW_IPC_MAX - 32];
|
||||||
|
int rc = netmgr_bt("connected", NULL, text, sizeof text);
|
||||||
|
if (rc == NETMGR_UNREACH) return net_bt_connected(out, max); /* 直连回退 */
|
||||||
|
if (rc != MEOW_OK) return 0;
|
||||||
|
return nm_parse_devices(text, out, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
int nm_bt_devices(meow_net_t *out, int max)
|
||||||
|
{
|
||||||
|
char text[MEOW_IPC_MAX - 32];
|
||||||
|
int rc = netmgr_bt("devices", NULL, text, sizeof text);
|
||||||
|
if (rc == NETMGR_UNREACH) return net_bt_scan(out, max); /* 直连回退(读扫描缓存) */
|
||||||
|
if (rc != MEOW_OK) return 0;
|
||||||
|
return nm_parse_devices(text, out, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A2DP 端点解析:"A2DP <mac> codec=.. rate=.. channels=.. bits=.. acquired=.. role=.." */
|
||||||
|
int nm_bt_a2dp(bt_ep_t *eps, int max)
|
||||||
|
{
|
||||||
|
if (max <= 0) return 0;
|
||||||
|
char text[MEOW_IPC_MAX - 32];
|
||||||
|
int rc = netmgr_bt("a2dp", NULL, text, sizeof text);
|
||||||
|
if (rc == NETMGR_UNREACH || rc != MEOW_OK) return 0; /* A2DP 详情无直连回退(P2 bluealsa 才接) */
|
||||||
|
int n = 0;
|
||||||
|
char *line = text;
|
||||||
|
while (line && *line && n < max) {
|
||||||
|
char *eol = strchr(line, '\n'); if (eol) *eol = 0;
|
||||||
|
if (!strncmp(line, "A2DP ", 5)) {
|
||||||
|
char *p = line + 5;
|
||||||
|
bt_ep_t *e = &eps[n];
|
||||||
|
memset(e, 0, sizeof *e);
|
||||||
|
snprintf(e->role, sizeof e->role, "%s", "-");
|
||||||
|
e->acquired = 0;
|
||||||
|
char *mac = p; while (*mac && *mac != ' ' && *mac != '\t' && *mac != '\n') mac++;
|
||||||
|
int ml = (int)(mac - p); if (ml > 23) ml = 23;
|
||||||
|
if (ml > 0) { memcpy(e->mac, p, (size_t)ml); e->mac[ml] = 0; }
|
||||||
|
char *t = mac;
|
||||||
|
while (*t) {
|
||||||
|
while (*t == ' ' || *t == '\t') t++;
|
||||||
|
if (!strncmp(t, "codec=", 6)) { t += 6; nm_val_field(&t, e->codec, sizeof e->codec); }
|
||||||
|
else if (!strncmp(t, "rate=", 5)) { t += 5; nm_val_field(&t, e->rate, sizeof e->rate); }
|
||||||
|
else if (!strncmp(t, "channels=", 9)) { t += 9; nm_val_field(&t, e->chn, sizeof e->chn); }
|
||||||
|
else if (!strncmp(t, "bits=", 5)) { t += 5; nm_val_field(&t, e->bits, sizeof e->bits); }
|
||||||
|
else if (!strncmp(t, "acquired=", 9)) { t += 9; e->acquired = atoi(t); }
|
||||||
|
else if (!strncmp(t, "role=", 5)) { t += 5; nm_val_field(&t, e->role, sizeof e->role); }
|
||||||
|
while (*t && *t != ' ' && *t != '\t' && *t != '\n') t++;
|
||||||
|
}
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
line = eol ? eol + 1 : NULL;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 直连回退:bluetoothctl <sub> <mac>(带超时)。返回 MEOW_OK/MEOW_ERR_*。 */
|
||||||
|
static int bt_direct_cmd(const char *sub, const char *mac)
|
||||||
|
{
|
||||||
|
if (!mac || !mac[0]) return MEOW_ERR_INVAL;
|
||||||
|
char cmd[192];
|
||||||
|
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin bluetoothctl %s %s 2>/dev/null", sub, mac);
|
||||||
|
char *o = popen_read_tmo(8000, cmd);
|
||||||
|
int ok = o && (strstr(o, "successful") || strstr(o, "Connected") ||
|
||||||
|
strstr(o, "already") || strstr(o, "Paired"));
|
||||||
|
if (o) free(o);
|
||||||
|
return ok ? MEOW_OK : MEOW_ERR_NODEV;
|
||||||
|
}
|
||||||
|
|
||||||
|
int nm_bt_connect(const char *mac)
|
||||||
|
{
|
||||||
|
char text[128];
|
||||||
|
int rc = netmgr_bt("connect", mac, text, sizeof text);
|
||||||
|
if (rc == NETMGR_UNREACH) return bt_direct_cmd("connect", mac);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
int nm_bt_disconnect(const char *mac)
|
||||||
|
{
|
||||||
|
char text[128];
|
||||||
|
int rc = netmgr_bt("disconnect", mac, text, sizeof text);
|
||||||
|
if (rc == NETMGR_UNREACH) return bt_direct_cmd("disconnect", mac);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
int nm_bt_pair(const char *mac)
|
||||||
|
{
|
||||||
|
char text[128];
|
||||||
|
int rc = netmgr_bt("pair", mac, text, sizeof text);
|
||||||
|
if (rc == NETMGR_UNREACH) return bt_direct_cmd("pair", mac);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
int nm_bt_unpair(const char *mac)
|
||||||
|
{
|
||||||
|
char text[128];
|
||||||
|
int rc = netmgr_bt("unpair", mac, text, sizeof text);
|
||||||
|
if (rc == NETMGR_UNREACH) return bt_direct_cmd("remove", mac);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
void nm_bt_scan_on(void)
|
||||||
|
{
|
||||||
|
if (!netmgr_ok()) { net_bt_scan_trigger(); return; } /* 直连回退 */
|
||||||
|
netmgr_bt("scan", "on", NULL, 0);
|
||||||
|
}
|
||||||
|
void nm_bt_scan_off(void)
|
||||||
|
{
|
||||||
|
if (!netmgr_ok()) return;
|
||||||
|
netmgr_bt("scan", "off", NULL, 0);
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@
|
|||||||
* 元数据/源变化立即整页重画;交互只重绘按钮/进度带。
|
* 元数据/源变化立即整页重画;交互只重绘按钮/进度带。
|
||||||
*/
|
*/
|
||||||
#include "meow_launcher_internal.h"
|
#include "meow_launcher_internal.h"
|
||||||
|
#include "np_gesture.h" /* 批4:双页方向感知手势决策(header-only,宿主可单测) */
|
||||||
|
#include "meow_lyric_view.h" /* 批2控件:歌词页渲染(批4接入,数据批3 meow_media_lyric_*) */
|
||||||
|
|
||||||
|
|
||||||
#define NP_COVER 300 /* 封面边长(方,居中) */
|
#define NP_COVER 300 /* 封面边长(方,居中) */
|
||||||
@@ -33,6 +35,23 @@
|
|||||||
static meow_img *g_np_cover = NULL;
|
static meow_img *g_np_cover = NULL;
|
||||||
static char g_np_cover_path[200] = "";
|
static char g_np_cover_path[200] = "";
|
||||||
|
|
||||||
|
/* ---- 批4:Now Playing 双页状态 + 歌词页控件 ----
|
||||||
|
* 同一容器两页:NP_PAGE_PLAY(播放页,原有内容)+ NP_PAGE_LYRIC(歌词页,批2 meow_lyric_view)。
|
||||||
|
* 页面切换靠方向感知手势(np_gesture.h);歌词数据批3(meow_media_lyric_*),主题先走 meow_theme
|
||||||
|
* 默认;meow_lyric_view_set_colors 为批5 封面取色动态主题预留(本批不取色,见 np_lyric_refresh)。 */
|
||||||
|
static int g_np_page = NP_PAGE_PLAY;
|
||||||
|
static meow_lyric_view *g_np_lyric = NULL; /* 歌词视图(lazy 建一次,跨进入复用) */
|
||||||
|
static uint32_t g_np_lyric_seq = 0xFFFFFFFFu; /* 上次解析的歌曲代际(新 seq 才重解析) */
|
||||||
|
static char g_np_lyric_app[MEOW_APP_ID_MAX] = ""; /* 上次解析的源 app_id */
|
||||||
|
static int g_np_lyric_last_valid = -1; /* -1=未初始化 0=无词 1=有词(防重复 clear) */
|
||||||
|
|
||||||
|
/* 批4:歌词页宽度测量(launcher ui_width 包装成 meow_lyric_width_fn;meow_lyric_view 需要)。 */
|
||||||
|
static int np_lyric_width(const char *u, int size, void *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
return ui_width(u, size);
|
||||||
|
}
|
||||||
|
|
||||||
/* 封面解码缓存(artwork 路径变化才重载)。 */
|
/* 封面解码缓存(artwork 路径变化才重载)。 */
|
||||||
static void np_cover_load(const meow_media_session_t *s)
|
static void np_cover_load(const meow_media_session_t *s)
|
||||||
{
|
{
|
||||||
@@ -137,6 +156,12 @@ static void np_widgets_create(void)
|
|||||||
op.bg = th->primary; op.fg = th->onPrimary;
|
op.bg = th->primary; op.fg = th->onPrimary;
|
||||||
if (g_np_play) meow_widget_set_opts(g_np_play, &op);
|
if (g_np_play) meow_widget_set_opts(g_np_play, &op);
|
||||||
if (g_np_pause) meow_widget_set_opts(g_np_pause, &op);
|
if (g_np_pause) meow_widget_set_opts(g_np_pause, &op);
|
||||||
|
/* 批4:歌词页视图(覆盖状态栏下整页;宽测量走 launcher ui_width 包装)。
|
||||||
|
* set_colors 留作批5 封面取色覆盖入口;本批用 meow_theme 默认(不调用)。 */
|
||||||
|
g_np_lyric = meow_lyric_view_create(0, SB_BAR_H, fb_xres, fb_yres - SB_BAR_H,
|
||||||
|
np_lyric_width, NULL);
|
||||||
|
if (g_np_lyric)
|
||||||
|
meow_lyric_view_set_empty(g_np_lyric, meow_str_zh("np.no_lyric", "暂无歌词"));
|
||||||
meow_ui_activity_pop();
|
meow_ui_activity_pop();
|
||||||
g_np_init = 1;
|
g_np_init = 1;
|
||||||
}
|
}
|
||||||
@@ -180,12 +205,64 @@ static void np_draw_progress(const meow_media_session_t *s, uint32_t pos, const
|
|||||||
ui_draw(NP_PROG_X + NP_PROG_W - ui_width(rbuf, 16), NP_TIME_Y, rbuf, th->on_surface_variant, 16);
|
ui_draw(NP_PROG_X + NP_PROG_W - ui_width(rbuf, 16), NP_TIME_Y, rbuf, th->on_surface_variant, 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==================================================================
|
||||||
|
* 批4:歌词页渲染(批2 meow_lyric_view + 数据批3 meow_media_lyric_*)。
|
||||||
|
* - 有词:meow_lyric_parse(lrc,tlrc)→meow_lyric_view_set_pos(display_pos) 实时滚动,
|
||||||
|
* 当前行由 meow_lyric_view 内部 meow_lyric_line_at(pos_ms) 计算(与绘制/点跳同源)。
|
||||||
|
* - 无词/无源:清空并置 pos=0,视图内建空态文案(np.no_lyric→"暂无歌词")。
|
||||||
|
* 主题:先走 meow_theme 默认(视图自绘 scrim/活动行纯白/灰阶递减);批5 封面取色动态主题的可
|
||||||
|
* 接入点为 meow_lyric_view_set_colors(本批不取色,故不调用;接入只需在下方填 meow_lyric_view_colors
|
||||||
|
* 后调用一次,字段 0=回主题默认)。
|
||||||
|
* ================================================================== */
|
||||||
|
static void np_lyric_refresh(const meow_media_session_t *s)
|
||||||
|
{
|
||||||
|
if (!g_np_lyric) return;
|
||||||
|
int valid = meow_media_lyric_valid(s);
|
||||||
|
if (valid) {
|
||||||
|
/* 换歌/换源(新 lyric_seq/app_id)才重解析;否则沿用,只推进位置。 */
|
||||||
|
if (g_np_lyric_seq != s->lyric_seq || strcmp(g_np_lyric_app, s->app_id) != 0) {
|
||||||
|
meow_lyric_parse(meow_media_lyric_lrc(s), meow_media_lyric_tlrc(s));
|
||||||
|
g_np_lyric_seq = s->lyric_seq;
|
||||||
|
snprintf(g_np_lyric_app, sizeof g_np_lyric_app, "%s", s->app_id);
|
||||||
|
}
|
||||||
|
meow_lyric_view_set_pos(g_np_lyric, meow_media_display_pos(s));
|
||||||
|
} else {
|
||||||
|
/* 无词/无源:仅当上次仍有词才清空(省冗余 memset),置 pos=0。 */
|
||||||
|
if (g_np_lyric_last_valid != 0) {
|
||||||
|
meow_lyric_clear();
|
||||||
|
g_np_lyric_seq = 0xFFFFFFFFu;
|
||||||
|
g_np_lyric_app[0] = 0;
|
||||||
|
}
|
||||||
|
meow_lyric_view_set_pos(g_np_lyric, 0);
|
||||||
|
}
|
||||||
|
g_np_lyric_last_valid = valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 歌词页整页绘制:铺 theme surface 底 → lyric_view(自绘 scrim + 歌词/空态)。 */
|
||||||
|
static void np_draw_lyric_page(int redraw_sb)
|
||||||
|
{
|
||||||
|
const meow_ui_theme_t *th = s_ui_theme();
|
||||||
|
int W = fb_xres, H = fb_yres;
|
||||||
|
if (!g_np_init) np_widgets_create();
|
||||||
|
if (redraw_sb) fill_rect(0, 0, W, SB_BAR_H, th->surface);
|
||||||
|
fill_rect(0, SB_BAR_H, W, H - SB_BAR_H, th->surface);
|
||||||
|
np_lyric_refresh(meow_media_current());
|
||||||
|
if (g_np_lyric)
|
||||||
|
meow_widget_draw(meow_lyric_view_widget(g_np_lyric));
|
||||||
|
if (redraw_sb) draw_statusbar();
|
||||||
|
}
|
||||||
|
|
||||||
/* 整页绘制(空态占位)。redraw_sb=1 时喂/标脏状态栏(打开/主题变化),否则保留上次顶条。 */
|
/* 整页绘制(空态占位)。redraw_sb=1 时喂/标脏状态栏(打开/主题变化),否则保留上次顶条。 */
|
||||||
static void np_draw_page(int redraw_sb)
|
static void np_draw_page(int redraw_sb)
|
||||||
{
|
{
|
||||||
const meow_ui_theme_t *th = s_ui_theme();
|
const meow_ui_theme_t *th = s_ui_theme();
|
||||||
int W = fb_xres, H = fb_yres;
|
int W = fb_xres, H = fb_yres;
|
||||||
if (!g_np_init) np_widgets_create(); /* M7.0b:lazy 建控件(跨进入复用) */
|
if (!g_np_init) np_widgets_create(); /* M7.0b:lazy 建控件(跨进入复用) */
|
||||||
|
/* 批4:歌词页整页走 lyric_view(无源/无词空态由视图内建)。 */
|
||||||
|
if (g_np_page == NP_PAGE_LYRIC) {
|
||||||
|
np_draw_lyric_page(redraw_sb);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (redraw_sb) fill_rect(0, 0, W, SB_BAR_H, th->surface);
|
if (redraw_sb) fill_rect(0, 0, W, SB_BAR_H, th->surface);
|
||||||
fill_rect(0, SB_BAR_H, W, H - SB_BAR_H, th->surface);
|
fill_rect(0, SB_BAR_H, W, H - SB_BAR_H, th->surface);
|
||||||
const meow_media_session_t *s = meow_media_current();
|
const meow_media_session_t *s = meow_media_current();
|
||||||
@@ -299,10 +376,14 @@ void run_now_playing(struct app *apps, int napps)
|
|||||||
uint32_t last_state = 0xFFFFFFFFu;
|
uint32_t last_state = 0xFFFFFFFFu;
|
||||||
char last_app[MEOW_APP_ID_MAX] = "";
|
char last_app[MEOW_APP_ID_MAX] = "";
|
||||||
|
|
||||||
meow_ui_activity_push(NP_SENTINEL_ACT); /* M7.0b:进入 push 哨兵(与函数尾 449 行 pop 配对)。
|
meow_ui_activity_push(NP_SENTINEL_ACT); /* M7.0b:进入 push 哨兵(与函数尾 pop 配对)。
|
||||||
* 控件 owner 一直钉哨兵,命中要走 meow_widget_hit 的
|
* 控件 owner 一直钉哨兵,命中要走 meow_widget_hit 的
|
||||||
* owner==active 闸门——漏 push 则 active()!=0x51,
|
* owner==active 闸门——漏 push 则 active()!=0x51,
|
||||||
* 三钮 meow_widget_hit 恒 false(「播放页三键无效」根因)。 */
|
* 三钮 meow_widget_hit 恒 false(「播放页三键无效」根因)。 */
|
||||||
|
g_np_page = NP_PAGE_PLAY; /* 批4:每次进入 Now Playing 从播放页起(上次退出页不残留) */
|
||||||
|
g_np_lyric_last_valid = -1; /* 批4:重置歌词「是否有词」过渡态,防首帧多清一次 */
|
||||||
|
g_np_lyric_seq = 0xFFFFFFFFu; /* 批4:强制首次进歌词页重解析(防跨进入残留缓存) */
|
||||||
|
g_np_lyric_app[0] = 0;
|
||||||
np_draw_page(1);
|
np_draw_page(1);
|
||||||
meow_dirty_full();
|
meow_dirty_full();
|
||||||
meow_ui_flush();
|
meow_ui_flush();
|
||||||
@@ -335,43 +416,61 @@ void run_now_playing(struct app *apps, int napps)
|
|||||||
if (td_ok) {
|
if (td_ok) {
|
||||||
int dx = tlx - tdx, dy = tly - tdy;
|
int dx = tlx - tdx, dy = tly - tdy;
|
||||||
int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy;
|
int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy;
|
||||||
/* M6.21 诊断:打印该次抬手手势的完整输入与各判定结果(真机定位
|
/* M6.21 诊断 + 批4 页感知:打印该次抬手手势的完整输入与按当前页
|
||||||
* 「返回滑不出」根因——状态栏/通知中心已生效,惟返回未触发)。 */
|
* 的方向感知判定(真机定位「返回滑不出」根因曾依赖此日志;
|
||||||
int np_is_top = mnp_top_swipe_down(tdx, tdy, dx, dy, SB_BAR_H, 65);
|
* 保留,键改为 ges=np_gesture_decide 返回值)。 */
|
||||||
int np_edge = meow_edge_back(tdx, dx, dy);
|
np_gest_t gnp = np_gesture_decide(g_np_page, tdx, tdy, dx, dy);
|
||||||
int np_anyright = (dx >= 40 && adx > ady);
|
logmsg("np up: page=%d down=(%d,%d) d=(%d,%d) adx=%d ady=%d ges=%d",
|
||||||
logmsg("np up: down=(%d,%d) d=(%d,%d) adx=%d ady=%d top=%d edge=%d anyright=%d",
|
g_np_page, tdx, tdy, dx, dy, adx, ady, (int)gnp);
|
||||||
tdx, tdy, dx, dy, adx, ady, np_is_top, np_edge, np_anyright);
|
switch (gnp) {
|
||||||
/* 顶部热区下滑 → 打开通知中心(与主界面同语义;mnp_top_swipe_down
|
case NP_GEST_TOP:
|
||||||
* 内部已排除左缘 down_x<24,与下方返回手势互斥)。run_notif_center
|
/* 顶部热区下滑 → 打开通知中心(与主界面同语义;mnp_top_swipe_down
|
||||||
* 为阻塞模态,返回后整页重画播放页。 */
|
* 内部已排除左缘 down_x<24,与下方返回手势互斥)。run_notif_center
|
||||||
if (np_is_top) {
|
* 为阻塞模态,返回后整页重画。 */
|
||||||
notif_click_t click;
|
{
|
||||||
memset(&click, 0, sizeof click);
|
notif_click_t click;
|
||||||
click.app_idx = -1;
|
memset(&click, 0, sizeof click);
|
||||||
click.status = NOTIF_CLICK_STATUS_NONE;
|
click.app_idx = -1;
|
||||||
run_notif_center(apps, napps, &click, 0, 0, 0);
|
click.status = NOTIF_CLICK_STATUS_NONE;
|
||||||
need_redraw = 1;
|
run_notif_center(apps, napps, &click, 0, 0, 0);
|
||||||
meow_dirty_full();
|
need_redraw = 1;
|
||||||
meow_ui_flush();
|
meow_dirty_full();
|
||||||
}
|
meow_ui_flush();
|
||||||
/* 左缘右滑返回(全局手势语义),或任意横向右滑返回——与主界面
|
|
||||||
* 「左滑进入 Now Playing」对称。返回阈值 65→40 放宽(防用户右滑
|
|
||||||
* 幅度不足 65 判不中);播放页无横向分页内容、无进度条拖动
|
|
||||||
* (进度=点按,幅度<60 走 tap 分支),故任意位置横向右滑恒判返回。 */
|
|
||||||
else if (np_edge || np_anyright) {
|
|
||||||
logmsg("np: BACK break (edge=%d anyright=%d dx=%d)", np_edge, np_anyright, dx);
|
|
||||||
np_exit = 1;
|
|
||||||
break; /* 仅跳出内层读事件 while;外层主循环靠 np_exit 退出 */
|
|
||||||
}
|
|
||||||
if (adx < 60 && ady < 60) {
|
|
||||||
int cmd = np_btn_tap(tlx, tly);
|
|
||||||
if (cmd) { meow_media_playctl((uint32_t)cmd, 0); need_redraw = 1; }
|
|
||||||
else {
|
|
||||||
int64_t seek = np_progress_seek(tlx, tly);
|
|
||||||
if (seek > 0) meow_media_playctl(5, seek);
|
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
case NP_GEST_EXIT:
|
||||||
|
/* 播放页左缘右滑 → 回桌面(保留 M6.26 全局手势语义)。 */
|
||||||
|
logmsg("np: BACK break (exit, page=%d)", g_np_page);
|
||||||
|
np_exit = 1;
|
||||||
|
break;
|
||||||
|
case NP_GEST_NEXT:
|
||||||
|
/* 播放页 右→左滑 → 进歌词页。 */
|
||||||
|
g_np_page = NP_PAGE_LYRIC;
|
||||||
|
need_redraw = 1;
|
||||||
|
logmsg("np: -> lyric page");
|
||||||
|
break;
|
||||||
|
case NP_GEST_PREV:
|
||||||
|
/* 歌词页 左→右滑 → 回播放页(含左缘;不接桌面,避免误退)。 */
|
||||||
|
g_np_page = NP_PAGE_PLAY;
|
||||||
|
need_redraw = 1;
|
||||||
|
logmsg("np: -> play page");
|
||||||
|
break;
|
||||||
|
case NP_GEST_TAP:
|
||||||
|
/* 近点按(按页分派):播放页=三钮/进度看点;歌词页预留点跳
|
||||||
|
* (本批不启用,见 batch5/后续:借 meow_lyric_view_hit_line + on_seek)。 */
|
||||||
|
if (g_np_page == NP_PAGE_PLAY) {
|
||||||
|
int cmd = np_btn_tap(tlx, tly);
|
||||||
|
if (cmd) { meow_media_playctl((uint32_t)cmd, 0); need_redraw = 1; }
|
||||||
|
else {
|
||||||
|
int64_t seek = np_progress_seek(tlx, tly);
|
||||||
|
if (seek > 0) meow_media_playctl(5, seek);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break; /* NP_GEST_NONE:反向/纵向主导,忽略不误切 */
|
||||||
}
|
}
|
||||||
|
if (np_exit) break; /* M6.22:返回手势已命中 → 跳出内层读事件 while */
|
||||||
tdx = tdy = -1;
|
tdx = tdy = -1;
|
||||||
} else { tdx = tdy = -1; }
|
} else { tdx = tdy = -1; }
|
||||||
}
|
}
|
||||||
@@ -438,9 +537,18 @@ void run_now_playing(struct app *apps, int napps)
|
|||||||
need_redraw = 0;
|
need_redraw = 0;
|
||||||
last_prog = now;
|
last_prog = now;
|
||||||
} else if (s && (now - last_prog >= 1000)) {
|
} else if (s && (now - last_prog >= 1000)) {
|
||||||
/* 进度推进:仅重绘进度带 + 按钮 */
|
/* 进度推进(按页分流):播放页=进度带+按钮;歌词页=歌词实时滚动重绘(当前行随 pos)。 */
|
||||||
last_prog = now;
|
last_prog = now;
|
||||||
np_repaint_progress();
|
if (g_np_page == NP_PAGE_LYRIC) {
|
||||||
|
np_lyric_refresh(s);
|
||||||
|
if (g_np_lyric) {
|
||||||
|
meow_widget_draw(meow_lyric_view_widget(g_np_lyric));
|
||||||
|
meow_dirty_add(0, SB_BAR_H, fb_xres, fb_yres - SB_BAR_H);
|
||||||
|
meow_ui_flush();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
np_repaint_progress();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/* M6.20:播放页状态栏时钟 1s 刷新(与主界面/子页同语义;draw_clock 喂 SDK 全局
|
/* M6.20:播放页状态栏时钟 1s 刷新(与主界面/子页同语义;draw_clock 喂 SDK 全局
|
||||||
* 状态栏 + 标脏顶条,SDK present() 重绘——否则时间冻结在进入播放页那一刻)。 */
|
* 状态栏 + 标脏顶条,SDK present() 重绘——否则时间冻结在进入播放页那一刻)。 */
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/* np_gesture.h — Now Playing 双页手势方向感知(header-only 纯逻辑,宿主可单测)。
|
||||||
|
*
|
||||||
|
* 定案(用户,批4):Now Playing 同一容器两页——播放页 + 歌词页,手势按当前页方向感知:
|
||||||
|
* - 播放页:右→左滑 → 进歌词页;左缘右滑 → 回桌面(保留 M6.26 语义)。
|
||||||
|
* - 歌词页:左→右滑 → 回播放页(不是回桌面);左缘右滑不接桌面(避免误退)。
|
||||||
|
* 本头为 now_playing.c 与宿主单测(test_now_playing_widgets.c)共用的唯一手势决策,避免两端漂移。
|
||||||
|
*
|
||||||
|
* 依赖:仅 <stdint.h> + meow_notif_panel.h(mnp_top_swipe_down,纯函数)。左缘返回逻辑自
|
||||||
|
* (re)实现(镜像 meow_touch.c meow_edge_back 同口径,宿主编译无需链 meow_touch.c)。
|
||||||
|
*/
|
||||||
|
#ifndef NP_GESTURE_H
|
||||||
|
#define NP_GESTURE_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "meow_notif_panel.h" /* mnp_top_swipe_down(顶部下滑→通知中心) */
|
||||||
|
|
||||||
|
/* ---- 页面 ---- */
|
||||||
|
#define NP_PAGE_PLAY 0
|
||||||
|
#define NP_PAGE_LYRIC 1
|
||||||
|
|
||||||
|
/* ---- 手势动作 ---- */
|
||||||
|
typedef enum {
|
||||||
|
NP_GEST_NONE = 0, /* 无(横向反向滑/纵向主导不匹配;调用方忽略,不误切页) */
|
||||||
|
NP_GEST_TOP, /* 顶部热区下滑 → 打开通知中心 */
|
||||||
|
NP_GEST_NEXT, /* 播放页 右→左滑 → 进入歌词页 */
|
||||||
|
NP_GEST_PREV, /* 歌词页 左→右滑 → 回到播放页 */
|
||||||
|
NP_GEST_EXIT, /* 播放页 左缘右滑 → 返回桌面 */
|
||||||
|
NP_GEST_TAP, /* 近点按(按当前页分派:播放页=按钮/进度看点) */
|
||||||
|
} np_gest_t;
|
||||||
|
|
||||||
|
/* ---- 常量(对齐 launcher 同口径;#ifndef 防与 meow_touch.h 等重复定义冲突)---- */
|
||||||
|
#ifndef NP_SWIPE_THRESH
|
||||||
|
#define NP_SWIPE_THRESH 65 /* 顶部下滑触发位移阈值(对齐 launcher MENU_SWIPE_THRESH) */
|
||||||
|
#endif
|
||||||
|
#ifndef NP_PAGE_SWIPE
|
||||||
|
#define NP_PAGE_SWIPE 40 /* 横向切页最小位移(对齐 M6.26 np_anyright 的 dx>=40) */
|
||||||
|
#endif
|
||||||
|
#ifndef NP_TAP_MAX
|
||||||
|
#define NP_TAP_MAX 60 /* 点按位移上限(对齐现有 adx<60 && ady<60) */
|
||||||
|
#endif
|
||||||
|
#ifndef NP_TOP_HOTZONE
|
||||||
|
#define NP_TOP_HOTZONE 28 /* 顶部热区高(= launcher SB_BAR_H;通知中心/播放页同) */
|
||||||
|
#endif
|
||||||
|
#ifndef NP_EDGE_W
|
||||||
|
#define NP_EDGE_W 24 /* 左缘起点宽度(镜像 meow_touch.h MEOW_EDGE_W) */
|
||||||
|
#endif
|
||||||
|
#ifndef NP_EDGE_SWIPE
|
||||||
|
#define NP_EDGE_SWIPE 65 /* 左缘右滑位移阈值(镜像 meow_touch.h MEOW_EDGE_SWIPE) */
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* 左缘返回(镜像 meow_touch.c meow_edge_back:down_x∈[0,NP_EDGE_W) 且 dx>=NP_EDGE_SWIPE
|
||||||
|
* 且横向主导)。为宿主测试免链 meow_touch.c,此处 static inline 复刻同口径。 */
|
||||||
|
static inline int np_edge_back(int down_x, int dx, int dy)
|
||||||
|
{
|
||||||
|
int ady = dy < 0 ? -dy : dy;
|
||||||
|
if (down_x >= 0 && down_x < NP_EDGE_W && dx >= NP_EDGE_SWIPE && ady < dx)
|
||||||
|
return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按当前页把手势几何 → 动作。down_x/down_y=抬手原点;dx/dy=抬手位移(释放-原点,即 tlx-tdx)。
|
||||||
|
* 优先级:顶部下滑 > 横向主导切页/返回 > 点按。横向反向或无主导 → NONE(调用方忽略,不误切)。 */
|
||||||
|
static inline np_gest_t np_gesture_decide(int page, int down_x, int down_y, int dx, int dy)
|
||||||
|
{
|
||||||
|
int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy;
|
||||||
|
/* 顶部热区下滑 → 通知中心(与主界面同语义;mnp_top_swipe_down 内部排除左缘起点) */
|
||||||
|
if (mnp_top_swipe_down(down_x, down_y, dx, dy, NP_TOP_HOTZONE, NP_SWIPE_THRESH))
|
||||||
|
return NP_GEST_TOP;
|
||||||
|
/* 横向主导(adx>=NP_PAGE_SWIPE 且横向位移>竖向)→ 按当前页分流切页/返回 */
|
||||||
|
if (adx >= NP_PAGE_SWIPE && adx > ady) {
|
||||||
|
if (page == NP_PAGE_PLAY) {
|
||||||
|
if (np_edge_back(down_x, dx, dy)) return NP_GEST_EXIT; /* 左缘右滑 → 回桌面 */
|
||||||
|
if (dx <= -NP_PAGE_SWIPE) return NP_GEST_NEXT; /* 右→左滑 → 进歌词页 */
|
||||||
|
return NP_GEST_NONE; /* 非左缘右滑=播放页无上一页,忽略 */
|
||||||
|
} else { /* NP_PAGE_LYRIC */
|
||||||
|
if (dx >= NP_PAGE_SWIPE) return NP_GEST_PREV; /* 左→右滑 → 回播放页(含左缘,不接桌面) */
|
||||||
|
return NP_GEST_NONE; /* 右→左滑=歌词页是末页,忽略 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* 非横向主导:近点按 */
|
||||||
|
if (adx < NP_TAP_MAX && ady < NP_TAP_MAX) return NP_GEST_TAP;
|
||||||
|
return NP_GEST_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* NP_GESTURE_H */
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
/* 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 附近设备仅列未配对(已配对者归「已配对设备」)。
|
||||||
|
*
|
||||||
|
* 编译(在 patch_v2/meow_launcherd 下):
|
||||||
|
* gcc -I../sdk -I. -Wall -Wextra -fsanitize=undefined test_bt_page.c -o test_bt_page
|
||||||
|
* 运行:./test_bt_page
|
||||||
|
*/
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <strings.h>
|
||||||
|
|
||||||
|
#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 桩值 */
|
||||||
|
|
||||||
|
int nm_bt_powered(void) { return 1; }
|
||||||
|
int nm_bt_onoff(int on) { (void)on; return 0; }
|
||||||
|
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 = 1; 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")) 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;
|
||||||
|
int s_dd_modal(const char *title, const char *item[S_NET_MAX], int n, int cur) { (void)title; (void)item; (void)n; (void)cur; return -1; }
|
||||||
|
int meow_subpage_run(sub_page_t *p) { (void)p; return 0; }
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
|
||||||
|
return g_fail;
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
/* test_media_lyric.c — 宿主单测:批3 live 歌词数据源框架(meow_media 会话携带 lrc/tlrc → meow_lyric 解析 → 随播放位置命中当前行)。
|
||||||
|
*
|
||||||
|
* 链路:App 上报歌词(分片 m_lyric → meow_media_lyric)→ 会话累积 lrc/tlrc(有界、seq 代际)
|
||||||
|
* → launcher 读取(meow_media_lyric_valid/lrc/tlrc)→ meow_lyric_parse → meow_lyric_line_at(meow_media_display_pos)。
|
||||||
|
*
|
||||||
|
* 覆盖矩阵:
|
||||||
|
* L1 App 上报 lrc/tlrc → 会话携带歌词(valid/lrc/tlrc 正确)
|
||||||
|
* L2 launcher 读取 → meow_lyric_parse 成功(count/行文本/翻译合并正确)
|
||||||
|
* L3 播放位置推进(display_pos 随 PLAYING 单调增)→ line_at 命中当前行(pos 增大 → 行号不降)
|
||||||
|
* L4 分片重组:>MEOW_LYRIC_CHUNK 的 lrc 拆多帧上报,会话按 off 累积成完整串
|
||||||
|
* L5 seq 代际切换(换歌)→ 清旧词;同 seq 重报(幂等)不破坏
|
||||||
|
* L6 会话不存在 → meow_media_lyric 返回 -1;翻译无匹配自动忽略
|
||||||
|
* L7 多 App 独立:另一源上报自己的歌词,互不串
|
||||||
|
*
|
||||||
|
* 编译(宿主):meow_media.c(纯 libc)+ meow_lyric.c(纯 libc+pthread)。
|
||||||
|
* gcc -Wall -Wextra -pthread -o test_media_lyric test_media_lyric.c meow_media.c ../sdk/meow_lyric.c -I. -I../sdk
|
||||||
|
* ./test_media_lyric
|
||||||
|
*/
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include "meow_media.h"
|
||||||
|
#include "meow_ipc.h"
|
||||||
|
#include "meow_lyric.h"
|
||||||
|
|
||||||
|
static int g_fails = 0;
|
||||||
|
#define CHECK(cond, msg) do { \
|
||||||
|
if (cond) printf("PASS: %s\n", msg); \
|
||||||
|
else { printf("FAIL: %s (%s:%d)\n", msg, __FILE__, __LINE__); g_fails++; } } while (0)
|
||||||
|
|
||||||
|
static size_t test_send(int fd, uint16_t type, uint32_t req_id,
|
||||||
|
const void *payload, uint32_t len)
|
||||||
|
{
|
||||||
|
/* 用不到(本链路无下行),占位满足 meow_media_ops.send 签名。 */
|
||||||
|
(void)fd; (void)type; (void)req_id; (void)payload; (void)len;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void fill_meta(m_meta_t *m, const char *app, uint32_t state, uint32_t pos, uint32_t dur)
|
||||||
|
{
|
||||||
|
memset(m, 0, sizeof *m);
|
||||||
|
snprintf(m->app_id, sizeof m->app_id, "%s", app);
|
||||||
|
m->state = state; m->pos_ms = pos; m->dur_ms = dur;
|
||||||
|
snprintf(m->title, sizeof m->title, "Song");
|
||||||
|
snprintf(m->artist, sizeof m->artist, "Artist");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 模拟 ncm 的 send_lyric_kind:把 text 按 ≤MEOW_LYRIC_CHUNK 分片,逐片调 meow_media_lyric。 */
|
||||||
|
static void report_lyric_chunked(const char *app, uint32_t seq, uint16_t kind,
|
||||||
|
const char *text, size_t cmax)
|
||||||
|
{
|
||||||
|
if (!text) text = "";
|
||||||
|
size_t total = strlen(text);
|
||||||
|
if (total > cmax) total = cmax;
|
||||||
|
size_t off = 0;
|
||||||
|
do {
|
||||||
|
size_t n = total - off;
|
||||||
|
if (n > MEOW_LYRIC_CHUNK) n = MEOW_LYRIC_CHUNK;
|
||||||
|
m_lyric_t ly;
|
||||||
|
memset(&ly, 0, sizeof ly);
|
||||||
|
snprintf(ly.app_id, sizeof ly.app_id, "%s", app);
|
||||||
|
ly.seq = seq;
|
||||||
|
ly.kind = kind;
|
||||||
|
ly.total = (uint16_t)total;
|
||||||
|
ly.off = (uint16_t)off;
|
||||||
|
ly.len = (uint16_t)n;
|
||||||
|
if (n) memcpy(ly.data, text + off, n);
|
||||||
|
ly.data[n] = 0;
|
||||||
|
int rc = meow_media_lyric(&ly);
|
||||||
|
if (rc != 0) { printf(" (report chunk off=%zu rc=%d)\n", off, rc); }
|
||||||
|
off += n;
|
||||||
|
} while (off < total);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
meow_media_ops ops;
|
||||||
|
ops.send = test_send;
|
||||||
|
ops.log = NULL;
|
||||||
|
meow_media_init(&ops);
|
||||||
|
|
||||||
|
const char *lrc = "[00:01.00]Hello\n[00:02.00]World\n[00:03.00]Bye\n";
|
||||||
|
const char *tlrc = "[00:01.00]你好\n[00:03.00]再见\n[99:99.99]ignore me\n";
|
||||||
|
|
||||||
|
/* ---- L1+L4 大片(跨分片)---- */
|
||||||
|
char big[4096];
|
||||||
|
int bi = 0;
|
||||||
|
bi += snprintf(big + bi, sizeof big - bi, "[00:00.10]p0\n");
|
||||||
|
for (int i = 1; i <= 40; i++) /* 约 40 行 → >MEOW_LYRIC_CHUNK(980B) */
|
||||||
|
bi += snprintf(big + bi, sizeof big - bi, "[00:%02d.%02d]line%d\n", i / 60, i % 60, i);
|
||||||
|
|
||||||
|
/* 注册会话(META 先行,否则 meow_media_lyric 丢弃) */
|
||||||
|
m_meta_t m;
|
||||||
|
fill_meta(&m, "dev.player.ncm", MEOW_MEDIA_PLAYING, 1000, 120000);
|
||||||
|
CHECK(meow_media_meta(&m, -1) == 0, "META registers session");
|
||||||
|
|
||||||
|
/* 上报歌词:seq=1(新代际),lrc 与 tlrc 各分片 */
|
||||||
|
report_lyric_chunked("dev.player.ncm", 1, 0, lrc, MEOW_MEDIA_LRC_MAX);
|
||||||
|
report_lyric_chunked("dev.player.ncm", 1, 1, tlrc, MEOW_MEDIA_TLRC_MAX);
|
||||||
|
|
||||||
|
const meow_media_session_t *s = meow_media_current();
|
||||||
|
CHECK(s && !strcmp(s->app_id, "dev.player.ncm"), "current() == dev.player.ncm");
|
||||||
|
CHECK(meow_media_lyric_valid(s) == 1, "L1: lyric_valid == 1");
|
||||||
|
CHECK(strcmp(meow_media_lyric_lrc(s), lrc) == 0, "L1: lrc matches");
|
||||||
|
CHECK(strcmp(meow_media_lyric_tlrc(s), tlrc) == 0, "L1: tlrc matches");
|
||||||
|
|
||||||
|
/* ---- L2 launcher 读取 → 解析 ---- */
|
||||||
|
int n = meow_lyric_parse(meow_media_lyric_lrc(s), meow_media_lyric_tlrc(s));
|
||||||
|
CHECK(n == 3, "L2: meow_lyric_parse count == 3");
|
||||||
|
CHECK(meow_lyric_count() == 3, "L2: count() == 3");
|
||||||
|
CHECK(strcmp(meow_lyric_line_text(0), "Hello") == 0, "L2: line0 text Hello");
|
||||||
|
CHECK(strcmp(meow_lyric_line_tl(0), "你好") == 0, "L2: line0 tl 你好 (2000ms 合并)");
|
||||||
|
CHECK(strcmp(meow_lyric_line_text(2), "Bye") == 0, "L2: line2 text Bye");
|
||||||
|
CHECK(strcmp(meow_lyric_line_tl(2), "再见") == 0, "L2: line2 tl 再见");
|
||||||
|
CHECK(meow_lyric_count() == 3, "L2: 无匹配翻译行被忽略(仍 3 行)");
|
||||||
|
|
||||||
|
/* ---- L3 播放位置推进 → 命中当前行 ---- */
|
||||||
|
uint32_t pA = meow_media_display_pos(s);
|
||||||
|
int lineA = meow_lyric_line_at(pA);
|
||||||
|
CHECK(lineA == 0, "L3: display_pos ≈ 1000ms → line 0 (Hello)");
|
||||||
|
usleep(600 * 1000); /* 播放中:display_pos 随墙钟推进 */
|
||||||
|
uint32_t pB = meow_media_display_pos(s);
|
||||||
|
int lineB = meow_lyric_line_at(pB);
|
||||||
|
CHECK(pB >= pA, "L3: display_pos advances (playing)");
|
||||||
|
CHECK(lineB >= lineA, "L3: line_at pos 增大 → 行号不降(随播放命中当前行)");
|
||||||
|
/* 再推进到 ≥3000ms 应到末行 */
|
||||||
|
usleep(1500 * 1000);
|
||||||
|
uint32_t pC = meow_media_display_pos(s);
|
||||||
|
CHECK(pC >= 3000u || lineB >= 2, "L3: pos 推进后到位(或已末行)");
|
||||||
|
CHECK(meow_lyric_line_at(pC) >= meow_lyric_line_at(pB), "L3: 行号单调不降");
|
||||||
|
|
||||||
|
/* ---- L4 大片分片重组(big lrc)---- */
|
||||||
|
meow_media_clear(NULL);
|
||||||
|
fill_meta(&m, "dev.player.ncm", MEOW_MEDIA_PLAYING, 0, 120000);
|
||||||
|
meow_media_meta(&m, -1);
|
||||||
|
report_lyric_chunked("dev.player.ncm", 2, 0, big, MEOW_MEDIA_LRC_MAX);
|
||||||
|
s = meow_media_current();
|
||||||
|
CHECK(s && meow_media_lyric_valid(s) == 1, "L4: big lyric_valid == 1");
|
||||||
|
int bn = meow_lyric_parse(meow_media_lyric_lrc(s), NULL);
|
||||||
|
CHECK(bn > 40, "L4: 大片跨分片重组后解析行数 > 40");
|
||||||
|
CHECK(meow_lyric_line_at(0) == 0 && meow_lyric_line_at(10) == 0, "L4: 低 pos 命中第0行");
|
||||||
|
CHECK(meow_lyric_line_at(meow_lyric_line_time(bn - 1)) == bn - 1, "L4: 末行命中");
|
||||||
|
|
||||||
|
/* ---- L5 seq 代际切换(换歌)清旧词 ---- */
|
||||||
|
report_lyric_chunked("dev.player.ncm", 3, 0, "[00:05.00]New song line\n", MEOW_MEDIA_LRC_MAX);
|
||||||
|
s = meow_media_current();
|
||||||
|
CHECK(meow_media_lyric_valid(s) == 1, "L5: 新代际仍 valid");
|
||||||
|
CHECK(strcmp(meow_media_lyric_lrc(s), "[00:05.00]New song line\n") == 0, "L5: 新词覆盖旧词");
|
||||||
|
CHECK(meow_media_lyric_tlrc(s)[0] == 0, "L5: 新代际清空旧翻译");
|
||||||
|
/* seq 不变重报:幂等(不清) */
|
||||||
|
report_lyric_chunked("dev.player.ncm", 3, 1, "[00:05.00]新歌\n", MEOW_MEDIA_TLRC_MAX);
|
||||||
|
s = meow_media_current();
|
||||||
|
CHECK(strcmp(meow_media_lyric_tlrc(s), "[00:05.00]新歌\n") == 0, "L5: 同 seq 补翻译不清");
|
||||||
|
|
||||||
|
/* ---- L6 会话不存在 → -1;空 app_id → -1 ---- */
|
||||||
|
m_lyric_t empty;
|
||||||
|
memset(&empty, 0, sizeof empty);
|
||||||
|
snprintf(empty.app_id, sizeof empty.app_id, "no.session.app");
|
||||||
|
empty.seq = 9; empty.kind = 0; empty.total = 0; empty.off = 0; empty.len = 0;
|
||||||
|
CHECK(meow_media_lyric(&empty) == -1, "L6: 未注册会话 → -1");
|
||||||
|
memset(&empty, 0, sizeof empty);
|
||||||
|
CHECK(meow_media_lyric(&empty) == -1, "L6: 空 app_id → -1");
|
||||||
|
|
||||||
|
/* ---- L7 多 App 独立 ---- */
|
||||||
|
fill_meta(&m, "com.player2", MEOW_MEDIA_PLAYING, 0, 180000);
|
||||||
|
meow_media_meta(&m, -1);
|
||||||
|
meow_media_clear("dev.player.ncm"); /* 只清 ncm */
|
||||||
|
report_lyric_chunked("com.player2", 1, 0, "[00:01.00]Second app\n", MEOW_MEDIA_LRC_MAX);
|
||||||
|
s = meow_media_current();
|
||||||
|
CHECK(s && !strcmp(s->app_id, "com.player2"), "L7: current == com.player2");
|
||||||
|
CHECK(meow_media_lyric_valid(s) == 1, "L7: player2 歌词有效");
|
||||||
|
CHECK(strcmp(meow_media_lyric_lrc(s), "[00:01.00]Second app\n") == 0, "L7: player2 歌词独立");
|
||||||
|
|
||||||
|
/* ---- 空歌词(无词歌曲):上报空串 → valid=0 ---- */
|
||||||
|
report_lyric_chunked("com.player2", 2, 0, "", MEOW_MEDIA_LRC_MAX);
|
||||||
|
report_lyric_chunked("com.player2", 2, 1, "", MEOW_MEDIA_TLRC_MAX);
|
||||||
|
s = meow_media_current();
|
||||||
|
CHECK(meow_media_lyric_valid(s) == 0, "空词上报 → lyric_valid == 0");
|
||||||
|
|
||||||
|
printf("\n%s (%d fails)\n", g_fails ? "FAILED" : "ALL PASS", g_fails);
|
||||||
|
return g_fails ? 1 : 0;
|
||||||
|
}
|
||||||
@@ -16,11 +16,15 @@
|
|||||||
* ../sdk/meow_ui_dialog.c ../sdk/meow_ui_ime.c ../sdk/meow_ui_str.c ../sdk/meow_pinyin.c \
|
* ../sdk/meow_ui_dialog.c ../sdk/meow_ui_ime.c ../sdk/meow_ui_str.c ../sdk/meow_pinyin.c \
|
||||||
* ../sdk/meow_ui_draw.c ../sdk/meow_ui_view.c ../sdk/meow_ui_box.c ../sdk/meow_ui_anim.c \
|
* ../sdk/meow_ui_draw.c ../sdk/meow_ui_view.c ../sdk/meow_ui_box.c ../sdk/meow_ui_anim.c \
|
||||||
* ../sdk/meow_ui_scroll.c ../sdk/meow_ui_img.c ../sdk/meow_ui_media.c \
|
* ../sdk/meow_ui_scroll.c ../sdk/meow_ui_img.c ../sdk/meow_ui_media.c \
|
||||||
* ../sdk/meow_notify_ipc.c ../sdk/stb_image.c -I../sdk -I. -DMEOW_UI_HOST_TEST -pthread -lm
|
* ../sdk/meow_notify_ipc.c ../sdk/stb_image.c \
|
||||||
|
* ../sdk/meow_lyric.c ../sdk/meow_lyric_view.c \
|
||||||
|
* -I../sdk -I. -DMEOW_UI_HOST_TEST -pthread -lm
|
||||||
* 运行:./test_now_playing_widgets
|
* 运行:./test_now_playing_widgets
|
||||||
*/
|
*/
|
||||||
#include "meow_ui.h"
|
#include "meow_ui.h"
|
||||||
#include "meow_ui_internal.h" /* meow_ui_back_buf/pitch/bpp */
|
#include "meow_ui_internal.h" /* meow_ui_back_buf/pitch/bpp */
|
||||||
|
#include "np_gesture.h" /* 批4:双页方向感知手势决策(纯逻辑,宿主可单测) */
|
||||||
|
#include "meow_lyric_view.h" /* 批2:歌词页控件(批4接入,数据批3 meow_media_lyric_*) */
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -86,6 +90,13 @@ static void mk_widgets(void)
|
|||||||
meow_ui_activity_pop();
|
meow_ui_activity_pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 歌词视图宽度测量(宿主:字节数近似,够用不裂屏;launcher 侧为 ui_width)。 */
|
||||||
|
static int lv_test_width(const char *u, int size, void *ctx)
|
||||||
|
{
|
||||||
|
(void)size; (void)ctx;
|
||||||
|
return (int)strlen(u);
|
||||||
|
}
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
setbuf(stdout, NULL);
|
setbuf(stdout, NULL);
|
||||||
@@ -168,6 +179,72 @@ int main(void)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============ G5 双页手势方向感知(np_gesture.h,纯逻辑) ============ */
|
||||||
|
printf("G5 dual-page direction-aware gestures\n");
|
||||||
|
/* 播放页:右→左滑 → 进歌词页 */
|
||||||
|
CHECK(np_gesture_decide(NP_PAGE_PLAY, 200, 100, -50, 5) == NP_GEST_NEXT,
|
||||||
|
"play: left-swipe -> lyric");
|
||||||
|
/* 播放页:左缘右滑 → 回桌面(保留 M6.26) */
|
||||||
|
CHECK(np_gesture_decide(NP_PAGE_PLAY, 8, 100, 80, 5) == NP_GEST_EXIT,
|
||||||
|
"play: edge right-swipe -> exit");
|
||||||
|
/* 播放页:非左缘右滑 → 播放页无上一页,忽略(不误切/不误退) */
|
||||||
|
CHECK(np_gesture_decide(NP_PAGE_PLAY, 200, 100, 80, 5) == NP_GEST_NONE,
|
||||||
|
"play: non-edge right-swipe -> none");
|
||||||
|
/* 歌词页:左→右滑 → 回播放页(不是回桌面) */
|
||||||
|
CHECK(np_gesture_decide(NP_PAGE_LYRIC, 200, 100, 60, 5) == NP_GEST_PREV,
|
||||||
|
"lyric: right-swipe -> play");
|
||||||
|
/* 歌词页:左缘右滑 → 回播放页(不接桌面,避免误退) */
|
||||||
|
CHECK(np_gesture_decide(NP_PAGE_LYRIC, 8, 100, 80, 5) == NP_GEST_PREV,
|
||||||
|
"lyric: edge right-swipe -> play (not exit)");
|
||||||
|
/* 歌词页:右→左滑 → 歌词页是末页,忽略 */
|
||||||
|
CHECK(np_gesture_decide(NP_PAGE_LYRIC, 200, 100, -60, 5) == NP_GEST_NONE,
|
||||||
|
"lyric: left-swipe -> none");
|
||||||
|
/* 顶部热区下滑 → 通知中心(两个页面同语义) */
|
||||||
|
CHECK(np_gesture_decide(NP_PAGE_PLAY, 100, 10, 0, 70) == NP_GEST_TOP,
|
||||||
|
"top swipe-down -> notif");
|
||||||
|
/* 近点按 → TAP;纵向主导 → NONE(不误切) */
|
||||||
|
CHECK(np_gesture_decide(NP_PAGE_PLAY, 200, 300, 5, 5) == NP_GEST_TAP,
|
||||||
|
"near tap -> TAP");
|
||||||
|
CHECK(np_gesture_decide(NP_PAGE_PLAY, 200, 300, 5, 80) == NP_GEST_NONE,
|
||||||
|
"vertical dominant -> NONE");
|
||||||
|
|
||||||
|
/* ============ G6 歌词页:空态 + 渲染(批2 控件 + 批3 数据语义) ============ */
|
||||||
|
printf("G6 lyric view: empty + render\n");
|
||||||
|
meow_ui_activity_push(NP_SENTINEL);
|
||||||
|
meow_lyric_view *lv = meow_lyric_view_create(0, 28, 480, 720 - 28, lv_test_width, NULL);
|
||||||
|
meow_lyric_view_set_empty(lv, "暂无歌词");
|
||||||
|
meow_ui_activity_pop();
|
||||||
|
CHECK(lv != NULL, "lyric_view created");
|
||||||
|
if (lv) {
|
||||||
|
/* 无歌词空态:无解析 → 0 行、visible=0(视图内建空态文案) */
|
||||||
|
meow_lyric_clear();
|
||||||
|
CHECK(meow_lyric_count() == 0, "clear -> 0 lines");
|
||||||
|
CHECK(meow_lyric_view_visible(lv, 0, NULL, 0) == 0, "empty -> 0 visible entries");
|
||||||
|
/* 有词:解析两行 → count/line_at/visible/当前行色 */
|
||||||
|
int np = meow_lyric_parse("[00:01.00]第一行歌词\n[00:05.00]第二行歌词", "");
|
||||||
|
CHECK(np == 2, "parse -> 2 lines");
|
||||||
|
CHECK(meow_lyric_count() == 2, "count == 2");
|
||||||
|
CHECK(meow_lyric_line_at(0) == 0, "line_at(0) -> first");
|
||||||
|
CHECK(meow_lyric_line_at(3000) == 0, "line_at(3000) -> first");
|
||||||
|
CHECK(meow_lyric_line_at(6000) == 1, "line_at(6000) -> second");
|
||||||
|
CHECK(strcmp(meow_lyric_line_text(0), "第一行歌词") == 0, "text(0) == 第一行歌词");
|
||||||
|
meow_lyric_view_vis vb[9];
|
||||||
|
int vn = meow_lyric_view_visible(lv, 3000, vb, 9);
|
||||||
|
CHECK(vn > 0, "visible(3000) > 0");
|
||||||
|
CHECK(meow_lyric_view_cur_color(lv) == meow_theme_get()->onSurface,
|
||||||
|
"cur_color == theme onSurface");
|
||||||
|
/* 批5 取色预留:set_colors 可接入(覆盖活动行为主题 accent 后再还原默认) */
|
||||||
|
meow_lyric_view_colors c; memset(&c, 0, sizeof c);
|
||||||
|
c.cur = meow_theme_get()->accent;
|
||||||
|
meow_lyric_view_set_colors(lv, &c);
|
||||||
|
CHECK(meow_lyric_view_cur_color(lv) == meow_theme_get()->accent,
|
||||||
|
"set_colors cur override works");
|
||||||
|
meow_lyric_view_set_colors(lv, NULL); /* 回主题默认 */
|
||||||
|
CHECK(meow_lyric_view_cur_color(lv) == meow_theme_get()->onSurface,
|
||||||
|
"set_colors NULL -> theme default");
|
||||||
|
meow_lyric_view_free(lv);
|
||||||
|
}
|
||||||
|
|
||||||
meow_ui_activity_pop();
|
meow_ui_activity_pop();
|
||||||
meow_ui_shutdown();
|
meow_ui_shutdown();
|
||||||
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
|
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
|
||||||
|
|||||||
@@ -1633,6 +1633,22 @@ static void ipc_dispatch(struct meow_ipc_msg *m)
|
|||||||
(long long)c.seek_ms, rc);
|
(long long)c.seek_ms, rc);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
/* 阶段D·批3:媒体会话歌词上报(App 播放时把 lrc/tlrc 上报到会话)。分片经
|
||||||
|
* meow_media_lyric 归并(有界缓冲+seq 代际);回 ACK。 */
|
||||||
|
case MSG_META_LYRIC: {
|
||||||
|
m_lyric_t l;
|
||||||
|
memset(&l, 0, sizeof l);
|
||||||
|
memcpy(&l, m->payload, m->plen < (uint16_t)sizeof l ? m->plen : (uint16_t)sizeof l);
|
||||||
|
l.app_id[MEOW_APP_ID_MAX - 1] = 0;
|
||||||
|
int rc = meow_media_lyric(&l);
|
||||||
|
m_ack_t a;
|
||||||
|
a.code = MEOW_OK; /* 会话策略:无会话丢弃(rc=-1),不视为错误 */
|
||||||
|
a.status = (uint32_t)rc;
|
||||||
|
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
|
||||||
|
logmsg("ipc: META_LYRIC app=%s kind=%u seq=%u total=%u off=%u len=%u rc=%d -> ACK",
|
||||||
|
l.app_id, l.kind, l.seq, l.total, l.off, l.len, rc);
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
logmsg("ipc: unsupported frame type 0x%04x (req %u)", m->type, m->req_id);
|
logmsg("ipc: unsupported frame type 0x%04x (req %u)", m->type, m->req_id);
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -69,6 +69,9 @@ static const char *g_sock = "/usr/data/meow_net.sock"; /* MEOW_NET_S
|
|||||||
static const char *g_wpa_cli = "wpa_cli"; /* MEOW_WPA_CLI */
|
static const char *g_wpa_cli = "wpa_cli"; /* MEOW_WPA_CLI */
|
||||||
static const char *g_hciconfig = "hciconfig"; /* MEOW_HCICONFIG */
|
static const char *g_hciconfig = "hciconfig"; /* MEOW_HCICONFIG */
|
||||||
static const char *g_bluetoothctl= "bluetoothctl"; /* MEOW_BLUETOOTHCTL */
|
static const char *g_bluetoothctl= "bluetoothctl"; /* MEOW_BLUETOOTHCTL */
|
||||||
|
static const char *g_hcitool = "hcitool"; /* MEOW_HCITOOL(内核 ACL 连接权威) */
|
||||||
|
static const char *g_bluealsa_cli= "bluealsa-cli"; /* MEOW_BLUEALSA_CLI(A2DP 传输详情) */
|
||||||
|
static const char *g_bt_scan_cache = "/tmp/meow_bt_scan.txt"; /* MEOW_BT_SCAN_CACHE(扫描缓存;launcher net.c 同路径) */
|
||||||
static const char *g_ip = "ip"; /* MEOW_IP */
|
static const char *g_ip = "ip"; /* MEOW_IP */
|
||||||
static const char *g_wifi_if = "wlan0"; /* MEOW_WIFI_IF */
|
static const char *g_wifi_if = "wlan0"; /* MEOW_WIFI_IF */
|
||||||
|
|
||||||
@@ -353,6 +356,7 @@ typedef struct {
|
|||||||
int powered;
|
int powered;
|
||||||
char address[32];
|
char address[32];
|
||||||
int paired_n; /* -1=null */
|
int paired_n; /* -1=null */
|
||||||
|
int connected_n; /* 已连接(内核 ACL 权威)计数;-1=null */
|
||||||
} bt_t;
|
} bt_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@@ -603,10 +607,12 @@ static void wifi_status(wifi_t *w)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- 蓝牙:hciconfig/bluetoothctl 包装 ---------------- */
|
/* ---------------- 蓝牙:hciconfig/bluetoothctl 包装 ---------------- */
|
||||||
|
static int bt_acl_macs(char macs[][24], int max); /* 前向声明(bt_status 早于其定义) */
|
||||||
static void bt_status(bt_t *b)
|
static void bt_status(bt_t *b)
|
||||||
{
|
{
|
||||||
memset(b, 0, sizeof *b);
|
memset(b, 0, sizeof *b);
|
||||||
b->paired_n = -1;
|
b->paired_n = -1;
|
||||||
|
b->connected_n = -1;
|
||||||
char p[192];
|
char p[192];
|
||||||
snprintf(p, sizeof p, "%s/hci0", g_bt_dir);
|
snprintf(p, sizeof p, "%s/hci0", g_bt_dir);
|
||||||
if (access(p, F_OK) == 0) b->available = 1;
|
if (access(p, F_OK) == 0) b->available = 1;
|
||||||
@@ -670,6 +676,286 @@ static void bt_status(bt_t *b)
|
|||||||
}
|
}
|
||||||
b->paired_n = n;
|
b->paired_n = n;
|
||||||
}
|
}
|
||||||
|
/* 已连接数:内核 ACL(hcitool con)权威;快速,不受 bluetoothctl 过滤/慢启动影响。 */
|
||||||
|
{
|
||||||
|
char macs[8][24];
|
||||||
|
b->connected_n = bt_acl_macs(macs, 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* P1:蓝牙权威收口(BLUETOOTH_FULL_OVERHAUL_PLAN §六/§八 P1)。
|
||||||
|
* 权威路径设计(修复「已连接设备显示不全」根因):
|
||||||
|
* - 已连接 = 内核 ACL(hcitool con,快速/权威/不受 bluetoothctl 过滤影响),
|
||||||
|
* 名/配对来自 bluetoothctl `paired-devices`/`devices`(蓝虫子缓存)——不再依赖
|
||||||
|
* bluetoothctl `devices Connected`(慢 + 只反映 agent 缓存视图,易截断)。
|
||||||
|
* - A2DP 详情 = bluealsa-cli a2dp list(codec/采样率/声道/位深/传输状态/角色)。
|
||||||
|
* - 扫描 = netmgrd 统一生命周期(scan on 才扫、off/超时收尾;缓存 /tmp/meow_bt_scan.txt)。
|
||||||
|
* 输出均为文本(m_net_cmd_result_t.text),launcher 只做客户端解析,不再 bluetoothctl popen。
|
||||||
|
* ================================================================ */
|
||||||
|
static int bt_hex(int c)
|
||||||
|
{
|
||||||
|
if (c >= '0' && c <= '9') return c - '0';
|
||||||
|
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||||
|
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
static int bt_mac_eq(const char *a, const char *b)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 17; i++) {
|
||||||
|
if (bt_hex((unsigned char)a[i]) != bt_hex((unsigned char)b[i])) return 0;
|
||||||
|
if (i % 3 == 2 && a[i] != b[i]) return 0;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
/* 在行内找第一个形如 XX:XX:XX:XX:XX:XX 的 MAC,返其指针(否则 NULL)。 */
|
||||||
|
static const char *bt_find_mac(const char *s)
|
||||||
|
{
|
||||||
|
while (*s) {
|
||||||
|
int ok = 1;
|
||||||
|
for (int i = 0; i < 17; i++) {
|
||||||
|
int exp_colon = (i % 3 == 2);
|
||||||
|
if (exp_colon) { if (s[i] != ':') { ok = 0; break; } }
|
||||||
|
else if (bt_hex((unsigned char)s[i]) < 0) { ok = 0; break; }
|
||||||
|
}
|
||||||
|
if (ok) return s;
|
||||||
|
s++;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内核 ACL 连接 MAC(hcitool con)。返回数(≤max)。 */
|
||||||
|
static int bt_acl_macs(char macs[][24], int max)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
int n = 0;
|
||||||
|
char *line = buf;
|
||||||
|
while (line && *line && n < max) {
|
||||||
|
char *nl = strchr(line, '\n'); if (nl) *nl = 0;
|
||||||
|
const char *m = bt_find_mac(line);
|
||||||
|
if (m) { memcpy(macs[n], m, 17); macs[n][17] = 0; n++; }
|
||||||
|
line = nl ? nl + 1 : NULL;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 设备名解析(bluetoothctl `paired-devices`)+`devices` 两次兜底;命中返 1。 */
|
||||||
|
static int bt_name_for_mac(const char *mac, char *name, int nlen)
|
||||||
|
{
|
||||||
|
if (name && nlen > 0) name[0] = 0;
|
||||||
|
static const char *subs[] = { "paired-devices", "devices" };
|
||||||
|
for (int s = 0; s < 2; s++) {
|
||||||
|
char cmd[176];
|
||||||
|
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s %s 2>/dev/null",
|
||||||
|
g_bluetoothctl, subs[s]);
|
||||||
|
char buf[1024];
|
||||||
|
if (run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd) <= 0) continue;
|
||||||
|
char *line = buf;
|
||||||
|
while (line && *line) {
|
||||||
|
char *nl = strchr(line, '\n'); if (nl) *nl = 0;
|
||||||
|
char *q = line; while (*q == ' ' || *q == '\t') q++;
|
||||||
|
if (!strncmp(q, "Device ", 7)) {
|
||||||
|
q += 7;
|
||||||
|
char *m = q; while (*m && *m != ' ' && *m != '\t' && *m != '\n' && *m != '\r') m++;
|
||||||
|
int ml = (int)(m - q);
|
||||||
|
if (ml == 17 && bt_mac_eq(q, mac)) {
|
||||||
|
char *nm = m; while (*nm == ' ' || *nm == '\t') nm++;
|
||||||
|
char *e = nm; while (*e && *e != '\n' && *e != '\r') e++;
|
||||||
|
*e = 0;
|
||||||
|
int l = (int)(e - nm); if (l > nlen - 1) l = nlen - 1;
|
||||||
|
if (l > 0) memcpy(name, nm, (size_t)l), name[l] = 0;
|
||||||
|
else { memcpy(name, mac, 17); name[17] = 0; }
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
line = nl ? nl + 1 : NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 已连接设备文本(权威=内核 ACL;名经 paired/devices 兜底)。行格式 "Device <MAC> <Name>"。 */
|
||||||
|
static void bt_connected_text(char *buf, int cap)
|
||||||
|
{
|
||||||
|
int off = 0;
|
||||||
|
char macs[8][24];
|
||||||
|
int n = bt_acl_macs(macs, 8);
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
char name[64] = "";
|
||||||
|
bt_name_for_mac(macs[i], name, sizeof name);
|
||||||
|
int l = snprintf(buf + off, (size_t)(cap - off), "Device %s %s\n", macs[i], name);
|
||||||
|
if (l <= 0 || off + l >= cap) break;
|
||||||
|
off += l;
|
||||||
|
}
|
||||||
|
buf[off] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A2DP 传输详情:bluealsa-cli a2dp list → 每端点一行
|
||||||
|
* "A2DP <mac> codec=<..> rate=<..> channels=<n> bits=<n> acquired=<0|1> role=<src|snk|->"
|
||||||
|
* 解析宽松:字段缺失留空/0;整串不可辨认时退回原文。 */
|
||||||
|
static void bt_a2dp_text(char *buf, int cap)
|
||||||
|
{
|
||||||
|
char cmd[176];
|
||||||
|
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s a2dp list 2>/dev/null", g_bluealsa_cli);
|
||||||
|
char raw[MEOW_IPC_MAX - 16];
|
||||||
|
if (run_cmd(raw, sizeof raw, RUNCMD_TMO_MS, cmd) <= 0) {
|
||||||
|
snprintf(buf, (size_t)cap, "A2DP none\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* 端点累积器 */
|
||||||
|
char emac[24] = "", role[4] = "-";
|
||||||
|
char codec[16] = "", rate[16] = "", chn[8] = "", bits[8] = "";
|
||||||
|
int acquired = -1, has_mac = 0, wrote = 0, off = 0;
|
||||||
|
char *line = raw;
|
||||||
|
while (line && *line) {
|
||||||
|
char *nl = strchr(line, '\n'); if (nl) *nl = 0;
|
||||||
|
char *p = line; while (*p == ' ' || *p == '\t') p++;
|
||||||
|
if (*p == '/') { /* 新端点:/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc[:] */
|
||||||
|
if (has_mac && wrote) {
|
||||||
|
int l = snprintf(buf + off, (size_t)(cap - off),
|
||||||
|
"A2DP %s codec=%s rate=%s channels=%s bits=%s acquired=%d role=%s\n",
|
||||||
|
emac, codec[0] ? codec : "-", rate[0] ? rate : "-",
|
||||||
|
chn[0] ? chn : "-", bits[0] ? bits : "-",
|
||||||
|
acquired >= 0 ? acquired : 0, role);
|
||||||
|
if (l > 0 && off + l < cap) off += l;
|
||||||
|
}
|
||||||
|
/* 重置并为新端点解析 MAC/role */
|
||||||
|
emac[0] = role[0] = codec[0] = rate[0] = chn[0] = bits[0] = 0;
|
||||||
|
snprintf(role, sizeof role, "%s", "-");
|
||||||
|
acquired = -1; has_mac = 0; wrote = 0;
|
||||||
|
const char *dv = strstr(p, "dev_");
|
||||||
|
if (dv) {
|
||||||
|
dv += 4;
|
||||||
|
int j = 0;
|
||||||
|
const char *q = dv;
|
||||||
|
while (*q && j < 17) {
|
||||||
|
if (*q == '_') { if (j == 2 || j == 5 || j == 8 || j == 11 || j == 14) emac[j] = ':'; else break; }
|
||||||
|
else if (bt_hex((unsigned char)*q) >= 0) emac[j] = *q;
|
||||||
|
else break;
|
||||||
|
j++; q++;
|
||||||
|
}
|
||||||
|
emac[17] = 0;
|
||||||
|
has_mac = (j == 17);
|
||||||
|
if (strstr(p, "a2dpsrc")) snprintf(role, sizeof role, "%s", "src");
|
||||||
|
else if (strstr(p, "a2dpsnk")) snprintf(role, sizeof role, "%s", "snk");
|
||||||
|
} else has_mac = 0;
|
||||||
|
wrote = 1;
|
||||||
|
line = nl ? nl + 1 : NULL;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
/* 字段行:Codec:/Sampling:/Channels:/Bits:/State:/Acquired: */
|
||||||
|
{ const char *v = strstr(p, "Codec:"); if (v && codec[0] == 0) {
|
||||||
|
v += 6; while (*v == ' ' || *v == '\t') v++;
|
||||||
|
{ const char *e = v; while (*e && *e != '\n' && *e != '\r') e++;
|
||||||
|
while (e > v && (e[-1] == ' ' || e[-1] == '\t')) e--;
|
||||||
|
int l = (int)(e - v); if (l > 15) l = 15; memcpy(codec, v, (size_t)l); codec[l] = 0; } } }
|
||||||
|
{ const char *v = strstr(p, "Sampling"); if (v && rate[0] == 0) {
|
||||||
|
v = strchr(v, ':'); if (v) { v++; while (*v == ' ' || *v == '\t') v++;
|
||||||
|
{ const char *e = v; while (*e && *e != '\n' && *e != '\r') e++;
|
||||||
|
while (e > v && (e[-1] == ' ' || e[-1] == '\t')) e--;
|
||||||
|
int l = (int)(e - v); if (l > 15) l = 15; memcpy(rate, v, (size_t)l); rate[l] = 0; } } } }
|
||||||
|
{ const char *v = strstr(p, "Channels:"); if (v && chn[0] == 0) {
|
||||||
|
v += 9; while (*v == ' ' || *v == '\t') v++;
|
||||||
|
{ const char *e = v; while (*e && *e != '\n' && *e != '\r') e++;
|
||||||
|
while (e > v && (e[-1] == ' ' || e[-1] == '\t')) e--;
|
||||||
|
int l = (int)(e - v); if (l > 7) l = 7; memcpy(chn, v, (size_t)l); chn[l] = 0; } } }
|
||||||
|
{ const char *v = strstr(p, "Bits"); if (v && bits[0] == 0) {
|
||||||
|
v = strchr(v, ':'); if (v) { v++; while (*v == ' ' || *v == '\t') v++;
|
||||||
|
{ const char *e = v; while (*e && *e != '\n' && *e != '\r') e++;
|
||||||
|
while (e > v && (e[-1] == ' ' || e[-1] == '\t')) e--;
|
||||||
|
int l = (int)(e - v); if (l > 7) l = 7; memcpy(bits, v, (size_t)l); bits[l] = 0; } } } }
|
||||||
|
if (strstr(p, "State:") || strstr(p, "State :") || strstr(p, "Acquired")) {
|
||||||
|
if (strstr(p, "RUNNING") || strstr(p, "ACTIVE") || strstr(p, "acquired") || strstr(p, "yes"))
|
||||||
|
acquired = 1;
|
||||||
|
}
|
||||||
|
line = nl ? nl + 1 : NULL;
|
||||||
|
}
|
||||||
|
/* 尾端点收尾 */
|
||||||
|
if (has_mac && wrote) {
|
||||||
|
int l = snprintf(buf + off, (size_t)(cap - off),
|
||||||
|
"A2DP %s codec=%s rate=%s channels=%s bits=%s acquired=%d role=%s\n",
|
||||||
|
emac, codec[0] ? codec : "-", rate[0] ? rate : "-",
|
||||||
|
chn[0] ? chn : "-", bits[0] ? bits : "-",
|
||||||
|
acquired >= 0 ? acquired : 0, role);
|
||||||
|
if (l > 0 && off + l < cap) off += l;
|
||||||
|
}
|
||||||
|
if (off == 0) snprintf(buf, (size_t)cap, "A2DP none\n");
|
||||||
|
else buf[off] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A2DP 摘要(首个带 codec/rate 的端点)给 overview JSON;失败置空。 */
|
||||||
|
static void bt_a2dp_summary(char *codec, int clen, char *rate, int rlen)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
char *line = buf;
|
||||||
|
while (line && *line) {
|
||||||
|
char *nl = strchr(line, '\n'); if (nl) *nl = 0;
|
||||||
|
if (!strncmp(line, "A2DP ", 5)) {
|
||||||
|
const char *c = strstr(line, " codec=");
|
||||||
|
const char *r = strstr(line, " rate=");
|
||||||
|
if (c && codec) { c += 7; const char *e = c; while (*e && *e != ' ' && *e != '\n') e++;
|
||||||
|
int l = (int)(e - c); if (l > clen - 1) l = clen - 1; memcpy(codec, c, (size_t)l); codec[l] = 0; }
|
||||||
|
if (r && rate) { r += 6; const char *e = r; while (*e && *e != ' ' && *e != '\n') e++;
|
||||||
|
int l = (int)(e - r); if (l > rlen - 1) l = rlen - 1; memcpy(rate, r, (size_t)l); rate[l] = 0; }
|
||||||
|
if ((codec && codec[0]) || (rate && rate[0])) return;
|
||||||
|
}
|
||||||
|
line = nl ? nl + 1 : NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 附近设备文本:netmgrd 权威扫描缓存(/tmp/meow_bt_scan.txt),行 "Device <MAC> <Name>"(去重)。 */
|
||||||
|
static void bt_devices_text(char *buf, int cap)
|
||||||
|
{
|
||||||
|
int off = 0;
|
||||||
|
FILE *f = fopen(g_bt_scan_cache, "r");
|
||||||
|
if (!f) { buf[0] = 0; return; }
|
||||||
|
char seen[16][24]; int n_seen = 0;
|
||||||
|
char line[256];
|
||||||
|
while (fgets(line, sizeof line, f) && n_seen < 16) {
|
||||||
|
char *p = line; while (*p == ' ' || *p == '\t') p++;
|
||||||
|
const char *dv = NULL;
|
||||||
|
if (!strncmp(p, "Device ", 7)) dv = p + 7;
|
||||||
|
else if (!strncmp(p, "[NEW] Device ", 13)) dv = p + 13;
|
||||||
|
if (!dv) continue;
|
||||||
|
const char *mac = bt_find_mac(dv);
|
||||||
|
if (!mac) continue;
|
||||||
|
int dup = 0;
|
||||||
|
for (int i = 0; i < n_seen; i++) if (bt_mac_eq(seen[i], mac)) { dup = 1; break; }
|
||||||
|
if (dup) continue;
|
||||||
|
memcpy(seen[n_seen], mac, 17); seen[n_seen][17] = 0; n_seen++;
|
||||||
|
char *nm = (char *)(mac + 17); while (*nm == ' ' || *nm == '\t') nm++;
|
||||||
|
char *e = nm; while (*e && *e != '\n' && *e != '\r') e++;
|
||||||
|
*e = 0;
|
||||||
|
int l = snprintf(buf + off, (size_t)(cap - off), "Device %s %s\n", mac, nm);
|
||||||
|
if (l <= 0 || off + l >= cap) break;
|
||||||
|
off += l;
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
buf[off] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 扫描生命周期(§七:进「附近查找」才 scan on;退页/超时 off)。默认 7s 节流。 */
|
||||||
|
static long g_bt_scan_ms = 0;
|
||||||
|
static void bt_scan_on(void)
|
||||||
|
{
|
||||||
|
long t = mono_now();
|
||||||
|
if (g_bt_scan_ms && t - g_bt_scan_ms < 7000) return;
|
||||||
|
g_bt_scan_ms = t;
|
||||||
|
char cmd[256];
|
||||||
|
snprintf(cmd, sizeof cmd,
|
||||||
|
"(sleep 1; echo 'scan on'; sleep 4; echo 'scan off'; echo 'devices'; echo 'quit') | "
|
||||||
|
"bluetoothctl > '%s' 2>&1 &", g_bt_scan_cache);
|
||||||
|
system(cmd);
|
||||||
|
}
|
||||||
|
static void bt_scan_off(void)
|
||||||
|
{
|
||||||
|
system("PATH=/bin:/sbin:/usr/bin:/usr/sbin bluetoothctl scan off 2>/dev/null");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- rfkill:统一软开关 ---------------- */
|
/* ---------------- rfkill:统一软开关 ---------------- */
|
||||||
@@ -974,6 +1260,11 @@ static void build_status_json(char *out, size_t cap)
|
|||||||
bt.powered ? "true" : "false");
|
bt.powered ? "true" : "false");
|
||||||
jadd_str_or_null(&w, "address", bt.address);
|
jadd_str_or_null(&w, "address", bt.address);
|
||||||
jadd_int_or_null(&w, "paired", bt.paired_n);
|
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); }
|
||||||
jadd(&w, "\"dns\":[");
|
jadd(&w, "\"dns\":[");
|
||||||
{
|
{
|
||||||
char ns[DNS_MAX][40];
|
char ns[DNS_MAX][40];
|
||||||
@@ -2326,8 +2617,9 @@ static int wifi_cmd_is_write(const char *cmd, const char *arg)
|
|||||||
}
|
}
|
||||||
static int bt_cmd_is_write(const char *cmd)
|
static int bt_cmd_is_write(const char *cmd)
|
||||||
{
|
{
|
||||||
if (!strcmp(cmd, "status") || !strcmp(cmd, "paired")) return 0;
|
if (!strcmp(cmd, "status") || !strcmp(cmd, "paired") ||
|
||||||
return 1; /* on/off */
|
!strcmp(cmd, "connected") || !strcmp(cmd, "a2dp") || !strcmp(cmd, "devices")) return 0;
|
||||||
|
return 1; /* on/off/connect/disconnect/pair/unpair/scan */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 写级授权决策(纯函数,宿主自测用;handle_conn 复用):读级任意;写级仅系统身份 + MeowOS 模式。
|
/* 写级授权决策(纯函数,宿主自测用;handle_conn 复用):读级任意;写级仅系统身份 + MeowOS 模式。
|
||||||
@@ -2597,14 +2889,13 @@ static void bt_onoff(const char *cmd, ctl_result_t *r)
|
|||||||
|
|
||||||
static void bt_ctl_execute(const char *cmd, const char *arg, ctl_result_t *r)
|
static void bt_ctl_execute(const char *cmd, const char *arg, ctl_result_t *r)
|
||||||
{
|
{
|
||||||
(void)arg;
|
|
||||||
ctl_reset(r, MEOW_OK);
|
ctl_reset(r, MEOW_OK);
|
||||||
if (!strcmp(cmd, "status")) {
|
if (!strcmp(cmd, "status")) {
|
||||||
bt_t b;
|
bt_t b;
|
||||||
bt_status(&b);
|
bt_status(&b);
|
||||||
char msg[192];
|
char msg[192];
|
||||||
snprintf(msg, sizeof msg, "bt available=%d powered=%d address=%s paired=%d",
|
snprintf(msg, sizeof msg, "bt available=%d powered=%d address=%s paired=%d connected=%d",
|
||||||
b.available, b.powered, b.address[0] ? b.address : "-", b.paired_n);
|
b.available, b.powered, b.address[0] ? b.address : "-", b.paired_n, b.connected_n);
|
||||||
ctl_sets(r, MEOW_OK, msg);
|
ctl_sets(r, MEOW_OK, msg);
|
||||||
} else if (!strcmp(cmd, "on") || !strcmp(cmd, "off")) {
|
} else if (!strcmp(cmd, "on") || !strcmp(cmd, "off")) {
|
||||||
bt_onoff(cmd, r);
|
bt_onoff(cmd, r);
|
||||||
@@ -2615,6 +2906,33 @@ static void bt_ctl_execute(const char *cmd, const char *arg, ctl_result_t *r)
|
|||||||
int n = run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd2);
|
int n = run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd2);
|
||||||
if (n <= 0) ctl_sets(r, MEOW_ERR_NODEV, "bt paired: 无输出 (bluetoothctl 不可用/未开)");
|
if (n <= 0) ctl_sets(r, MEOW_ERR_NODEV, "bt paired: 无输出 (bluetoothctl 不可用/未开)");
|
||||||
else ctl_sets(r, MEOW_OK, buf);
|
else ctl_sets(r, MEOW_OK, buf);
|
||||||
|
} else if (!strcmp(cmd, "connected")) {
|
||||||
|
char buf[MEOW_IPC_MAX - 16];
|
||||||
|
bt_connected_text(buf, sizeof buf);
|
||||||
|
ctl_sets(r, MEOW_OK, buf);
|
||||||
|
} else if (!strcmp(cmd, "a2dp")) {
|
||||||
|
char buf[MEOW_IPC_MAX - 16];
|
||||||
|
bt_a2dp_text(buf, sizeof buf);
|
||||||
|
ctl_sets(r, MEOW_OK, buf);
|
||||||
|
} else if (!strcmp(cmd, "devices")) {
|
||||||
|
char buf[MEOW_IPC_MAX - 16];
|
||||||
|
bt_devices_text(buf, sizeof buf);
|
||||||
|
ctl_sets(r, MEOW_OK, buf);
|
||||||
|
} else if (!strcmp(cmd, "connect") || !strcmp(cmd, "disconnect") ||
|
||||||
|
!strcmp(cmd, "pair") || !strcmp(cmd, "unpair")) {
|
||||||
|
if (!arg || !arg[0]) { char m[48]; snprintf(m, sizeof m, "bt %s: 需 <mac>", cmd); ctl_sets(r, MEOW_ERR_INVAL, m); return; }
|
||||||
|
const char *sub = (!strcmp(cmd, "unpair")) ? "remove" : cmd; /* bluetoothctl 取消配对=remove */
|
||||||
|
char cmd2[192];
|
||||||
|
snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s %s %s 2>/dev/null",
|
||||||
|
g_bluetoothctl, sub, arg);
|
||||||
|
char buf[256];
|
||||||
|
int n = run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd2);
|
||||||
|
if (n >= 0) ctl_sets(r, MEOW_OK, buf[0] ? buf : "bt cmd: 已下发");
|
||||||
|
else ctl_sets(r, MEOW_ERR_NODEV, "bt cmd: 后端执行失败");
|
||||||
|
} else if (!strcmp(cmd, "scan")) {
|
||||||
|
if (!arg || !strcmp(arg, "on")) { bt_scan_on(); ctl_sets(r, MEOW_OK, "bt scan on: 后台节流扫描已触发"); }
|
||||||
|
else if (!strcmp(arg, "off")) { bt_scan_off(); ctl_sets(r, MEOW_OK, "bt scan off: 扫描已停"); }
|
||||||
|
else ctl_sets(r, MEOW_ERR_INVAL, "bt scan: 用法 on|off");
|
||||||
} else {
|
} else {
|
||||||
ctl_sets(r, MEOW_ERR_INVAL, "bt: 未知子命令");
|
ctl_sets(r, MEOW_ERR_INVAL, "bt: 未知子命令");
|
||||||
}
|
}
|
||||||
@@ -3204,9 +3522,15 @@ static int cli_status(void)
|
|||||||
int bpow = strstr(obj, "\"powered\":true") != NULL;
|
int bpow = strstr(obj, "\"powered\":true") != NULL;
|
||||||
jstr(obj, "address", baddr, sizeof baddr);
|
jstr(obj, "address", baddr, sizeof baddr);
|
||||||
int bpaired = -1;
|
int bpaired = -1;
|
||||||
|
int bconn = -1;
|
||||||
jint(obj, "paired", &bpaired);
|
jint(obj, "paired", &bpaired);
|
||||||
printf("bt available=%d powered=%d address=%s paired=%d\n",
|
jint(obj, "connected", &bconn);
|
||||||
bavail, bpow, baddr[0] ? baddr : "-", bpaired);
|
char bcodec[24] = "", brate[24] = "";
|
||||||
|
jstr(obj, "a2dp_codec", bcodec, sizeof bcodec);
|
||||||
|
jstr(obj, "a2dp_rate", brate, sizeof brate);
|
||||||
|
printf("bt available=%d powered=%d address=%s paired=%d connected=%d a2dp=%s@%s\n",
|
||||||
|
bavail, bpow, baddr[0] ? baddr : "-", bpaired, bconn,
|
||||||
|
bcodec[0] ? bcodec : "-", brate[0] ? brate : "-");
|
||||||
}
|
}
|
||||||
/* rfkill 数组 */
|
/* rfkill 数组 */
|
||||||
{
|
{
|
||||||
@@ -3381,7 +3705,8 @@ static int cli_policy(int op, const char *entity, const char *arg)
|
|||||||
static const char *usage(void)
|
static const char *usage(void)
|
||||||
{
|
{
|
||||||
return "status | iface [name] | wifi scan|networks|connect <ssid>[,psk]|disconnect|on|off"
|
return "status | iface [name] | wifi scan|networks|connect <ssid>[,psk]|disconnect|on|off"
|
||||||
" | bt status|on|off|paired | rfkill [<type> <on|off>] | udhcpc renew|release [if]"
|
" | bt status|on|off|paired|connected|a2dp|devices|scan on|off|connect|disconnect|pair|unpair <mac>"
|
||||||
|
" | rfkill [<type> <on|off>] | udhcpc renew|release [if]"
|
||||||
" | apps | conns [entity] | top | dns status|stats|flush"
|
" | apps | conns [entity] | top | dns status|stats|flush"
|
||||||
" | policy show [entity] | policy set <entity> allow|block | policy domain <entity> +host|-host | policy del <entity>"
|
" | policy show [entity] | policy set <entity> allow|block | policy domain <entity> +host|-host | policy del <entity>"
|
||||||
" | help";
|
" | help";
|
||||||
@@ -3444,6 +3769,15 @@ static int cli_run(int n, char **v)
|
|||||||
}
|
}
|
||||||
if (!strcmp(cmd, "bt")) {
|
if (!strcmp(cmd, "bt")) {
|
||||||
if (!arg) { fprintf(stderr, "bt 子命令缺失(%s)\n", usage()); return 1; }
|
if (!arg) { fprintf(stderr, "bt 子命令缺失(%s)\n", usage()); return 1; }
|
||||||
|
if (!strcmp(arg, "connect") || !strcmp(arg, "disconnect") ||
|
||||||
|
!strcmp(arg, "pair") || !strcmp(arg, "unpair")) {
|
||||||
|
if (!arg2) { fprintf(stderr, "bt %s 需 <mac>\n", arg); return 1; }
|
||||||
|
return cli_cmd_result(MSG_NET_BT_CTL, arg, arg2);
|
||||||
|
}
|
||||||
|
if (!strcmp(arg, "scan")) {
|
||||||
|
if (arg2 && !strcmp(arg2, "off")) return cli_cmd_result(MSG_NET_BT_CTL, "scan", "off");
|
||||||
|
return cli_cmd_result(MSG_NET_BT_CTL, "scan", "on");
|
||||||
|
}
|
||||||
return cli_cmd_result(MSG_NET_BT_CTL, arg, NULL);
|
return cli_cmd_result(MSG_NET_BT_CTL, arg, NULL);
|
||||||
}
|
}
|
||||||
if (!strcmp(cmd, "rfkill")) {
|
if (!strcmp(cmd, "rfkill")) {
|
||||||
@@ -3480,6 +3814,9 @@ int main(int argc, char **argv)
|
|||||||
if ((e = getenv("MEOW_WPA_CLI")) && e[0]) g_wpa_cli = e;
|
if ((e = getenv("MEOW_WPA_CLI")) && e[0]) g_wpa_cli = e;
|
||||||
if ((e = getenv("MEOW_HCICONFIG")) && e[0]) g_hciconfig = e;
|
if ((e = getenv("MEOW_HCICONFIG")) && e[0]) g_hciconfig = e;
|
||||||
if ((e = getenv("MEOW_BLUETOOTHCTL")) && e[0]) g_bluetoothctl = e;
|
if ((e = getenv("MEOW_BLUETOOTHCTL")) && e[0]) g_bluetoothctl = e;
|
||||||
|
if ((e = getenv("MEOW_HCITOOL")) && e[0]) g_hcitool = e;
|
||||||
|
if ((e = getenv("MEOW_BLUEALSA_CLI")) && e[0]) g_bluealsa_cli = e;
|
||||||
|
if ((e = getenv("MEOW_BT_SCAN_CACHE")) && e[0]) g_bt_scan_cache = e;
|
||||||
if ((e = getenv("MEOW_IP")) && e[0]) g_ip = e;
|
if ((e = getenv("MEOW_IP")) && e[0]) g_ip = e;
|
||||||
if ((e = getenv("MEOW_WIFI_IF")) && e[0]) g_wifi_if = e;
|
if ((e = getenv("MEOW_WIFI_IF")) && e[0]) g_wifi_if = e;
|
||||||
if ((e = getenv("MEOW_NET_SOCK")) && e[0]) g_sock = e;
|
if ((e = getenv("MEOW_NET_SOCK")) && e[0]) g_sock = e;
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
/* test_bt_ctl.c — meow_netmgrd 蓝牙 P1 权威收口 纯逻辑宿主自测(本程序非生产代码)。
|
||||||
|
*
|
||||||
|
* 通过 #include 生产源文件(meow_netmgrd.c,用 #define main 改名避免入口冲突),同一 TU 直接访问
|
||||||
|
* FILE-STATIC 函数/全局:bt_cmd_is_write / bt_write_authorize / bt_ctl_execute(connect/disconnect/
|
||||||
|
* pair/unpair/connected/a2dp/devices/status)。工具经桩脚本(g_bluetoothctl/g_hcitool/g_bluealsa_cli/
|
||||||
|
* g_bt_scan_cache 指向 $T/桩)覆盖,不依赖真实蓝牙栈。
|
||||||
|
*
|
||||||
|
* 用法:cc -Wall -Wextra -I../meow_launcherd test_bt_ctl.c -o test_bt_ctl && ./test_bt_ctl
|
||||||
|
*/
|
||||||
|
#define main meow_netmgrd_prod_main_unused
|
||||||
|
#include "meow_netmgrd.c"
|
||||||
|
#undef main
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
static int g_fail = 0;
|
||||||
|
#define CHECK(cond) do { if (!(cond)) { printf(" FAIL: %s (line %d)\n", #cond, __LINE__); g_fail++; } \
|
||||||
|
else { printf(" ok : %s\n", #cond); } } while (0)
|
||||||
|
|
||||||
|
static void wf(const char *p, const char *s)
|
||||||
|
{
|
||||||
|
FILE *f = fopen(p, "w");
|
||||||
|
if (f) { fputs(s, f); fclose(f); }
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
static const char *T = "/tmp/nmg_bt";
|
||||||
|
char p[192];
|
||||||
|
mkdir(T, 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, "00:1A:7D:DA:71:13\n");
|
||||||
|
snprintf(p, sizeof p, "%s/bt/hci0/power", T); wf(p, "on\n"); }
|
||||||
|
|
||||||
|
/* 桩:bluetoothctl(paired/devices/connect/remove)、hcitool con、bluealsa-cli a2dp、扫描缓存 */
|
||||||
|
snprintf(p, sizeof p, "%s/bluetoothctl", T); wf(p,
|
||||||
|
"#!/bin/sh\ncase \"$1\" in\n"
|
||||||
|
" paired|paired-devices|devices) echo 'Device 33:DE:2E:21:AD:30 Pudding'; echo 'Device 11:22:33:44:55:66 Speaker';;\n"
|
||||||
|
" connect|disconnect|pair|remove) echo 'Controller 00:1A:7D:DA:71:13'; echo 'Device 33:DE:2E:21:AD:30 Connected: yes';;\n"
|
||||||
|
"esac\nexit 0\n"); chmod(p, 0755);
|
||||||
|
snprintf(p, sizeof p, "%s/hcitool", T); wf(p,
|
||||||
|
"#!/bin/sh\necho '\t< ACL 33:DE:2E:21:AD:30 handle 12 state 1 lm MASTER'\n"
|
||||||
|
"echo '\t< ACL 11:22:33:44:55:66 handle 13 state 1 lm SLAVE'\nexit 0\n"); chmod(p, 0755);
|
||||||
|
snprintf(p, sizeof p, "%s/bluealsa-cli", T); wf(p,
|
||||||
|
"#!/bin/sh\nif [ \"${1:-}\" = \"a2dp\" ]; then\n"
|
||||||
|
"echo '/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc:'\n"
|
||||||
|
"echo '\tCodec: SBC'\necho '\tSampling: 44100 Hz'\necho '\tChannels: 2'\n"
|
||||||
|
"echo '\tBits per Sample: 16'\necho '\tState: RUNNING'\necho '\tAcquired: yes'\nfi\nexit 0\n"); chmod(p, 0755);
|
||||||
|
snprintf(p, sizeof p, "%s/bt_scan.txt", T); wf(p,
|
||||||
|
"[NEW] Device 33:DE:2E:21:AD:30 Pudding\n[NEW] Device AA:BB:CC:DD:EE:FF Earbuds\n");
|
||||||
|
|
||||||
|
g_bluetoothctl = ""; { char x[160]; snprintf(x, sizeof x, "%s/bluetoothctl", T); static char s[160];
|
||||||
|
snprintf(s, sizeof s, "%s", x); g_bluetoothctl = s; }
|
||||||
|
g_hcitool = ""; { char x[160]; snprintf(x, sizeof x, "%s/hcitool", T); static char s[160];
|
||||||
|
snprintf(s, sizeof s, "%s", x); g_hcitool = s; }
|
||||||
|
g_bluealsa_cli = ""; { char x[160]; snprintf(x, sizeof x, "%s/bluealsa-cli", T); static char s[160];
|
||||||
|
snprintf(s, sizeof s, "%s", x); g_bluealsa_cli = s; }
|
||||||
|
g_bt_scan_cache = ""; { char x[160]; snprintf(x, sizeof x, "%s/bt_scan.txt", T); static char s[160];
|
||||||
|
snprintf(s, sizeof s, "%s", x); g_bt_scan_cache = s; }
|
||||||
|
g_bt_dir[0] = 0; snprintf(g_bt_dir, sizeof g_bt_dir, "%s/bt", T);
|
||||||
|
|
||||||
|
ctl_result_t r;
|
||||||
|
|
||||||
|
printf("== bt_cmd_is_write 分类(读/写级) ==\n");
|
||||||
|
CHECK(bt_cmd_is_write("status") == 0);
|
||||||
|
CHECK(bt_cmd_is_write("paired") == 0);
|
||||||
|
CHECK(bt_cmd_is_write("connected") == 0);
|
||||||
|
CHECK(bt_cmd_is_write("a2dp") == 0);
|
||||||
|
CHECK(bt_cmd_is_write("devices") == 0);
|
||||||
|
CHECK(bt_cmd_is_write("on") == 1);
|
||||||
|
CHECK(bt_cmd_is_write("off") == 1);
|
||||||
|
CHECK(bt_cmd_is_write("connect") == 1);
|
||||||
|
CHECK(bt_cmd_is_write("disconnect") == 1);
|
||||||
|
CHECK(bt_cmd_is_write("pair") == 1);
|
||||||
|
CHECK(bt_cmd_is_write("unpair") == 1);
|
||||||
|
CHECK(bt_cmd_is_write("scan") == 1);
|
||||||
|
|
||||||
|
printf("== bt_ctl_execute 写级(connect/disconnect/pair/unpair,经桩 bluetoothctl) ==\n");
|
||||||
|
bt_ctl_execute("connect", "33:DE:2E:21:AD:30", &r);
|
||||||
|
CHECK(r.code == MEOW_OK);
|
||||||
|
CHECK(strstr(r.text, "Connected") != NULL);
|
||||||
|
bt_ctl_execute("disconnect", "33:DE:2E:21:AD:30", &r);
|
||||||
|
CHECK(r.code == MEOW_OK);
|
||||||
|
bt_ctl_execute("pair", "33:DE:2E:21:AD:30", &r);
|
||||||
|
CHECK(r.code == MEOW_OK);
|
||||||
|
bt_ctl_execute("unpair", "33:DE:2E:21:AD:30", &r);
|
||||||
|
CHECK(r.code == MEOW_OK);
|
||||||
|
CHECK(strstr(r.text, "Connected") != NULL); /* remove 桩回 Connected: yes */
|
||||||
|
bt_ctl_execute("connect", "", &r);
|
||||||
|
CHECK(r.code == MEOW_ERR_INVAL); /* 缺 mac → INVAL */
|
||||||
|
bt_ctl_execute("scan", NULL, &r);
|
||||||
|
CHECK(r.code == MEOW_OK); /* scan on(默认) */
|
||||||
|
CHECK(strstr(r.text, "scan on") != NULL);
|
||||||
|
bt_ctl_execute("scan", "off", &r);
|
||||||
|
CHECK(r.code == MEOW_OK);
|
||||||
|
|
||||||
|
printf("== bt_ctl_execute 读级(connected/a2dp/devices/status,权威) ==\n");
|
||||||
|
bt_ctl_execute("connected", NULL, &r);
|
||||||
|
CHECK(r.code == MEOW_OK);
|
||||||
|
CHECK(strstr(r.text, "Device 33:DE:2E:21:AD:30") != NULL);
|
||||||
|
CHECK(strstr(r.text, "Pudding") != NULL);
|
||||||
|
bt_ctl_execute("a2dp", NULL, &r);
|
||||||
|
CHECK(r.code == MEOW_OK);
|
||||||
|
CHECK(strstr(r.text, "codec=SBC") != NULL);
|
||||||
|
CHECK(strstr(r.text, "rate=44100") != NULL);
|
||||||
|
CHECK(strstr(r.text, "acquired=1") != NULL);
|
||||||
|
CHECK(strstr(r.text, "role=src") != NULL);
|
||||||
|
snprintf(p, sizeof p, "%s/bt_scan.txt", T); wf(p, /* 重写(bt scan on 曾覆盖) */
|
||||||
|
"[NEW] Device 33:DE:2E:21:AD:30 Pudding\n[NEW] Device AA:BB:CC:DD:EE:FF Earbuds\n");
|
||||||
|
bt_ctl_execute("devices", NULL, &r);
|
||||||
|
CHECK(r.code == MEOW_OK);
|
||||||
|
CHECK(strstr(r.text, "Earbuds") != NULL);
|
||||||
|
CHECK(strstr(r.text, "Device 33:DE:2E:21:AD:30") != NULL);
|
||||||
|
bt_ctl_execute("status", NULL, &r);
|
||||||
|
CHECK(r.code == MEOW_OK);
|
||||||
|
CHECK(strstr(r.text, "connected=2") != NULL);
|
||||||
|
|
||||||
|
printf("== bt_write_authorize(系统身份 + MeowOS 才放行写级) ==\n");
|
||||||
|
caller_t other; memset(&other, 0, sizeof other); other.kind = CALL_OTHER;
|
||||||
|
caller_t sys; memset(&sys, 0, sizeof sys); sys.kind = CALL_SYS;
|
||||||
|
snprintf(sys.pkg, sizeof sys.pkg, "meow_net");
|
||||||
|
CHECK(bt_write_authorize(&other, 1, "connect") == MEOW_ERR_PERM); /* 非系统 → 拒 */
|
||||||
|
CHECK(bt_write_authorize(&sys, 1, "connect") == MEOW_OK); /* 系统+meow → 放 */
|
||||||
|
CHECK(bt_write_authorize(&sys, 0, "connect") == MEOW_ERR_PERM); /* 系统+hiby → 拒 */
|
||||||
|
CHECK(bt_write_authorize(&other, 1, "status") == MEOW_OK); /* 读级任意 → 放 */
|
||||||
|
|
||||||
|
if (g_fail) { printf("\nBT_CTL TEST: %d FAIL\n", g_fail); return 1; }
|
||||||
|
printf("\nBT_CTL TEST: ALL PASS\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -75,8 +75,39 @@ echo " UP RUNNING PSCAN"
|
|||||||
EOF
|
EOF
|
||||||
cat > "$T/bluetoothctl" <<'EOF'
|
cat > "$T/bluetoothctl" <<'EOF'
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
echo "Device 00:1A:7D:DA:71:13 My Headset"
|
case "${1:-}" in
|
||||||
echo "Device 11:22:33:44:55:66 Speaker"
|
paired|paired-devices)
|
||||||
|
echo "Device 00:1A:7D:DA:71:13 My Headset"
|
||||||
|
echo "Device 11:22:33:44:55:66 Speaker";;
|
||||||
|
devices)
|
||||||
|
echo "Device 00:1A:7D:DA:71:13 My Headset"
|
||||||
|
echo "Device 11:22:33:44:55:66 Speaker"
|
||||||
|
echo "Device 33:DE:2E:21:AD:30 Pudding";;
|
||||||
|
connect|disconnect|pair|remove)
|
||||||
|
echo "Controller 00:1A:7D:DA:71:13 Connected: yes";;
|
||||||
|
*) echo "Device 00:1A:7D:DA:71:13 My Headset"; echo "Device 11:22:33:44:55:66 Speaker";;
|
||||||
|
esac
|
||||||
|
exit 0
|
||||||
|
EOF
|
||||||
|
cat > "$T/hcitool" <<'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
echo " < ACL 33:DE:2E:21:AD:30 handle 12 state 1 lm MASTER"
|
||||||
|
echo " < ACL 11:22:33:44:55:66 handle 13 state 1 lm SLAVE"
|
||||||
|
exit 0
|
||||||
|
EOF
|
||||||
|
cat > "$T/bluealsa-cli" <<'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
if [ "${1:-}" = "a2dp" ]; then
|
||||||
|
echo "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc:"
|
||||||
|
echo " Type: A2DP Source"
|
||||||
|
echo " Codec: SBC"
|
||||||
|
echo " Sampling: 44100 Hz"
|
||||||
|
echo " Channels: 2"
|
||||||
|
echo " Bits per Sample: 16"
|
||||||
|
echo " State: RUNNING"
|
||||||
|
echo " Acquired: yes"
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
EOF
|
EOF
|
||||||
cat > "$T/ip" <<'EOF'
|
cat > "$T/ip" <<'EOF'
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
@@ -86,13 +117,14 @@ for a in "$@"; do
|
|||||||
done
|
done
|
||||||
exit 0
|
exit 0
|
||||||
EOF
|
EOF
|
||||||
for f in wpa_cli hciconfig bluetoothctl ip; do chmod +x "$T/$f"; done
|
for f in wpa_cli hciconfig bluetoothctl hcitool bluealsa-cli ip; do chmod +x "$T/$f"; done
|
||||||
|
|
||||||
mkdir -p "$T/logs" "$T/pid"
|
mkdir -p "$T/logs" "$T/pid"
|
||||||
# 期2:DNS 禁用(宿主无 root 不绑 53),观测层经 MEOW_TCP/UDP 打桩;amsd/svcd 缺省(实体归因走 proc:comm→unknown)。
|
# 期2:DNS 禁用(宿主无 root 不绑 53),观测层经 MEOW_TCP/UDP 打桩;amsd/svcd 缺省(实体归因走 proc:comm→unknown)。
|
||||||
ENVS="MEOW_NET_DIR=$NET MEOW_NETDEV=$T/netdev MEOW_NET_ROUTE=$T/route MEOW_RESOLV=$T/resolv
|
ENVS="MEOW_NET_DIR=$NET MEOW_NETDEV=$T/netdev MEOW_NET_ROUTE=$T/route MEOW_RESOLV=$T/resolv
|
||||||
MEOW_RFKILL_DIR=$RF MEOW_BT_DIR=$BT MEOW_BOOT_MODE=$T/boot_mode MEOW_BOOTTS_CACHE=$T/bootts
|
MEOW_RFKILL_DIR=$RF MEOW_BT_DIR=$BT MEOW_BOOT_MODE=$T/boot_mode MEOW_BOOTTS_CACHE=$T/bootts
|
||||||
MEOW_IP=$T/ip MEOW_WPA_CLI=$T/wpa_cli MEOW_HCICONFIG=$T/hciconfig MEOW_BLUETOOTHCTL=$T/bluetoothctl
|
MEOW_IP=$T/ip MEOW_WPA_CLI=$T/wpa_cli MEOW_HCICONFIG=$T/hciconfig MEOW_BLUETOOTHCTL=$T/bluetoothctl
|
||||||
|
MEOW_HCITOOL=$T/hcitool MEOW_BLUEALSA_CLI=$T/bluealsa-cli MEOW_BT_SCAN_CACHE=$T/bt_scan.txt
|
||||||
MEOW_NET_SOCK=$T/net.sock MEOW_NET_POLL_SEC=1 MEOW_LOG_DIR=$T/logs
|
MEOW_NET_SOCK=$T/net.sock MEOW_NET_POLL_SEC=1 MEOW_LOG_DIR=$T/logs
|
||||||
MEOW_TCP=$T/tcp MEOW_UDP=$T/udp MEOW_NET_POLICY=$T/policy
|
MEOW_TCP=$T/tcp MEOW_UDP=$T/udp MEOW_NET_POLICY=$T/policy
|
||||||
MEOW_AMS_SOCK=$T/no_ams MEOW_SVCD_SOCK=$T/no_svcd MEOW_DNS_DISABLE=1"
|
MEOW_AMS_SOCK=$T/no_ams MEOW_SVCD_SOCK=$T/no_svcd MEOW_DNS_DISABLE=1"
|
||||||
@@ -137,10 +169,40 @@ ck T4a "permission denied" "$OUT"
|
|||||||
OUT=$(env $ENVS $Q "$T/meow_net" bt status 2>&1)
|
OUT=$(env $ENVS $Q "$T/meow_net" bt status 2>&1)
|
||||||
ck T5a "available=1 powered=1" "$OUT"
|
ck T5a "available=1 powered=1" "$OUT"
|
||||||
ck T5b "paired=2" "$OUT"
|
ck T5b "paired=2" "$OUT"
|
||||||
|
ck T5b2 "connected=2" "$OUT" # P1:内核 ACL 权威已连接数
|
||||||
OUT=$(env $ENVS $Q "$T/meow_net" bt paired 2>&1)
|
OUT=$(env $ENVS $Q "$T/meow_net" bt paired 2>&1)
|
||||||
ck T5c "My Headset" "$OUT"
|
ck T5c "My Headset" "$OUT"
|
||||||
ck T5d "Speaker" "$OUT"
|
ck T5d "Speaker" "$OUT"
|
||||||
|
|
||||||
|
# ---- T5e: bt connected(P1:内核 ACL 权威,名经 devices 缓存兜底)----
|
||||||
|
OUT=$(env $ENVS $Q "$T/meow_net" bt connected 2>&1)
|
||||||
|
ck T5e "Device 33:DE:2E:21:AD:30 Pudding" "$OUT"
|
||||||
|
ck T5e2 "Device 11:22:33:44:55:66 Speaker" "$OUT"
|
||||||
|
|
||||||
|
# ---- T5f: bt a2dp(P1:bluealsa-cli a2dp list → codec/rate/acquired/role)----
|
||||||
|
OUT=$(env $ENVS $Q "$T/meow_net" bt a2dp 2>&1)
|
||||||
|
ck T5f "codec=SBC" "$OUT"
|
||||||
|
ck T5f2 "rate=44100" "$OUT"
|
||||||
|
ck T5f3 "acquired=1" "$OUT"
|
||||||
|
ck T5f4 "role=src" "$OUT"
|
||||||
|
|
||||||
|
# ---- T5g: bt devices(P1:netmgrd 权威扫描缓存)----
|
||||||
|
cat > "$T/bt_scan.txt" <<'EOF'
|
||||||
|
[NEW] Device 33:DE:2E:21:AD:30 Pudding
|
||||||
|
[NEW] Device AA:BB:CC:DD:EE:FF Earbuds
|
||||||
|
[DEL] Device 00:11:22:33:44:55
|
||||||
|
EOF
|
||||||
|
OUT=$(env $ENVS $Q "$T/meow_net" bt devices 2>&1)
|
||||||
|
ck T5g "Device 33:DE:2E:21:AD:30 Pudding" "$OUT"
|
||||||
|
ck T5g2 "Device AA:BB:CC:DD:EE:FF Earbuds" "$OUT"
|
||||||
|
|
||||||
|
# ---- T5h: bt 写级(connect/pair 等)在宿主 CLI=CALL_OTHER → 拒(仅系统身份+MeowOS 放行,见 T6)----
|
||||||
|
OUT=$(env $ENVS $Q "$T/meow_net" bt connect 33:DE:2E:21:AD:30 2>&1); RC=$?
|
||||||
|
ck T5h "permission denied" "$OUT"
|
||||||
|
ck T5h2 "rc=1" "rc=$RC"
|
||||||
|
OUT=$(env $ENVS $Q "$T/meow_net" bt scan on 2>&1)
|
||||||
|
ck T5i "permission denied" "$OUT"
|
||||||
|
|
||||||
# ---- T6: HibyOS 只读降级(读可查,写拒)----
|
# ---- T6: HibyOS 只读降级(读可查,写拒)----
|
||||||
echo hiby > "$T/boot_mode"
|
echo hiby > "$T/boot_mode"
|
||||||
sleep 2 # 等守护下一拍热切换
|
sleep 2 # 等守护下一拍热切换
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ volatile int g_net_ready = 0;
|
|||||||
int g_host_mock = 0;
|
int g_host_mock = 0;
|
||||||
|
|
||||||
static meow_view *g_login_view, *g_home_view, *g_pl_view, *g_plr_view, *g_set_view,
|
static meow_view *g_login_view, *g_home_view, *g_pl_view, *g_plr_view, *g_set_view,
|
||||||
*g_search_view;
|
*g_search_view, *g_queue_view;
|
||||||
|
|
||||||
static void on_sigterm(int s) { (void)s; meow_ui_request_exit(); }
|
static void on_sigterm(int s) { (void)s; meow_ui_request_exit(); }
|
||||||
|
|
||||||
@@ -92,6 +92,7 @@ static void app_on_tap(int x, int y)
|
|||||||
if (player_is_active()) { player_on_tap(x, y); return; }
|
if (player_is_active()) { player_on_tap(x, y); return; }
|
||||||
if (settings_is_active()) { settings_on_tap(x, y); return; }
|
if (settings_is_active()) { settings_on_tap(x, y); return; }
|
||||||
if (playlist_is_active()) { playlist_on_tap(x, y); return; }
|
if (playlist_is_active()) { playlist_on_tap(x, y); return; }
|
||||||
|
if (queue_is_active()) { queue_on_tap(x, y); return; }
|
||||||
if (search_is_active()) { search_on_tap(x, y); return; }
|
if (search_is_active()) { search_on_tap(x, y); return; }
|
||||||
home_on_tap(x, y);
|
home_on_tap(x, y);
|
||||||
}
|
}
|
||||||
@@ -105,6 +106,7 @@ static void app_on_swipe(int dir, int dx, int dy)
|
|||||||
if (player_is_active()) { meow_view_page_pop(); return; }
|
if (player_is_active()) { meow_view_page_pop(); return; }
|
||||||
if (settings_is_active()) { meow_view_page_pop(); return; }
|
if (settings_is_active()) { meow_view_page_pop(); return; }
|
||||||
if (playlist_is_active()) { meow_view_page_pop(); return; }
|
if (playlist_is_active()) { meow_view_page_pop(); return; }
|
||||||
|
if (queue_is_active()) { meow_view_page_pop(); return; }
|
||||||
if (search_is_active()) { meow_view_page_pop(); return; }
|
if (search_is_active()) { meow_view_page_pop(); return; }
|
||||||
if (home_is_active()) {
|
if (home_is_active()) {
|
||||||
/* 任务2:home 根页左滑不再退出进程 → 请求 launcher 退到桌面/后台(进程+媒体会话保留)。
|
/* 任务2:home 根页左滑不再退出进程 → 请求 launcher 退到桌面/后台(进程+媒体会话保留)。
|
||||||
@@ -121,6 +123,7 @@ static void app_on_swipe(int dir, int dx, int dy)
|
|||||||
}
|
}
|
||||||
if (home_is_active()) { home_swipe(dir, dy); return; }
|
if (home_is_active()) { home_swipe(dir, dy); return; }
|
||||||
if (playlist_is_active()) { playlist_swipe(dir, dy); return; }
|
if (playlist_is_active()) { playlist_swipe(dir, dy); return; }
|
||||||
|
if (queue_is_active()) { queue_swipe(dir, dy); return; }
|
||||||
if (search_is_active()) { search_swipe(dir, dy); return; }
|
if (search_is_active()) { search_swipe(dir, dy); return; }
|
||||||
if (player_is_active()) { player_on_swipe(dir, dy); return; }
|
if (player_is_active()) { player_on_swipe(dir, dy); return; }
|
||||||
}
|
}
|
||||||
@@ -130,6 +133,7 @@ static void app_on_touch_down(int x, int y)
|
|||||||
if (player_is_active()) player_touch_down(x, y); /* §22 中部横滑页切换跟踪 */
|
if (player_is_active()) player_touch_down(x, y); /* §22 中部横滑页切换跟踪 */
|
||||||
else if (home_is_active()) home_touch_down(x, y);
|
else if (home_is_active()) home_touch_down(x, y);
|
||||||
else if (playlist_is_active()) playlist_touch_down(x, y);
|
else if (playlist_is_active()) playlist_touch_down(x, y);
|
||||||
|
else if (queue_is_active()) queue_touch_down(x, y);
|
||||||
else if (settings_is_active()) settings_touch_down(x, y);
|
else if (settings_is_active()) settings_touch_down(x, y);
|
||||||
else if (search_is_active()) search_touch_down(x, y);
|
else if (search_is_active()) search_touch_down(x, y);
|
||||||
}
|
}
|
||||||
@@ -139,6 +143,7 @@ static void app_on_touch_move(int x, int y)
|
|||||||
if (player_is_active()) player_touch_move(x, y);
|
if (player_is_active()) player_touch_move(x, y);
|
||||||
else if (home_is_active()) home_touch_move(x, y);
|
else if (home_is_active()) home_touch_move(x, y);
|
||||||
else if (playlist_is_active()) playlist_touch_move(x, y);
|
else if (playlist_is_active()) playlist_touch_move(x, y);
|
||||||
|
else if (queue_is_active()) queue_touch_move(x, y);
|
||||||
else if (settings_is_active()) settings_touch_move(x, y);
|
else if (settings_is_active()) settings_touch_move(x, y);
|
||||||
else if (search_is_active()) search_touch_move(x, y);
|
else if (search_is_active()) search_touch_move(x, y);
|
||||||
}
|
}
|
||||||
@@ -188,6 +193,7 @@ static void app_on_tick(void)
|
|||||||
if (login_is_active()) { login_tick(now); return; }
|
if (login_is_active()) { login_tick(now); return; }
|
||||||
if (player_is_active()) { player_tick(now); return; }
|
if (player_is_active()) { player_tick(now); return; }
|
||||||
if (search_is_active()) { search_tick(now); return; }
|
if (search_is_active()) { search_tick(now); return; }
|
||||||
|
if (queue_is_active()) { queue_tick(now); return; }
|
||||||
if (home_is_active()) { home_tick(now); }
|
if (home_is_active()) { home_tick(now); }
|
||||||
if (playlist_is_active()) { playlist_tick(now); }
|
if (playlist_is_active()) { playlist_tick(now); }
|
||||||
chrome_tick();
|
chrome_tick();
|
||||||
@@ -219,13 +225,14 @@ int main(void)
|
|||||||
ipc_media_start();
|
ipc_media_start();
|
||||||
g_net_ready = 1;
|
g_net_ready = 1;
|
||||||
|
|
||||||
/* 建六页 view */
|
/* 建页 view */
|
||||||
g_login_view = login_view_create();
|
g_login_view = login_view_create();
|
||||||
g_home_view = home_view_create();
|
g_home_view = home_view_create();
|
||||||
g_pl_view = playlist_view_create();
|
g_pl_view = playlist_view_create();
|
||||||
g_plr_view = player_view_create();
|
g_plr_view = player_view_create();
|
||||||
g_set_view = settings_view_create();
|
g_set_view = settings_view_create();
|
||||||
g_search_view = search_view_create();
|
g_search_view = search_view_create();
|
||||||
|
g_queue_view = queue_view_create(); /* §NEW 当前播放队列页 */
|
||||||
mini_bar_init(); /* mini 播放栏:登记跨 Activity 绘制物(进 root 页前,首帧即参与重画) */
|
mini_bar_init(); /* mini 播放栏:登记跨 Activity 绘制物(进 root 页前,首帧即参与重画) */
|
||||||
|
|
||||||
/* 进入根页:已登录→home,否则→login */
|
/* 进入根页:已登录→home,否则→login */
|
||||||
@@ -248,6 +255,7 @@ int main(void)
|
|||||||
meow_view_free(g_home_view);
|
meow_view_free(g_home_view);
|
||||||
meow_view_free(g_login_view);
|
meow_view_free(g_login_view);
|
||||||
meow_view_free(g_search_view);
|
meow_view_free(g_search_view);
|
||||||
|
meow_view_free(g_queue_view);
|
||||||
meow_ui_shutdown();
|
meow_ui_shutdown();
|
||||||
meow_log(MEOW_LOG_INFO, "exit");
|
meow_log(MEOW_LOG_INFO, "exit");
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -30,6 +30,17 @@
|
|||||||
#define ACT_PLAYER 3 /* 播放页(可横竖屏切,v1 允许切) */
|
#define ACT_PLAYER 3 /* 播放页(可横竖屏切,v1 允许切) */
|
||||||
#define ACT_SETTINGS 4 /* 设置页 */
|
#define ACT_SETTINGS 4 /* 设置页 */
|
||||||
#define ACT_SEARCH 5 /* 搜索页(顶栏搜索按钮进入) */
|
#define ACT_SEARCH 5 /* 搜索页(顶栏搜索按钮进入) */
|
||||||
|
#define ACT_QUEUE 6 /* 当前播放队列页(播放页「队列」钮进入,展示实时播放列表) */
|
||||||
|
|
||||||
|
/* ---- 播放模式三态(REPORT §NEW:随机 / 列表循环 / 单曲循环)----
|
||||||
|
* 默认=列表循环(延续既有「顺序下一首」语义;列表循环下自动顺延会在队尾回绕)。
|
||||||
|
* 三态按钮点击按 enum 序循环(SHUFFLE→LISTLOOP→SINGLE→SHUFFLE),图标/文案见 ncm_mode_name。 */
|
||||||
|
enum {
|
||||||
|
NCM_MODE_SHUFFLE = 0, /* 随机播放:自动播完/手动下一首 → 随机取队列另一首 */
|
||||||
|
NCM_MODE_LISTLOOP = 1, /* 列表循环:自动播完/手动下一首 → 顺序下一首(队尾回绕队首) */
|
||||||
|
NCM_MODE_SINGLE = 2, /* 单曲循环:当前曲播完 → 重播本曲;手动下一首仍顺序 */
|
||||||
|
};
|
||||||
|
#define NCM_MODE_COUNT 3
|
||||||
|
|
||||||
/* ---- 封面 CDN 尺寸参数(REPORT §NEW 任务2:网易云图片 CDN 对 URL 追加 ?param=WxH 返等比
|
/* ---- 封面 CDN 尺寸参数(REPORT §NEW 任务2:网易云图片 CDN 对 URL 追加 ?param=WxH 返等比
|
||||||
* 缩略 JPEG——省内存 +解 webp 三分型③+命中率高;已含 param= 的 URL 尊重原样)。
|
* 缩略 JPEG——省内存 +解 webp 三分型③+命中率高;已含 param= 的 URL 尊重原样)。
|
||||||
@@ -137,7 +148,8 @@ enum {
|
|||||||
typedef struct ncm_play_run {
|
typedef struct ncm_play_run {
|
||||||
pthread_mutex_t mu;
|
pthread_mutex_t mu;
|
||||||
int state; /* NCM_PC_* */
|
int state; /* NCM_PC_* */
|
||||||
int cur_song; /* 当前曲目在 g_st.songs[] 下标;-1=无 */
|
int play_mode; /* NCM_MODE_*(播放模式三态;UI 读写,播放线程切歌依此) */
|
||||||
|
int cur_song; /* 当前曲目在队列(queue_src)下标;-1=无 */
|
||||||
int64_t pos_ms; /* 当前播放位置(播放进程推进,seek 写此) */
|
int64_t pos_ms; /* 当前播放位置(播放进程推进,seek 写此) */
|
||||||
int64_t dur_ms; /* 当前曲目标称时长 */
|
int64_t dur_ms; /* 当前曲目标称时长 */
|
||||||
int cmd; /* 暂存最新内部命令(play/pause/prev/next/seek)*/
|
int cmd; /* 暂存最新内部命令(play/pause/prev/next/seek)*/
|
||||||
@@ -224,6 +236,7 @@ typedef struct ncm_app_state {
|
|||||||
int level; /* NCM_LEVEL_* */
|
int level; /* NCM_LEVEL_* */
|
||||||
int cache_limit_gb;
|
int cache_limit_gb;
|
||||||
int player_bg_mode; /* 歌词页背景模式(§22):NCM_BG_DEFAULT / NCM_BG_POWER_SAVE */
|
int player_bg_mode; /* 歌词页背景模式(§22):NCM_BG_DEFAULT / NCM_BG_POWER_SAVE */
|
||||||
|
int play_mode; /* 播放模式(§NEW 三态):NCM_MODE_*;持久化 ncm.play_mode */
|
||||||
|
|
||||||
/* 搜索(REPORT §23:独立结果数组,不覆写歌单窗口;单曲/歌单两类页签各自独立结果集/分页/滚动) */
|
/* 搜索(REPORT §23:独立结果数组,不覆写歌单窗口;单曲/歌单两类页签各自独立结果集/分页/滚动) */
|
||||||
char search_query[256];
|
char search_query[256];
|
||||||
@@ -328,6 +341,7 @@ void player_on_tap(int x, int y);
|
|||||||
void player_tick(uint64_t now); /* 播放进度/歌词滚动 */
|
void player_tick(uint64_t now); /* 播放进度/歌词滚动 */
|
||||||
void player_play_song(int idx); /* 从歌单窗口第 idx 首开始播放(queue=PLAYLIST) */
|
void player_play_song(int idx); /* 从歌单窗口第 idx 首开始播放(queue=PLAYLIST) */
|
||||||
void player_play_search_song(int i); /* 从搜索单曲第 i 首开始播放(queue=SEARCH,§23) */
|
void player_play_search_song(int i); /* 从搜索单曲第 i 首开始播放(queue=SEARCH,§23) */
|
||||||
|
void player_play_queue_song(int idx);/* 从「当前实时播放队列」第 idx 首播放(保持当前 queue_src,§NEW) */
|
||||||
void player_pause_toggle(int on);
|
void player_pause_toggle(int on);
|
||||||
void player_next(int dir); /* dir=+1 下一首 / -1 上一首 */
|
void player_next(int dir); /* dir=+1 下一首 / -1 上一首 */
|
||||||
void player_seek(int64_t ms);
|
void player_seek(int64_t ms);
|
||||||
@@ -355,6 +369,16 @@ void mini_bar_shutdown(void); /* 注销(app 退出前) */
|
|||||||
void mini_bar_tick(uint64_t now);/* 每帧:可见性/歌曲/暂停态/进度变化 → 失效栏带并重绘 */
|
void mini_bar_tick(uint64_t now);/* 每帧:可见性/歌曲/暂停态/进度变化 → 失效栏带并重绘 */
|
||||||
int mini_bar_hit(int x, int y); /* app_on_tap 顶层先行拦截:1=消费(toggle/进播放页) */
|
int mini_bar_hit(int x, int y); /* app_on_tap 顶层先行拦截:1=消费(toggle/进播放页) */
|
||||||
|
|
||||||
|
/* queue(当前播放队列页,§NEW:展示实时播放列表/分页/当前曲高亮/点播切歌) */
|
||||||
|
meow_view *queue_view_create(void);
|
||||||
|
int queue_is_active(void);
|
||||||
|
int queue_on_tap(int x, int y);
|
||||||
|
void queue_swipe(int dir, int dist);
|
||||||
|
void queue_touch_down(int x, int y);
|
||||||
|
void queue_touch_move(int x, int y);
|
||||||
|
void queue_open(void); /* 进当前播放队列页 */
|
||||||
|
void queue_tick(uint64_t now); /* UI tick:检查队列 worker 完成并刷新 */
|
||||||
|
|
||||||
/* search */
|
/* search */
|
||||||
meow_view *search_view_create(void);
|
meow_view *search_view_create(void);
|
||||||
int search_is_active(void);
|
int search_is_active(void);
|
||||||
@@ -433,6 +457,7 @@ void appstate_set_queue(int src); /* NCM_QUEUE_PLAYLIST / NCM_QUEU
|
|||||||
int appstate_get_queue(void);
|
int appstate_get_queue(void);
|
||||||
int appstate_queue_count(void); /* 当前播放队列长度(按 queue_src) */
|
int appstate_queue_count(void); /* 当前播放队列长度(按 queue_src) */
|
||||||
int appstate_queue_song(int idx, ncm_song_t *out); /* 队列第 idx 首;0=ok(按 queue_src) */
|
int appstate_queue_song(int idx, ncm_song_t *out); /* 队列第 idx 首;0=ok(按 queue_src) */
|
||||||
|
void appstate_apply_play_mode(int m); /* 设 g_st.play_mode + 落盘(持久化「记住模式」,§NEW) */
|
||||||
|
|
||||||
/* ---- 搜索结果独立数组(§23;g_search_mu 保护,UI/worker 经此访问)---- */
|
/* ---- 搜索结果独立数组(§23;g_search_mu 保护,UI/worker 经此访问)---- */
|
||||||
int appstate_search_count(int tab); /* tab: 0=单曲 1=歌单 */
|
int appstate_search_count(int tab); /* tab: 0=单曲 1=歌单 */
|
||||||
@@ -523,10 +548,14 @@ int player_manager_error(void);
|
|||||||
int player_manager_current_song(ncm_song_t *out); /* 播放曲快照锁内拷贝;0=有效 -1=无(REPORT §17) */
|
int player_manager_current_song(ncm_song_t *out); /* 播放曲快照锁内拷贝;0=有效 -1=无(REPORT §17) */
|
||||||
void player_manager_dl(int64_t *done, int64_t *total); /* 本曲下载进度(字节;total=0 未知,§18) */
|
void player_manager_dl(int64_t *done, int64_t *total); /* 本曲下载进度(字节;total=0 未知,§18) */
|
||||||
int player_manager_eff_level(void); /* 实际生效音质档(§20;-1=未定) */
|
int player_manager_eff_level(void); /* 实际生效音质档(§20;-1=未定) */
|
||||||
|
int player_manager_play_mode(void); /* 当前播放模式(§NEW 三态:NCM_MODE_*) */
|
||||||
|
void player_manager_set_play_mode(int m); /* 设播放模式(运行时态;持久化由 appstate_apply_play_mode 落盘) */
|
||||||
|
void player_manager_next_intent(void); /* 曲目自然播完 → 按播放模式决定下一首/重播(单曲循环重播本曲) */
|
||||||
|
|
||||||
/* 音质档位显示名/规格(ui/chrome.c,§20 八档 UI 权威口径;settings/player 共用) */
|
/* 音质档位显示名/规格(ui/chrome.c,§20 八档 UI 权威口径;settings/player 共用) */
|
||||||
const char *ncm_level_name(int lv); /* 短名:标准/极高 HQ/无损 SQ/Hi-Res/高清臻音/臻音全景声/超清母带/沉浸环绕声 */
|
const char *ncm_level_name(int lv); /* 短名:标准/极高 HQ/无损 SQ/Hi-Res/高清臻音/臻音全景声/超清母带/沉浸环绕声 */
|
||||||
const char *ncm_level_spec(int lv); /* 规格:128kbps / ≤320kbps / 48kHz·16bit / ... */
|
const char *ncm_level_spec(int lv); /* 规格:128kbps / ≤320kbps / 48kHz·16bit / ... */
|
||||||
|
const char *ncm_mode_name(int m); /* 播放模式短名(§NEW 三态):随机 / 列表 / 单曲 */
|
||||||
|
|
||||||
/* songmeta(app/logic/songmeta.c):歌曲元数据本地缓存(<cache_dir>/meta/<id>.bin),
|
/* songmeta(app/logic/songmeta.c):歌曲元数据本地缓存(<cache_dir>/meta/<id>.bin),
|
||||||
* 拉过的 song_detail 结果序列化落盘,再次入窗免重复请求(REPORT §17)。 */
|
* 拉过的 song_detail 结果序列化落盘,再次入窗免重复请求(REPORT §17)。 */
|
||||||
|
|||||||
@@ -295,3 +295,47 @@ void ipc_media_stop(void)
|
|||||||
/* 线程在 200ms poll 内退出(detach,不 join)*/
|
/* 线程在 200ms poll 内退出(detach,不 join)*/
|
||||||
g_thr_on = 0;
|
g_thr_on = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- 批3:live 歌词上报(lrc 原文 + tlrc 翻译 → 媒体会话,桌面歌词页实时滚动源)---- */
|
||||||
|
|
||||||
|
/* 歌词代际(换歌/换词自增;launcher 见新 seq 清旧词)。IC 线程外调用(lyric_worker),
|
||||||
|
* 单线程写(worker 是唯一调用者),无需锁。 */
|
||||||
|
static uint32_t g_lyr_media_seq;
|
||||||
|
|
||||||
|
/* 把一个 kind 的歌词文本按分片发到媒体会话(≤MEOW_LYRIC_CHUNK 一帧)。cmax=该 kind 上限:
|
||||||
|
* 0=MEOW_MEDIA_LRC_MAX,1=MEOW_MEDIA_TLRC_MAX。total=0(空串)时发一个空帧=清词。 */
|
||||||
|
static void send_lyric_kind(int fd, uint32_t seq, uint16_t kind, const char *text, size_t cmax)
|
||||||
|
{
|
||||||
|
if (!text) text = "";
|
||||||
|
size_t total = strlen(text);
|
||||||
|
if (total > cmax) total = cmax; /* 接收侧缓冲上限:超限截断 */
|
||||||
|
size_t off = 0;
|
||||||
|
do {
|
||||||
|
size_t n = total - off;
|
||||||
|
if (n > MEOW_LYRIC_CHUNK) n = MEOW_LYRIC_CHUNK;
|
||||||
|
m_lyric_t ly;
|
||||||
|
memset(&ly, 0, sizeof ly);
|
||||||
|
ncm_strlcpy(ly.app_id, "dev.player.ncm", sizeof ly.app_id);
|
||||||
|
ly.seq = seq;
|
||||||
|
ly.kind = kind;
|
||||||
|
ly.total = (uint16_t)total;
|
||||||
|
ly.off = (uint16_t)off;
|
||||||
|
ly.len = (uint16_t)n;
|
||||||
|
if (n) memcpy(ly.data, text + off, n);
|
||||||
|
ly.data[n] = 0;
|
||||||
|
send_frame(fd, MSG_META_LYRIC, seq, &ly, sizeof ly);
|
||||||
|
off += n;
|
||||||
|
} while (off < total);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ipc_media_lyric_report(const char *lrc, const char *tl)
|
||||||
|
{
|
||||||
|
g_lyr_media_seq++;
|
||||||
|
int fd = connect_path(sock_path());
|
||||||
|
if (fd < 0) { meow_log(MEOW_LOG_WARN, "ipc: lyric no launcher socket"); return; }
|
||||||
|
send_lyric_kind(fd, g_lyr_media_seq, 0, lrc, MEOW_MEDIA_LRC_MAX); /* 原文 */
|
||||||
|
send_lyric_kind(fd, g_lyr_media_seq, 1, tl, MEOW_MEDIA_TLRC_MAX); /* 翻译 */
|
||||||
|
close(fd);
|
||||||
|
meow_log(MEOW_LOG_INFO, "ipc: lyric reported seq=%u lrc=%zuB tlrc=%zuB",
|
||||||
|
g_lyr_media_seq, lrc ? strlen(lrc) : 0, tl ? strlen(tl) : 0);
|
||||||
|
}
|
||||||
@@ -49,6 +49,63 @@ static int g_cache_gb;
|
|||||||
* 同曲护栏据它分流:pending=1 走重播(换档),pending=0 只聚焦不重播(点正在播的曲)。 */
|
* 同曲护栏据它分流:pending=1 走重播(换档),pending=0 只聚焦不重播(点正在播的曲)。 */
|
||||||
static int g_pending_replay;
|
static int g_pending_replay;
|
||||||
|
|
||||||
|
/* ---- 播放模式三态(REPORT §NEW:随机 / 列表循环 / 单曲循环)----
|
||||||
|
* 运行时态在 g_play.play_mode(mu 保护,播放线程读);持久化走 g_st.play_mode(appstate)。
|
||||||
|
* 随机下一首用 xorshift32(种懒初始化自 ncm_now_ms ^ 本变量地址,避免恒同序列);
|
||||||
|
* 随机「上一首」用有界历史栈回溯(next/自动顺延 压栈当前曲目,prev 弹栈回放)。 */
|
||||||
|
static uint32_t g_shuffle_seed;
|
||||||
|
#define SHUFFLE_HIST_DEPTH 32
|
||||||
|
static int g_shuffle_hist[SHUFFLE_HIST_DEPTH];
|
||||||
|
static int g_shuffle_hist_len;
|
||||||
|
|
||||||
|
/* 随机取一个 ≠cur 的队列下标(队列>1 时;队列≤1 时返回 cur——单曲等价重播)。持 g_play.mu。 */
|
||||||
|
static int shuffle_pick(int cur, int cnt)
|
||||||
|
{
|
||||||
|
if (cnt <= 1) return (cur >= 0 && cur < cnt) ? cur : 0;
|
||||||
|
if (g_shuffle_seed == 0)
|
||||||
|
g_shuffle_seed = ((uint32_t)ncm_now_ms() ^ (uint32_t)(uintptr_t)&g_shuffle_seed) | 1u;
|
||||||
|
uint32_t x = g_shuffle_seed;
|
||||||
|
x ^= x << 13; x ^= x >> 17; x ^= x << 5;
|
||||||
|
g_shuffle_seed = x;
|
||||||
|
int r = (int)(x % (uint32_t)cnt);
|
||||||
|
if (r < 0) r = 0;
|
||||||
|
if (r == cur) r = (r + 1) % cnt;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 随机历史压栈/弹栈(持 g_play.mu;换模式时清空)。 */
|
||||||
|
static void shuffle_hist_push(int cur)
|
||||||
|
{
|
||||||
|
if (cur < 0) return;
|
||||||
|
if (g_shuffle_hist_len >= SHUFFLE_HIST_DEPTH) {
|
||||||
|
memmove(g_shuffle_hist, g_shuffle_hist + 1,
|
||||||
|
(size_t)(SHUFFLE_HIST_DEPTH - 1) * sizeof(int));
|
||||||
|
g_shuffle_hist_len = SHUFFLE_HIST_DEPTH - 1;
|
||||||
|
}
|
||||||
|
g_shuffle_hist[g_shuffle_hist_len++] = cur;
|
||||||
|
}
|
||||||
|
static int shuffle_hist_pop(void)
|
||||||
|
{
|
||||||
|
return (g_shuffle_hist_len > 0) ? g_shuffle_hist[--g_shuffle_hist_len] : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int player_manager_play_mode(void)
|
||||||
|
{
|
||||||
|
int m; pthread_mutex_lock(&g_play.mu); m = g_play.play_mode; pthread_mutex_unlock(&g_play.mu);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
void player_manager_set_play_mode(int m)
|
||||||
|
{
|
||||||
|
if (m < NCM_MODE_SHUFFLE || m >= NCM_MODE_COUNT) return;
|
||||||
|
pthread_mutex_lock(&g_play.mu);
|
||||||
|
if (m != g_play.play_mode) {
|
||||||
|
g_play.play_mode = m;
|
||||||
|
g_shuffle_hist_len = 0; /* 换模式清随机历史(上一首不再回溯旧队列) */
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&g_play.mu);
|
||||||
|
}
|
||||||
|
|
||||||
/* 被抢占回调(SDK 读线程分发;audiod 后开流赢):走 g_play 命令通道暂停(内部持锁),
|
/* 被抢占回调(SDK 读线程分发;audiod 后开流赢):走 g_play 命令通道暂停(内部持锁),
|
||||||
* 不直接动锁外状态。恢复时 audio_out_pause(0) 会检测流死并自动重开(见 audio_out.c)。 */
|
* 不直接动锁外状态。恢复时 audio_out_pause(0) 会检测流死并自动重开(见 audio_out.c)。 */
|
||||||
static void on_audio_preempt(const char *by_app, void *ctx)
|
static void on_audio_preempt(const char *by_app, void *ctx)
|
||||||
@@ -146,28 +203,88 @@ void player_manager_seek(int64_t ms)
|
|||||||
pthread_mutex_unlock(&g_play.mu);
|
pthread_mutex_unlock(&g_play.mu);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 落地一个新播放意图(cur=idx):置 BUFFER + 清进度/错误/下载统计 + seq++(新代际,§25)。
|
||||||
|
* 持 g_play.mu。idx 可与当前 cur 相同(单曲循环重播本曲:seq++ 触 run_song 重跑换档)。 */
|
||||||
|
static void start_intent(int idx)
|
||||||
|
{
|
||||||
|
g_pending_replay = 0; /* 新播放意图落地:清「切档重播」待办 */
|
||||||
|
g_play.cur_song = idx;
|
||||||
|
g_play.cmd = 0;
|
||||||
|
g_play.state = NCM_PC_BUFFER;
|
||||||
|
g_play.pos_ms = 0;
|
||||||
|
g_play.err = 0;
|
||||||
|
g_play.dl_done = 0;
|
||||||
|
g_play.dl_total = 0;
|
||||||
|
g_play.seq++;
|
||||||
|
g_play.eff_level = -1; /* 切歌:生效档随新曲重估(下首恢复用户档,§20) */
|
||||||
|
g_play.underruns = 0;
|
||||||
|
g_play.underrun_ms = 0;
|
||||||
|
g_play.stalled = 0;
|
||||||
|
g_play.refaults = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 手动 下一首(dir=+1) / 上一首(dir=-1):按播放模式定语义。
|
||||||
|
* - 列表循环/单曲循环:顺序 ±1(队列内回绕)。
|
||||||
|
* - 随机:下一首=随机另一首(并把当前曲压入历史);上一首=历史回溯(栈空则随机另一首)。
|
||||||
|
* 队列空/未物化(cur 越界)时不改状态。持 g_play.mu 外调用(内部自锁)。 */
|
||||||
void player_manager_next(int dir)
|
void player_manager_next(int dir)
|
||||||
{
|
{
|
||||||
pthread_mutex_lock(&g_play.mu);
|
pthread_mutex_lock(&g_play.mu);
|
||||||
int c = g_play.cur_song + dir;
|
int mode = g_play.play_mode;
|
||||||
if (c < 0) c = 0;
|
int c = g_play.cur_song;
|
||||||
int cnt = appstate_queue_count(); /* §23:按队列来源(窗口全量 或 搜索条数) */
|
int cnt = appstate_queue_count(); /* §23:按队列来源(窗口全量 或 搜索条数) */
|
||||||
if (c >= cnt) c = cnt - 1;
|
int newc = -1;
|
||||||
if (c != g_play.cur_song) {
|
if (cnt > 0) {
|
||||||
g_pending_replay = 0; /* 新播放意图落地:清「切档重播」待办 */
|
if (dir > 0) {
|
||||||
g_play.cur_song = c;
|
if (mode == NCM_MODE_SHUFFLE) {
|
||||||
g_play.cmd = 0;
|
shuffle_hist_push(c);
|
||||||
g_play.state = NCM_PC_BUFFER;
|
newc = shuffle_pick(c, cnt);
|
||||||
|
} else {
|
||||||
|
newc = (c < 0) ? 0 : ((c + 1) % cnt); /* 列表/单曲:顺序下一首(回绕) */
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (mode == NCM_MODE_SHUFFLE) {
|
||||||
|
int h = shuffle_hist_pop();
|
||||||
|
newc = (h >= 0 && h < cnt) ? h : shuffle_pick(c, cnt);
|
||||||
|
} else {
|
||||||
|
newc = (c < 0) ? 0 : ((c - 1 + cnt) % cnt); /* 列表/单曲:顺序上一首(回绕) */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newc >= 0 && newc < cnt && newc != c)
|
||||||
|
start_intent(newc);
|
||||||
|
pthread_mutex_unlock(&g_play.mu);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 曲目自然播完(decode 循环退出、state 仍 PLAYING)→ 按播放模式决定下一首:
|
||||||
|
* - 单曲循环:重播本曲(start_intent(c),seq++ → run_song 重跑)。
|
||||||
|
* - 列表循环:顺序下一首(队尾回绕队首)。
|
||||||
|
* - 随机:随机另一首(并压栈当前曲供上一首回溯)。
|
||||||
|
* 队列空/cur 非法 → 停止。持 g_play.mu 外调用(内部自锁)。 */
|
||||||
|
void player_manager_next_intent(void)
|
||||||
|
{
|
||||||
|
pthread_mutex_lock(&g_play.mu);
|
||||||
|
int mode = g_play.play_mode;
|
||||||
|
int c = g_play.cur_song;
|
||||||
|
int cnt = appstate_queue_count();
|
||||||
|
if (cnt <= 0 || c < 0 || c >= cnt) {
|
||||||
|
/* 无可续播目标:停止(清态) */
|
||||||
|
g_pending_replay = 0;
|
||||||
|
g_play.cur_song = -1;
|
||||||
|
g_play.state = NCM_PC_STOPPED;
|
||||||
g_play.pos_ms = 0;
|
g_play.pos_ms = 0;
|
||||||
g_play.err = 0;
|
g_play.play_song_valid = 0;
|
||||||
g_play.dl_done = 0;
|
|
||||||
g_play.dl_total = 0;
|
|
||||||
g_play.seq++;
|
g_play.seq++;
|
||||||
g_play.eff_level = -1; /* 切歌:生效档随新曲重估(下首恢复用户档,§20) */
|
pthread_mutex_unlock(&g_play.mu);
|
||||||
g_play.underruns = 0;
|
return;
|
||||||
g_play.underrun_ms = 0;
|
}
|
||||||
g_play.stalled = 0;
|
if (mode == NCM_MODE_SINGLE) {
|
||||||
g_play.refaults = 0;
|
start_intent(c); /* 单曲循环:重播本曲 */
|
||||||
|
} else if (mode == NCM_MODE_LISTLOOP) {
|
||||||
|
start_intent((c + 1) % cnt); /* 列表循环:顺序下一首(回绕) */
|
||||||
|
} else {
|
||||||
|
shuffle_hist_push(c); /* 随机:压栈当前曲 + 随机下一首 */
|
||||||
|
start_intent(shuffle_pick(c, cnt));
|
||||||
}
|
}
|
||||||
pthread_mutex_unlock(&g_play.mu);
|
pthread_mutex_unlock(&g_play.mu);
|
||||||
}
|
}
|
||||||
@@ -865,14 +982,13 @@ static void *player_thread(void *arg)
|
|||||||
int cur2 = g_play.cur_song;
|
int cur2 = g_play.cur_song;
|
||||||
int st2 = g_play.state;
|
int st2 = g_play.state;
|
||||||
pthread_mutex_unlock(&g_play.mu);
|
pthread_mutex_unlock(&g_play.mu);
|
||||||
/* 自然播完(decode 循环正常退出,state 仍 PLAYING)且无新命令 → 顺延下一首;
|
/* 自然播完(decode 循环正常退出,state 仍 PLAYING)且无新命令 → 按播放模式顺延
|
||||||
|
* (列表循环=顺序下一首回绕 / 随机=随机下一首 / 单曲循环=重播本曲);
|
||||||
* 若是因同 idx 重播(seq 变、state=BUFFER)返回,则循环顶会按当前 cur/state 重跑
|
* 若是因同 idx 重播(seq 变、state=BUFFER)返回,则循环顶会按当前 cur/state 重跑
|
||||||
* run_song(§25:切音质后重播同一曲换档生效),**不得**当播完顺延下一首。 */
|
* run_song(§25:切音质后重播同一曲换档生效),**不得**当播完顺延下一首。 */
|
||||||
int qc = appstate_queue_count();
|
int qc = appstate_queue_count();
|
||||||
if (cur2 >= 0 && cur2 < qc && st2 == NCM_PC_PLAYING && cur2 == cur) {
|
if (cur2 >= 0 && cur2 < qc && st2 == NCM_PC_PLAYING && cur2 == cur) {
|
||||||
int nx = cur + 1;
|
player_manager_next_intent();
|
||||||
if (nx < qc) { player_manager_next(1); }
|
|
||||||
else { player_manager_stop(); }
|
|
||||||
} else {
|
} else {
|
||||||
usleep(50 * 1000);
|
usleep(50 * 1000);
|
||||||
}
|
}
|
||||||
@@ -929,8 +1045,14 @@ static void *lyric_worker(void *arg)
|
|||||||
pthread_mutex_lock(&g_lyrmu); g_lyr_state = 0; pthread_mutex_unlock(&g_lyrmu);
|
pthread_mutex_lock(&g_lyrmu); g_lyr_state = 0; pthread_mutex_unlock(&g_lyrmu);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
lrc[0] = tl[0] = 0; /* 默认空串(fetch 失败/空词时上报清词,防会话留旧曲歌词) */
|
||||||
int rc = ncm_api_lyric(g_st.ctx.device_id, g_st.ctx.cookie, id, lrc, 64 * 1024, tl, 32 * 1024);
|
int rc = ncm_api_lyric(g_st.ctx.device_id, g_st.ctx.cookie, id, lrc, 64 * 1024, tl, 32 * 1024);
|
||||||
if (rc == NCM_OK) lyric_parse(lrc, tl);
|
if (rc == NCM_OK) {
|
||||||
|
lyric_parse(lrc, tl);
|
||||||
|
ipc_media_lyric_report(lrc, tl); /* 批3:上报 live 歌词到媒体会话 */
|
||||||
|
} else {
|
||||||
|
ipc_media_lyric_report("", ""); /* 无词/拉取失败:清 media session 该曲歌词 */
|
||||||
|
}
|
||||||
free(lrc); free(tl);
|
free(lrc); free(tl);
|
||||||
|
|
||||||
pthread_mutex_lock(&g_lyrmu);
|
pthread_mutex_lock(&g_lyrmu);
|
||||||
@@ -976,6 +1098,9 @@ int player_manager_start(void)
|
|||||||
g_want_run = 1;
|
g_want_run = 1;
|
||||||
g_level = g_st.level;
|
g_level = g_st.level;
|
||||||
g_cache_gb = g_st.cache_limit_gb;
|
g_cache_gb = g_st.cache_limit_gb;
|
||||||
|
g_play.play_mode = g_st.play_mode; /* §NEW:恢复持久化的播放模式(默认列表循环) */
|
||||||
|
g_shuffle_hist_len = 0;
|
||||||
|
g_shuffle_seed = 0;
|
||||||
pthread_mutex_init(&g_play.mu, NULL);
|
pthread_mutex_init(&g_play.mu, NULL);
|
||||||
meow_audio_on_preempt(on_audio_preempt, NULL); /* 被抢占 → 暂停(SDK 读线程分发) */
|
meow_audio_on_preempt(on_audio_preempt, NULL); /* 被抢占 → 暂停(SDK 读线程分发) */
|
||||||
mem_watch_start(); /* 边播边监控(防 OOM):起监控线程读 RSS,驱下载降载 */
|
mem_watch_start(); /* 边播边监控(防 OOM):起监控线程读 RSS,驱下载降载 */
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ static int page_id(const char *name)
|
|||||||
if (strcmp(name, "searchhist") == 0) return ACT_SEARCH; /* §23 空态=历史列表 */
|
if (strcmp(name, "searchhist") == 0) return ACT_SEARCH; /* §23 空态=历史列表 */
|
||||||
if (strcmp(name, "searchpl") == 0) return ACT_SEARCH; /* §23 歌单页签 */
|
if (strcmp(name, "searchpl") == 0) return ACT_SEARCH; /* §23 歌单页签 */
|
||||||
if (strcmp(name, "searchtap") == 0) return ACT_SEARCH; /* §23 误触抑制自检 */
|
if (strcmp(name, "searchtap") == 0) return ACT_SEARCH; /* §23 误触抑制自检 */
|
||||||
|
if (strcmp(name, "modetest") == 0) return ACT_PLAYER; /* §NEW 播放模式三态逻辑自检 */
|
||||||
|
if (strcmp(name, "queuetest") == 0) return ACT_QUEUE; /* §NEW 播放队列页/点播语义自检 */
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,6 +235,16 @@ int main(int argc, char **argv)
|
|||||||
appstate_search_history_add("Jay \u5553\u8868");
|
appstate_search_history_add("Jay \u5553\u8868");
|
||||||
appstate_search_history_add("lofi beat");
|
appstate_search_history_add("lofi beat");
|
||||||
appstate_search_history_add("Jay \u5553\u8868"); /* 去重 → 仍 3 条 */
|
appstate_search_history_add("Jay \u5553\u8868"); /* 去重 → 仍 3 条 */
|
||||||
|
} else if (strcmp(name, "modetest") == 0 || strcmp(name, "queuetest") == 0) {
|
||||||
|
/* §NEW 播放模式 / 播放队列自检:歌单滑窗队列(6 首)+ 播放态 */
|
||||||
|
mock_playlists(); mock_songs();
|
||||||
|
pthread_mutex_lock(&g_play.mu);
|
||||||
|
g_play.state = NCM_PC_PLAYING;
|
||||||
|
g_play.cur_song = 0;
|
||||||
|
g_play.play_mode = NCM_MODE_LISTLOOP;
|
||||||
|
g_play.play_song = g_st.songs[0];
|
||||||
|
g_play.play_song_valid = 1;
|
||||||
|
pthread_mutex_unlock(&g_play.mu);
|
||||||
} else if (strcmp(name, "plsplay") == 0 || strcmp(name, "homeplay") == 0 ||
|
} else if (strcmp(name, "plsplay") == 0 || strcmp(name, "homeplay") == 0 ||
|
||||||
strcmp(name, "barhit") == 0) {
|
strcmp(name, "barhit") == 0) {
|
||||||
/* mini 播放栏目检/命中自检:非播放页(歌单/首页)+ 播放态 → 底部显示 mini 栏 */
|
/* mini 播放栏目检/命中自检:非播放页(歌单/首页)+ 播放态 → 底部显示 mini 栏 */
|
||||||
@@ -254,7 +266,8 @@ int main(int argc, char **argv)
|
|||||||
else if (strcmp(name, "home") == 0) v = home_view_create();
|
else if (strcmp(name, "home") == 0) v = home_view_create();
|
||||||
else if (strcmp(name, "playlist") == 0 || strcmp(name, "plsplay") == 0 ||
|
else if (strcmp(name, "playlist") == 0 || strcmp(name, "plsplay") == 0 ||
|
||||||
strcmp(name, "barhit") == 0) v = playlist_view_create();
|
strcmp(name, "barhit") == 0) v = playlist_view_create();
|
||||||
else if (page_id(name) == ACT_PLAYER) v = player_view_create(); /* player/lands/playerbuf/playererr */
|
else if (page_id(name) == ACT_PLAYER) v = player_view_create(); /* player/lands/playerbuf/playererr/modetest */
|
||||||
|
else if (page_id(name) == ACT_QUEUE) v = queue_view_create(); /* §NEW 播放队列页 */
|
||||||
else if (strcmp(name, "settings") == 0) v = settings_view_create();
|
else if (strcmp(name, "settings") == 0) v = settings_view_create();
|
||||||
else if (strcmp(name, "search") == 0 || strcmp(name, "searchhist") == 0 ||
|
else if (strcmp(name, "search") == 0 || strcmp(name, "searchhist") == 0 ||
|
||||||
strcmp(name, "searchpl") == 0 || strcmp(name, "searchtap") == 0) v = search_view_create();
|
strcmp(name, "searchpl") == 0 || strcmp(name, "searchtap") == 0) v = search_view_create();
|
||||||
@@ -358,6 +371,78 @@ int main(int argc, char **argv)
|
|||||||
if (!ok_tg) return 1;
|
if (!ok_tg) return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (strcmp(name, "modetest") == 0) {
|
||||||
|
/* §NEW 播放模式三态逻辑自检:列表循环(顺序回绕)/单曲循环(自然播完重播)/随机(下一首随机+上一首回溯)。 */
|
||||||
|
int ok = 1;
|
||||||
|
pthread_mutex_lock(&g_play.mu);
|
||||||
|
g_play.cur_song = 0; g_play.state = NCM_PC_PLAYING; g_play.play_mode = NCM_MODE_LISTLOOP;
|
||||||
|
pthread_mutex_unlock(&g_play.mu);
|
||||||
|
int cnt = appstate_queue_count();
|
||||||
|
player_manager_next(1); int c1 = player_manager_playing_song(); /* 0→1 */
|
||||||
|
player_manager_next(-1); int c2 = player_manager_playing_song(); /* 1→0 */
|
||||||
|
player_manager_next(-1); int c3 = player_manager_playing_song(); /* 0→5 回绕 */
|
||||||
|
player_manager_next(1); int c4 = player_manager_playing_song(); /* 5→0 回绕 */
|
||||||
|
ok &= (cnt == 6 && c1 == 1 && c2 == 0 && c3 == 5 && c4 == 0);
|
||||||
|
/* 单曲循环:自然播完 → 重播本曲(cur 不变,state=BUFFER) */
|
||||||
|
player_manager_set_play_mode(NCM_MODE_SINGLE);
|
||||||
|
player_manager_next_intent();
|
||||||
|
int cs = player_manager_playing_song();
|
||||||
|
int st = player_manager_state();
|
||||||
|
ok &= (cs == 0 && st == NCM_PC_BUFFER);
|
||||||
|
/* 单曲循环手上一首:顺序回绕(0→5) */
|
||||||
|
player_manager_next(-1);
|
||||||
|
int c5 = player_manager_playing_song();
|
||||||
|
ok &= (c5 == 5);
|
||||||
|
/* 随机:next 变(≠5),prev 历史回溯回 5 */
|
||||||
|
player_manager_set_play_mode(NCM_MODE_SHUFFLE);
|
||||||
|
player_manager_next(1); int c6 = player_manager_playing_song();
|
||||||
|
player_manager_next(-1); int c7 = player_manager_playing_song();
|
||||||
|
ok &= (c6 >= 0 && c6 < 6 && c6 != 5 && c7 == 5);
|
||||||
|
/* 模式循环:LISTLOOP→SINGLE→SHUFFLE→LISTLOOP */
|
||||||
|
player_manager_set_play_mode(NCM_MODE_LISTLOOP);
|
||||||
|
int m1 = (NCM_MODE_LISTLOOP + 1) % NCM_MODE_COUNT;
|
||||||
|
int m2 = (m1 + 1) % NCM_MODE_COUNT;
|
||||||
|
int m3 = (m2 + 1) % NCM_MODE_COUNT;
|
||||||
|
ok &= (m1 == NCM_MODE_SINGLE && m2 == NCM_MODE_SHUFFLE && m3 == NCM_MODE_LISTLOOP);
|
||||||
|
printf("modetest: listloop=%d single=%d shuffle=%d cycle=%d -> %s\n",
|
||||||
|
(c1 == 1 && c2 == 0 && c3 == 5 && c4 == 0),
|
||||||
|
(cs == 0 && st == NCM_PC_BUFFER && c5 == 5),
|
||||||
|
(c6 != 5 && c7 == 5),
|
||||||
|
(m1 == NCM_MODE_SINGLE && m2 == NCM_MODE_SHUFFLE && m3 == NCM_MODE_LISTLOOP),
|
||||||
|
ok ? "PASS" : "FAIL");
|
||||||
|
if (!ok) return 1;
|
||||||
|
}
|
||||||
|
if (strcmp(name, "queuetest") == 0) {
|
||||||
|
/* §NEW 播放队列:点播切歌保持当前 queue_src(歌单/搜索两源)+ 队列长度一致。 */
|
||||||
|
pthread_mutex_lock(&g_play.mu);
|
||||||
|
g_play.cur_song = 0; g_play.state = NCM_PC_PLAYING; g_play.play_mode = NCM_MODE_LISTLOOP;
|
||||||
|
pthread_mutex_unlock(&g_play.mu);
|
||||||
|
int cnt0 = appstate_queue_count();
|
||||||
|
player_play_queue_song(2); /* PLAYLIST 队列点播第 2 首 */
|
||||||
|
int cur = player_manager_playing_song();
|
||||||
|
int qsrc = appstate_get_queue();
|
||||||
|
int ok1 = (cnt0 == 6 && cur == 2 && qsrc == NCM_QUEUE_PLAYLIST);
|
||||||
|
/* 切到 SEARCH 队列:点播第 1 首,保持 SEARCH */
|
||||||
|
static const char *sn[] = { "S1", "S2", "S3" };
|
||||||
|
appstate_set_queue(NCM_QUEUE_SEARCH);
|
||||||
|
g_st.search_song_count = 3;
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
snprintf(g_st.search_songs[i].id, sizeof g_st.search_songs[i].id, "%d", 2000 + i);
|
||||||
|
snprintf(g_st.search_songs[i].name, sizeof g_st.search_songs[i].name, "%s", sn[i]);
|
||||||
|
snprintf(g_st.search_songs[i].art_name, sizeof g_st.search_songs[i].art_name, "A%d", i);
|
||||||
|
snprintf(g_st.search_songs[i].album_name, sizeof g_st.search_songs[i].album_name, "Al%d", i);
|
||||||
|
g_st.search_songs[i].duration_ms = 100000;
|
||||||
|
}
|
||||||
|
int cnt1 = appstate_queue_count();
|
||||||
|
player_play_queue_song(1);
|
||||||
|
int cur2 = player_manager_playing_song();
|
||||||
|
int qsrc2 = appstate_get_queue();
|
||||||
|
int ok2 = (cnt1 == 3 && cur2 == 1 && qsrc2 == NCM_QUEUE_SEARCH);
|
||||||
|
printf("queuetest: playlist=%s search=%s -> %s\n", ok1 ? "PASS" : "FAIL",
|
||||||
|
ok2 ? "PASS" : "FAIL", (ok1 && ok2) ? "PASS" : "FAIL");
|
||||||
|
if (!(ok1 && ok2)) return 1;
|
||||||
|
}
|
||||||
|
|
||||||
meow_ui_shutdown();
|
meow_ui_shutdown();
|
||||||
printf("dumped %s -> %s\n", name, dump);
|
printf("dumped %s -> %s\n", name, dump);
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -89,7 +89,11 @@ static ncm_rect_t g_cover_r; /* 封面页静态带(方形大图
|
|||||||
static ncm_rect_t g_dyn_r; /* 封面页动态带(元信息/进度/控制/指示点) */
|
static ncm_rect_t g_dyn_r; /* 封面页动态带(元信息/进度/控制/指示点) */
|
||||||
static int g_title_y, g_artist_y, g_qual_y, g_times_y;
|
static int g_title_y, g_artist_y, g_qual_y, g_times_y;
|
||||||
static ncm_rect_t g_bar;
|
static ncm_rect_t g_bar;
|
||||||
static int g_ctl_y, g_prev_x, g_play_x, g_next_x, g_r_side, g_r_play, g_dots_y;
|
static int g_ctl_y, g_prev_x, g_play_x, g_next_x, g_mode_x, g_queue_x, g_r_side, g_r_play, g_dots_y;
|
||||||
|
/* §NEW 模式/队列钮:三键左(mode,prev 更左)/ 三键右(queue,next 更右)。间距 60 保不遮挡。 */
|
||||||
|
#define GAP_SIDE 60
|
||||||
|
#define MODE_BTN_R (g_r_side) /* 与侧钮同半径(命中取 CTL_RR) */
|
||||||
|
#define MODE_LBL_SZ 14 /* 模式/队列钮文字字号(两字,居中) */
|
||||||
|
|
||||||
/* ---- 歌词页几何 ---- */
|
/* ---- 歌词页几何 ---- */
|
||||||
static int g_lyr_cx; /* 歌词垂直中心(屏中线) */
|
static int g_lyr_cx; /* 歌词垂直中心(屏中线) */
|
||||||
@@ -284,6 +288,18 @@ static void draw_ctrl_btn(int icon, int cx, int cy, int r, uint32_t circle, uint
|
|||||||
draw_md3_icon(icon, cx, cy, (r * 4) / 3, fg);
|
draw_md3_icon(icon, cx, cy, (r * 4) / 3, fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 文字钮:圆形底 + 居中两字文案(§NEW 模式/队列钮)。无逐帧三角行扫,纯圆形填充。 */
|
||||||
|
static void draw_text_btn(const char *label, int cx, int cy, int r, uint32_t circle, uint32_t fg)
|
||||||
|
{
|
||||||
|
for (int dy = -r; dy <= r; dy++) {
|
||||||
|
int half = (int)sqrt((double)(r * r - dy * dy));
|
||||||
|
if (half <= 0) continue;
|
||||||
|
meow_ui_fill_rect(cx - half, cy + dy, 2 * half, 1, circle);
|
||||||
|
}
|
||||||
|
if (label && label[0])
|
||||||
|
meow_ui_text_center(cx, cy - MODE_LBL_SZ / 2, label, MODE_LBL_SZ, fg);
|
||||||
|
}
|
||||||
|
|
||||||
/* 圆角封面四角裁掉:把方形图四角(圆角外-方形内)盖回背景色(廉价圆角,无 GPU 裁剪)。 */
|
/* 圆角封面四角裁掉:把方形图四角(圆角外-方形内)盖回背景色(廉价圆角,无 GPU 裁剪)。 */
|
||||||
static void round_corners_bg(int x, int y, int w, int h, int r, uint32_t bg)
|
static void round_corners_bg(int x, int y, int w, int h, int r, uint32_t bg)
|
||||||
{
|
{
|
||||||
@@ -335,6 +351,8 @@ static void player_compute_geom(void)
|
|||||||
g_times_y = g_bar.y + g_bar.h + 12; /* 500 */
|
g_times_y = g_bar.y + g_bar.h + 12; /* 500 */
|
||||||
g_ctl_y = g_times_y + 64; /* 564 */
|
g_ctl_y = g_times_y + 64; /* 564 */
|
||||||
g_prev_x = W / 2 - 92; g_play_x = W / 2; g_next_x = W / 2 + 92;
|
g_prev_x = W / 2 - 92; g_play_x = W / 2; g_next_x = W / 2 + 92;
|
||||||
|
g_mode_x = g_prev_x - GAP_SIDE; /* 播放模式三态钮(prev 更左) */
|
||||||
|
g_queue_x = g_next_x + GAP_SIDE; /* 当前播放队列钮(next 更右) */
|
||||||
g_r_side = 24; g_r_play = 34;
|
g_r_side = 24; g_r_play = 34;
|
||||||
g_dots_y = g_ctl_y + 62; /* 626 */
|
g_dots_y = g_ctl_y + 62; /* 626 */
|
||||||
|
|
||||||
@@ -449,6 +467,12 @@ static void plr_draw_cover_dyn(void *ctx)
|
|||||||
g_play_x, g_ctl_y, g_r_play, pa->red, pa->on_red);
|
g_play_x, g_ctl_y, g_r_play, pa->red, pa->on_red);
|
||||||
draw_ctrl_btn(NCM_I_NEXT, g_next_x, g_ctl_y, g_r_side, PLR_CTRL_BG, PLR_CTRL_FG);
|
draw_ctrl_btn(NCM_I_NEXT, g_next_x, g_ctl_y, g_r_side, PLR_CTRL_BG, PLR_CTRL_FG);
|
||||||
|
|
||||||
|
/* §NEW 播放模式三态钮(三键左,prev 更左)+ 当前播放队列钮(三键右,next 更右):
|
||||||
|
* 文案反映当前模式(随机/列表/单曲);队列钮进入实时播放列表。 */
|
||||||
|
draw_text_btn(ncm_mode_name(player_manager_play_mode()), g_mode_x, g_ctl_y,
|
||||||
|
MODE_BTN_R, PLR_CTRL_BG, PLR_CTRL_FG);
|
||||||
|
draw_text_btn("\u961f\u5217", g_queue_x, g_ctl_y, MODE_BTN_R, PLR_CTRL_BG, PLR_CTRL_FG); /* 队列 */
|
||||||
|
|
||||||
/* 页面指示点 */
|
/* 页面指示点 */
|
||||||
draw_page_dots(g_dots_y);
|
draw_page_dots(g_dots_y);
|
||||||
}
|
}
|
||||||
@@ -604,7 +628,9 @@ void player_touch_down(int x, int y)
|
|||||||
interactive = 1;
|
interactive = 1;
|
||||||
if (in_circle(g_prev_x, g_ctl_y, CTL_RR, x, y) ||
|
if (in_circle(g_prev_x, g_ctl_y, CTL_RR, x, y) ||
|
||||||
in_circle(g_play_x, g_ctl_y, g_r_play + 6, x, y) ||
|
in_circle(g_play_x, g_ctl_y, g_r_play + 6, x, y) ||
|
||||||
in_circle(g_next_x, g_ctl_y, CTL_RR, x, y))
|
in_circle(g_next_x, g_ctl_y, CTL_RR, x, y) ||
|
||||||
|
in_circle(g_mode_x, g_ctl_y, CTL_RR, x, y) ||
|
||||||
|
in_circle(g_queue_x, g_ctl_y, CTL_RR, x, y))
|
||||||
interactive = 1;
|
interactive = 1;
|
||||||
}
|
}
|
||||||
g_touch_on = (x > PLR_EDGE_W) && (y >= meow_ui_statusbar_hotzone()) && !interactive;
|
g_touch_on = (x > PLR_EDGE_W) && (y >= meow_ui_statusbar_hotzone()) && !interactive;
|
||||||
@@ -675,6 +701,19 @@ void player_on_tap(int x, int y)
|
|||||||
}
|
}
|
||||||
if (in_circle(g_prev_x, g_ctl_y, CTL_RR, x, y)) { player_manager_next(-1); return; }
|
if (in_circle(g_prev_x, g_ctl_y, CTL_RR, x, y)) { player_manager_next(-1); return; }
|
||||||
if (in_circle(g_next_x, g_ctl_y, CTL_RR, x, y)) { player_manager_next(1); return; }
|
if (in_circle(g_next_x, g_ctl_y, CTL_RR, x, y)) { player_manager_next(1); return; }
|
||||||
|
|
||||||
|
/* §NEW 播放模式三态钮:点击循环切换(SHUFFLE→LISTLOOP→SINGLE→SHUFFLE)+ 持久化 */
|
||||||
|
if (in_circle(g_mode_x, g_ctl_y, CTL_RR, x, y)) {
|
||||||
|
int m = player_manager_play_mode();
|
||||||
|
int nxt = (m + 1) % NCM_MODE_COUNT;
|
||||||
|
player_manager_set_play_mode(nxt);
|
||||||
|
appstate_apply_play_mode(nxt); /* 记住模式(退出后保留) */
|
||||||
|
player_dyn_invalidate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* §NEW 当前播放队列钮:进实时播放列表页 */
|
||||||
|
if (in_circle(g_queue_x, g_ctl_y, CTL_RR, x, y)) { queue_open(); return; }
|
||||||
|
|
||||||
if (y >= g_bar.y - 10 && y <= g_bar.y + g_bar.h + 10 && x >= g_bar.x && x <= g_bar.x + g_bar.w) {
|
if (y >= g_bar.y - 10 && y <= g_bar.y + g_bar.h + 10 && x >= g_bar.x && x <= g_bar.x + g_bar.w) {
|
||||||
int64_t dur = player_manager_dur_ms();
|
int64_t dur = player_manager_dur_ms();
|
||||||
if (dur > 0) player_seek((int64_t)(x - g_bar.x) * dur / g_bar.w);
|
if (dur > 0) player_seek((int64_t)(x - g_bar.x) * dur / g_bar.w);
|
||||||
@@ -731,6 +770,28 @@ void player_play_search_song(int i)
|
|||||||
meow_view_page_push(ACT_PLAYER);
|
meow_view_page_push(ACT_PLAYER);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 从「当前实时播放队列」第 idx 首播放(§NEW 队列页点播):保持当前 queue_src 不变
|
||||||
|
* (不把队列切回歌单/不覆写歌单窗口)——上/下一首仍在该队列内循环。封面用窗口/搜索数组
|
||||||
|
* 内的元数据(不触发网络),占位行由播放线程物化后快照接管。 */
|
||||||
|
void player_play_queue_song(int idx)
|
||||||
|
{
|
||||||
|
int cnt = appstate_queue_count();
|
||||||
|
if (idx < 0 || idx >= cnt) return;
|
||||||
|
ncm_song_t s;
|
||||||
|
int have = 0;
|
||||||
|
if (appstate_get_queue() == NCM_QUEUE_SEARCH) {
|
||||||
|
if (appstate_search_song(idx, &s) == 0) have = 1;
|
||||||
|
} else if (appstate_song_copy(idx, &s) == 0) {
|
||||||
|
have = 1;
|
||||||
|
}
|
||||||
|
const char *cover = have ? (s.al_pic[0] ? s.al_pic : g_st.cover_url) : g_st.cover_url;
|
||||||
|
player_manager_play(idx);
|
||||||
|
appstate_set_playing_song(idx, NULL);
|
||||||
|
if (have) cover_request(cover, s.id);
|
||||||
|
g_ppage = 0;
|
||||||
|
meow_view_page_push(ACT_PLAYER);
|
||||||
|
}
|
||||||
|
|
||||||
void player_pause_toggle(int on)
|
void player_pause_toggle(int on)
|
||||||
{
|
{
|
||||||
if (player_manager_state() == NCM_PC_PAUSED) player_manager_pause(0);
|
if (player_manager_state() == NCM_PC_PAUSED) player_manager_pause(0);
|
||||||
|
|||||||
@@ -0,0 +1,377 @@
|
|||||||
|
/* ui/queue.c — 当前实时播放队列页(ACT_QUEUE,§NEW:播放页「队列」钮进入)。
|
||||||
|
*
|
||||||
|
* 展示**当前播放队列**(player_manager 的 queue_src —— 歌单滑动窗口 或 搜索单曲结果集),
|
||||||
|
* 含当前曲高亮、可点某行切到那首播放。数据源与分页复用歌单滑动窗口(REPORT §17):
|
||||||
|
* - count = appstate_queue_count()(全量:track_ids_count 或 search_song_count,可 5194 首);
|
||||||
|
* - 行内容 = src2 逐行按 queue_src 取(PLAYLIST→滑窗拷贝,SEARCH→搜索数组),未物化槽显示
|
||||||
|
* ".../加载中" 占位,视口 ±24 行未物化 → 后台 worker ensure_range 补载(盘缓存优先,批≤NCM_SONG_BATCH);
|
||||||
|
* - 列表本身由 meow_listview 虚拟化(只画视口内行),不整歌单常驻内存 → 任意长度不卡死。
|
||||||
|
* - SEARCH 队列行全在内存(≤256),无需 ensure;PLAYLIST 队列走滑窗+ensure(含网络)。
|
||||||
|
* 当前曲高亮 = 播放页三态钮旁队列页以红色左条 + 红色轻染标记 player_manager_playing_song()
|
||||||
|
* 所在行(queue idx 与播放队列下标一致,两种 queue_src 通用)。
|
||||||
|
*
|
||||||
|
* 布局(无封面头块,纯列表):topbar「播放列表」+ 曲目列表 + 底部 `first-last / N 首` 页脚。
|
||||||
|
* 左缘返回 = 弹回上一页(播放页,app.c);点击行 → player_play_queue_song(保持当前 queue_src)。
|
||||||
|
*/
|
||||||
|
#include "../ncm_app_internal.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
|
||||||
|
typedef struct { meow_view *view; int active; } queue_ctrl;
|
||||||
|
static queue_ctrl g_qc;
|
||||||
|
|
||||||
|
static meow_widget *g_tb;
|
||||||
|
static meow_widget *g_list;
|
||||||
|
static ncm_rect_t r_cont, r_list;
|
||||||
|
static int g_bg_id, g_draw_id, g_empty_id, g_footer_id;
|
||||||
|
static int g_press_row, g_press_delta, g_dragging, g_drag_last_y;
|
||||||
|
static int g_last_count, g_last_cur;
|
||||||
|
|
||||||
|
/* 队列补载(PLAYLIST)后台 worker 参数:目标区间 [lo,hi)(全局下标)。SEARCH 无需补载。 */
|
||||||
|
static pthread_mutex_t g_qmu = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
static pthread_t g_qthr;
|
||||||
|
static int g_qthr_on;
|
||||||
|
static volatile int g_ens_lo, g_ens_hi;
|
||||||
|
static volatile int g_ens_err;
|
||||||
|
static volatile int g_ens_done; /* worker 已完成的代次(tick 消费) */
|
||||||
|
static volatile int g_ens_acked;
|
||||||
|
|
||||||
|
static void q_drag_reset(void)
|
||||||
|
{
|
||||||
|
g_press_row = -1; g_press_delta = 0; g_dragging = 0; g_drag_last_y = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 队列第 idx 行的 ncm_song_t:按 queue_src 取。返回 0=已物化(out 有效);-1=未物化/占位。
|
||||||
|
* PLAYLIST:滑窗拷贝(UI 线程不触发网络);SEARCH:搜索数组(内存)。 */
|
||||||
|
static int q_song_get(int idx, ncm_song_t *out)
|
||||||
|
{
|
||||||
|
if (appstate_get_queue() == NCM_QUEUE_SEARCH)
|
||||||
|
return (appstate_search_song(idx, out) == 0) ? 0 : -1;
|
||||||
|
return (appstate_song_copy(idx, out) == 0) ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 列表数据源(meow_list2_source):逐行按需填充(不做 IO,§5.5)。未物化槽显示占位行。 */
|
||||||
|
static int q_src2(int idx, meow_list_item *out, void *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
int cnt = appstate_queue_count();
|
||||||
|
if (idx < 0 || idx >= cnt) return 0;
|
||||||
|
static ncm_song_t s_s;
|
||||||
|
static char sub[256];
|
||||||
|
if (q_song_get(idx, &s_s) != 0) {
|
||||||
|
out->icon_kind = MEOW_LIST_ICON_NONE;
|
||||||
|
out->title = "...";
|
||||||
|
out->subtitle = "加载中";
|
||||||
|
out->right = MEOW_LIST_RIGHT_NONE;
|
||||||
|
out->on = 0; out->enabled = 1;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (s_s.art_name[0] && s_s.album_name[0]) snprintf(sub, sizeof sub, "%s / %s", s_s.art_name, s_s.album_name);
|
||||||
|
else if (s_s.art_name[0]) snprintf(sub, sizeof sub, "%s", s_s.art_name);
|
||||||
|
else if (s_s.album_name[0]) snprintf(sub, sizeof sub, "%s", s_s.album_name);
|
||||||
|
else snprintf(sub, sizeof sub, "-");
|
||||||
|
out->icon_kind = MEOW_LIST_ICON_NONE;
|
||||||
|
out->title = s_s.name;
|
||||||
|
out->subtitle = sub;
|
||||||
|
out->right = MEOW_LIST_RIGHT_NONE;
|
||||||
|
out->on = 0; out->enabled = 1;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 行背景(z=5,画在 listview 文本之下):每行 surfaceContainer 圆角卡 + 「当前播放行」红色左条/轻染。 */
|
||||||
|
static void q_row_bg(void *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
if (meow_ui_activity_active() != ACT_QUEUE) return;
|
||||||
|
int cnt = appstate_queue_count();
|
||||||
|
if (cnt <= 0) return;
|
||||||
|
const meow_theme *th = meow_theme_get();
|
||||||
|
const ncm_pal_t *pa = ncm_palette(g_dark);
|
||||||
|
int item = APP_ROW_H;
|
||||||
|
int off = meow_listview_scroll(g_list);
|
||||||
|
int first = off / item, last = (off + r_list.h) / item + 1;
|
||||||
|
if (last > cnt) last = cnt;
|
||||||
|
int cur = player_manager_playing_song();
|
||||||
|
for (int i = first; i < last; i++) {
|
||||||
|
int ry = r_list.y + (i * item - off) + 1, th2 = item - 2;
|
||||||
|
if (ry < r_list.y || ry + th2 > r_list.y + r_list.h) continue; /* 只画完整落在视口内的行 */
|
||||||
|
if (i == cur) {
|
||||||
|
/* 当前播放行:红色左条 + 红色轻染(高亮),底用 red_ctr 浅红 */
|
||||||
|
ncm_fill_round(r_list.x, ry, r_list.w, th2, 12, pa->red_ctr);
|
||||||
|
meow_ui_fill_rect(r_list.x + 2, ry + 4, 4, th2 - 8, pa->red);
|
||||||
|
} else {
|
||||||
|
ncm_fill_round(r_list.x, ry, r_list.w, th2, 12, th->surfaceContainer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主绘制(z=60):当前行红色 mini-vinyl 缩略座标(文本由 listview z=10 画)。 */
|
||||||
|
static void q_draw(void *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
if (meow_ui_activity_active() != ACT_QUEUE) return;
|
||||||
|
const ncm_pal_t *pa = ncm_palette(g_dark);
|
||||||
|
int cnt = appstate_queue_count();
|
||||||
|
if (cnt <= 0) return;
|
||||||
|
int item = APP_ROW_H, isz = 24;
|
||||||
|
int off = meow_listview_scroll(g_list);
|
||||||
|
int first = off / item, last = (off + r_list.h) / item + 1;
|
||||||
|
if (last > cnt) last = cnt;
|
||||||
|
for (int i = first; i < last; i++) {
|
||||||
|
int ry = r_list.y + (i * item - off) + 1, th2 = item - 2;
|
||||||
|
if (ry < r_list.y || ry + th2 > r_list.y + r_list.h) continue;
|
||||||
|
int icy = ry + (item - isz) / 2, bx = r_list.x + 16;
|
||||||
|
ncm_fill_round(bx, icy, isz, isz, 6, pa->red);
|
||||||
|
meow_ui_fill_rect(bx + isz / 2 - 2, icy + isz / 2 - 2, 4, 4, pa->on_red);
|
||||||
|
if (g_press_row == i) {
|
||||||
|
meow_ui_fill_rect(r_list.x, ry, 4, item - 2, pa->red);
|
||||||
|
meow_ui_fill_rect_alpha(r_list.x, ry, r_list.w, item - 2, pa->red, 26);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void q_draw_empty(void *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
if (meow_ui_activity_active() != ACT_QUEUE) return;
|
||||||
|
if (appstate_queue_count() != 0) return;
|
||||||
|
ncm_draw_empty(r_list, "暂无播放队列", "播放一首歌曲后在此查看列表");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void q_draw_footer(void *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
if (meow_ui_activity_active() != ACT_QUEUE) return;
|
||||||
|
int total = appstate_queue_count();
|
||||||
|
if (total <= 0) return;
|
||||||
|
int first = meow_listview_scroll(g_list) / APP_ROW_H + 1;
|
||||||
|
int last = first + r_list.h / APP_ROW_H;
|
||||||
|
if (last > total) last = total;
|
||||||
|
char foot[96];
|
||||||
|
snprintf(foot, sizeof foot, "%d-%d / %d 首", first, last, total);
|
||||||
|
ncm_draw_footer(r_cont, foot);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void q_apply(void)
|
||||||
|
{
|
||||||
|
if (g_list) {
|
||||||
|
int cnt = appstate_queue_count();
|
||||||
|
meow_listview_set_count(g_list, cnt);
|
||||||
|
if (cnt != g_last_count) { g_last_count = cnt; meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h); }
|
||||||
|
}
|
||||||
|
const meow_theme *th = meow_theme_get();
|
||||||
|
meow_ui_set_page_bg(th->bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void *queue_worker(void *arg)
|
||||||
|
{
|
||||||
|
(void)arg;
|
||||||
|
int lo, hi;
|
||||||
|
pthread_mutex_lock(&g_qmu);
|
||||||
|
lo = g_ens_lo; hi = g_ens_hi;
|
||||||
|
pthread_mutex_unlock(&g_qmu);
|
||||||
|
int rc = 0;
|
||||||
|
if (!g_host_mock) {
|
||||||
|
/* §26:滑窗补载(盘缓存优先,批网格补载);非致命(部分失败留占位行) */
|
||||||
|
rc = (lo < hi) ? appstate_song_ensure_range(lo, hi) : NCM_OK;
|
||||||
|
}
|
||||||
|
pthread_mutex_lock(&g_qmu);
|
||||||
|
g_ens_err = rc;
|
||||||
|
g_ens_done++;
|
||||||
|
pthread_mutex_unlock(&g_qmu);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 视口驱动滑窗补载(仅 PLAYLIST 队列):视口 ±24 行有未物化槽 → 发 ensure worker。
|
||||||
|
* SEARCH 队列全在内存,无需补载。 */
|
||||||
|
static void q_maybe_ensure(void)
|
||||||
|
{
|
||||||
|
if (appstate_get_queue() != NCM_QUEUE_PLAYLIST) return;
|
||||||
|
if (g_qthr_on || g_host_mock) return;
|
||||||
|
int total = appstate_queue_count();
|
||||||
|
if (total <= 0) return;
|
||||||
|
int scroll = meow_listview_scroll(g_list);
|
||||||
|
int first = scroll / APP_ROW_H;
|
||||||
|
int last = (scroll + r_list.h) / APP_ROW_H + 1;
|
||||||
|
int lo = first - 24, hi = last + 24;
|
||||||
|
if (lo < 0) lo = 0;
|
||||||
|
if (hi > total) hi = total;
|
||||||
|
if (lo >= hi) return;
|
||||||
|
if (appstate_range_loaded(lo, hi)) return; /* 已物化:无操作 */
|
||||||
|
pthread_mutex_lock(&g_qmu);
|
||||||
|
g_ens_lo = lo; g_ens_hi = hi;
|
||||||
|
pthread_mutex_unlock(&g_qmu);
|
||||||
|
g_qthr_on = 1;
|
||||||
|
if (ncm_thread_create(&g_qthr, queue_worker, NULL) != 0) {
|
||||||
|
g_qthr_on = 0; ncm_snack("加载失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void queue_open(void)
|
||||||
|
{
|
||||||
|
/* 从播放页「队列」钮进入:push 本页后按当前 queue 刷新(首屏立刻有 count/行)。 */
|
||||||
|
meow_view_page_push(ACT_QUEUE);
|
||||||
|
q_apply();
|
||||||
|
meow_ui_paint_full();
|
||||||
|
q_maybe_ensure();
|
||||||
|
}
|
||||||
|
|
||||||
|
int queue_is_active(void) { return g_qc.active; }
|
||||||
|
|
||||||
|
int queue_on_tap(int x, int y)
|
||||||
|
{
|
||||||
|
if (g_tb) {
|
||||||
|
int ah = meow_topappbar_hit(g_tb, x, y);
|
||||||
|
if (ah == MEOW_TOPBAR_BACK) { q_drag_reset(); meow_view_page_pop(); return 1; }
|
||||||
|
}
|
||||||
|
if (meow_widget_hit(g_list, x, y)) {
|
||||||
|
int was_drag = (g_press_delta > 10);
|
||||||
|
int row = (g_press_row >= 0) ? g_press_row : meow_listview_hit(g_list, x, y);
|
||||||
|
q_drag_reset();
|
||||||
|
if (row < 0 || was_drag) return 1;
|
||||||
|
player_play_queue_song(row); /* 点播:保持当前 queue_src,占位行由播放线程物化 */
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void queue_touch_down(int x, int y)
|
||||||
|
{
|
||||||
|
q_drag_reset();
|
||||||
|
if (appstate_queue_count() <= 0) return;
|
||||||
|
if (meow_widget_hit(g_list, x, y)) {
|
||||||
|
int row = meow_listview_hit(g_list, x, y);
|
||||||
|
if (row >= 0) g_press_row = row;
|
||||||
|
g_dragging = 1; g_drag_last_y = y;
|
||||||
|
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void queue_touch_move(int x, int y)
|
||||||
|
{
|
||||||
|
(void)x;
|
||||||
|
if (!g_dragging) return;
|
||||||
|
if (g_press_row >= 0) {
|
||||||
|
int d = y - g_drag_last_y; if (d < 0) d = -d;
|
||||||
|
g_press_delta += d;
|
||||||
|
if (g_press_delta > 10) g_press_row = -1;
|
||||||
|
}
|
||||||
|
int dy = y - g_drag_last_y; g_drag_last_y = y;
|
||||||
|
if (dy == 0) return;
|
||||||
|
int before = meow_listview_scroll(g_list);
|
||||||
|
meow_listview_scroll_by(g_list, -dy);
|
||||||
|
if (meow_listview_scroll(g_list) != before) {
|
||||||
|
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
|
||||||
|
q_maybe_ensure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void queue_swipe(int dir, int dist)
|
||||||
|
{
|
||||||
|
if (dir != MEOW_SWIPE_UP && dir != MEOW_SWIPE_DOWN) return;
|
||||||
|
if (appstate_queue_count() <= 0) return;
|
||||||
|
int del = (dir == MEOW_SWIPE_UP) ? dist : -dist;
|
||||||
|
int before = meow_listview_scroll(g_list);
|
||||||
|
meow_listview_scroll_by(g_list, del);
|
||||||
|
if (meow_listview_scroll(g_list) != before) {
|
||||||
|
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
|
||||||
|
q_maybe_ensure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void queue_tick(uint64_t now)
|
||||||
|
{
|
||||||
|
(void)now;
|
||||||
|
chrome_tick();
|
||||||
|
if (!g_qc.active) return;
|
||||||
|
/* worker 补载完成 → 刷新列表(占位行自愈) */
|
||||||
|
if (g_qthr_on) {
|
||||||
|
int done, acked;
|
||||||
|
pthread_mutex_lock(&g_qmu);
|
||||||
|
done = g_ens_done; acked = g_ens_acked;
|
||||||
|
pthread_mutex_unlock(&g_qmu);
|
||||||
|
if (done != acked) {
|
||||||
|
pthread_mutex_lock(&g_qmu);
|
||||||
|
g_ens_acked = done;
|
||||||
|
pthread_mutex_unlock(&g_qmu);
|
||||||
|
g_qthr_on = 0;
|
||||||
|
if (g_ens_err != 0) ncm_snack("加载失败");
|
||||||
|
q_apply();
|
||||||
|
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* 当前播放曲变化(切歌/顺延)→ 重画高亮行 */
|
||||||
|
int cur = player_manager_playing_song();
|
||||||
|
if (cur != g_last_cur) {
|
||||||
|
g_last_cur = cur;
|
||||||
|
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void q_page_create(meow_view *v, void *ctx)
|
||||||
|
{
|
||||||
|
(void)v;
|
||||||
|
((queue_ctrl *)ctx)->view = v;
|
||||||
|
r_cont = ncm_content_rect();
|
||||||
|
r_list = NR(16, r_cont.y + 8, r_cont.w - 32,
|
||||||
|
r_cont.y + r_cont.h - APP_FOOTER_H - (r_cont.y + 8));
|
||||||
|
if (r_list.h < 0) r_list.h = 0;
|
||||||
|
meow_ui_activity_push(ACT_QUEUE);
|
||||||
|
g_tb = ncm_topbar_create("播放列表", 1, NULL);
|
||||||
|
g_list = meow_listview_create(r_list.x, r_list.y, r_list.w, r_list.h, 0,
|
||||||
|
q_src2, NULL, MEOW_LIST_SEL_NONE);
|
||||||
|
meow_listview_set_item_h(g_list, APP_ROW_H);
|
||||||
|
meow_ui_activity_pop();
|
||||||
|
meow_widget_set_z(g_list, 10);
|
||||||
|
g_bg_id = meow_draw_custom_register(5, r_list.x, r_list.y, r_list.w, r_list.h,
|
||||||
|
q_row_bg, NULL);
|
||||||
|
g_draw_id = meow_draw_custom_register(60, r_cont.x, r_cont.y, r_cont.w, r_cont.h,
|
||||||
|
q_draw, NULL);
|
||||||
|
g_empty_id = meow_draw_custom_register(1, r_list.x, r_list.y, r_list.w, r_list.h,
|
||||||
|
q_draw_empty, NULL);
|
||||||
|
g_footer_id = meow_draw_custom_register(1, r_cont.x, r_cont.y + r_cont.h - APP_FOOTER_H,
|
||||||
|
r_cont.w, APP_FOOTER_H, q_draw_footer, NULL);
|
||||||
|
q_drag_reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void q_page_enter(meow_view *v, void *ctx)
|
||||||
|
{
|
||||||
|
(void)v;
|
||||||
|
((queue_ctrl *)ctx)->active = 1;
|
||||||
|
const meow_theme *th = meow_theme_get();
|
||||||
|
meow_ui_set_page_bg(th->bg);
|
||||||
|
g_last_count = -1; g_last_cur = -2;
|
||||||
|
q_apply();
|
||||||
|
meow_ui_paint_full();
|
||||||
|
q_maybe_ensure();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void q_page_leave(meow_view *v, void *ctx)
|
||||||
|
{
|
||||||
|
(void)v;
|
||||||
|
((queue_ctrl *)ctx)->active = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void q_page_destroy(meow_view *v, void *ctx)
|
||||||
|
{
|
||||||
|
(void)v; (void)ctx;
|
||||||
|
if (g_bg_id) { meow_draw_unregister(g_bg_id); g_bg_id = 0; }
|
||||||
|
if (g_draw_id) { meow_draw_unregister(g_draw_id); g_draw_id = 0; }
|
||||||
|
if (g_empty_id) { meow_draw_unregister(g_empty_id); g_empty_id = 0; }
|
||||||
|
if (g_footer_id){ meow_draw_unregister(g_footer_id);g_footer_id = 0; }
|
||||||
|
if (g_list) { meow_widget_free(g_list); g_list = NULL; }
|
||||||
|
if (g_tb) { meow_widget_free(g_tb); g_tb = NULL; }
|
||||||
|
}
|
||||||
|
|
||||||
|
meow_view *queue_view_create(void)
|
||||||
|
{
|
||||||
|
static const meow_view_hooks h = {
|
||||||
|
.on_create = q_page_create,
|
||||||
|
.on_enter = q_page_enter,
|
||||||
|
.on_leave = q_page_leave,
|
||||||
|
.on_destroy = q_page_destroy,
|
||||||
|
};
|
||||||
|
return meow_view_create(ACT_QUEUE, MEOW_VIEW_ACTIVITY, &h, &g_qc);
|
||||||
|
}
|
||||||
@@ -58,6 +58,8 @@ size_t ncm_strlcpy(char *dst, const char *src, size_t sz)
|
|||||||
}
|
}
|
||||||
void meow_log(meow_log_level lvl, const char *fmt, ...) { (void)lvl; (void)fmt; }
|
void meow_log(meow_log_level lvl, const char *fmt, ...) { (void)lvl; (void)fmt; }
|
||||||
void meow_audio_on_preempt(meow_audio_preempt_fn cb, void *ctx) { (void)cb; (void)ctx; }
|
void meow_audio_on_preempt(meow_audio_preempt_fn cb, void *ctx) { (void)cb; (void)ctx; }
|
||||||
|
/* core/ncm_common.c 提供 ncm_now_ms(墙钟毫秒);本 sim 不链 ncm_common.o,提供同语义桩。 */
|
||||||
|
int64_t ncm_now_ms(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (int64_t)ts.tv_sec*1000 + ts.tv_nsec/1000000; }
|
||||||
|
|
||||||
/* ---- appstate stub:窗内全命中,id=1000+idx;fee 可注入(§20 P8) ---- */
|
/* ---- appstate stub:窗内全命中,id=1000+idx;fee 可注入(§20 P8) ---- */
|
||||||
static int g_fee = 0;
|
static int g_fee = 0;
|
||||||
@@ -315,6 +317,8 @@ void audio_out_set_target(int ms) { (void)ms; }
|
|||||||
/* ---- 歌词(未被调用的 worker 会被 gc-sections 丢弃;lyric_clear 由 worker 用,防御性提供) ---- */
|
/* ---- 歌词(未被调用的 worker 会被 gc-sections 丢弃;lyric_clear 由 worker 用,防御性提供) ---- */
|
||||||
void lyric_clear(void) { }
|
void lyric_clear(void) { }
|
||||||
int lyric_parse(const char *a, const char *b) { (void)a; (void)b; return 0; }
|
int lyric_parse(const char *a, const char *b) { (void)a; (void)b; return 0; }
|
||||||
|
/* 批3:player_manager 的 lyric_worker 会上报歌词到媒体会话;本 sim 不链 ipc_media.c,提供空桩。 */
|
||||||
|
void ipc_media_lyric_report(const char *lrc, const char *tl) { (void)lrc; (void)tl; }
|
||||||
|
|
||||||
static int g_fail;
|
static int g_fail;
|
||||||
#define CHECK(cond, fmt, ...) do { \
|
#define CHECK(cond, fmt, ...) do { \
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ CORE="core/ncm_common.c core/ncm_json.c core/ncm_store.c core/ncm_http.c \
|
|||||||
core/ncm_crypto.c core/ncm_req.c core/ncm_login.c core/ncm_api.c \
|
core/ncm_crypto.c core/ncm_req.c core/ncm_login.c core/ncm_api.c \
|
||||||
core/ncm_cache.c core/ncm_ctx.c core/ncm_apicache.c"
|
core/ncm_cache.c core/ncm_ctx.c core/ncm_apicache.c"
|
||||||
APP="app/app.c app/ui/login.c app/ui/home.c app/ui/playlist.c app/ui/player.c \
|
APP="app/app.c app/ui/login.c app/ui/home.c app/ui/playlist.c app/ui/player.c \
|
||||||
app/ui/settings.c app/ui/search.c app/ui/minibar.c app/ui/ncm_colors.c app/ui/chrome.c \
|
app/ui/settings.c app/ui/search.c app/ui/minibar.c app/ui/queue.c app/ui/ncm_colors.c app/ui/chrome.c \
|
||||||
app/logic/appstate.c app/logic/lyric.c app/logic/songmeta.c \
|
app/logic/appstate.c app/logic/lyric.c app/logic/songmeta.c \
|
||||||
app/logic/cover.c app/logic/account.c app/play/player_manager.c app/play/audio_out.c \
|
app/logic/cover.c app/logic/account.c app/play/player_manager.c app/play/audio_out.c \
|
||||||
app/play/ipc_media.c app/play/decoder.c app/play/stream_src.c app/play/minimp3_impl.c \
|
app/play/ipc_media.c app/play/decoder.c app/play/stream_src.c app/play/minimp3_impl.c \
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ $CC_HOST -O2 -c -I"$SDK_DIR" -I"$ROOT/patch_v2/meow_launcherd" "$SDK_DIR/test_ho
|
|||||||
|
|
||||||
echo "==> host app sources (exclude app.c: has main + g_st; test provides them)"
|
echo "==> host app sources (exclude app.c: has main + g_st; test provides them)"
|
||||||
APP_SRC="app/ui/login.c app/ui/home.c app/ui/playlist.c app/ui/player.c \
|
APP_SRC="app/ui/login.c app/ui/home.c app/ui/playlist.c app/ui/player.c \
|
||||||
app/ui/settings.c app/ui/search.c app/ui/minibar.c app/ui/ncm_colors.c app/ui/chrome.c \
|
app/ui/settings.c app/ui/search.c app/ui/minibar.c app/ui/queue.c app/ui/ncm_colors.c app/ui/chrome.c \
|
||||||
app/logic/appstate.c app/logic/lyric.c app/logic/songmeta.c \
|
app/logic/appstate.c app/logic/lyric.c app/logic/songmeta.c \
|
||||||
app/logic/cover.c app/logic/account.c app/play/player_manager.c app/play/audio_out.c \
|
app/logic/cover.c app/logic/account.c app/play/player_manager.c app/play/audio_out.c \
|
||||||
app/play/ipc_media.c app/play/decoder.c app/play/stream_src.c app/play/minimp3_impl.c"
|
app/play/ipc_media.c app/play/decoder.c app/play/stream_src.c app/play/minimp3_impl.c"
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
# ---- 用户态/服务守护二进制(usr/bin,全部 chmod 755 静态 MIPS)----
|
# ---- 用户态/服务守护二进制(usr/bin,全部 chmod 755 静态 MIPS)----
|
||||||
patch_v2/bootmenu/meow_bootmenu|/usr/bin/meow_bootmenu|8de51144538d698c3ff82ee6651b8b1a|引导选单 M6.25(开机菜单/第二屏亮度按用户设置恢复:显示前读 settings.conf 写回,方案A)
|
patch_v2/bootmenu/meow_bootmenu|/usr/bin/meow_bootmenu|8de51144538d698c3ff82ee6651b8b1a|引导选单 M6.25(开机菜单/第二屏亮度按用户设置恢复:显示前读 settings.conf 写回,方案A)
|
||||||
patch_v2/meow_launcherd/meow_launcherd|/usr/bin/meow_launcherd|755286d44cf86ace5b48b80bb8927acb|M6.26 播放页三键命中(push哨兵)+媒体卡片体FOCUS(点击开播放页)
|
patch_v2/meow_launcherd/meow_launcherd|/usr/bin/meow_launcherd|081f7f87c708696f46c3e3cb3e7dea65|M6.28 桌面NowPlaying双页歌词页+歌词数据源(批3/批4)+蓝牙P1子页
|
||||||
patch_v2/meow_watchdog/meow_watchdog|/usr/bin/meow_watchdog|34ddbf0275526eaf9be988134845c3af|应用看门狗 M6.23(亮屏亮度按用户设置 display.brightness 恢复,修 panel ON 固定60跳变)
|
patch_v2/meow_watchdog/meow_watchdog|/usr/bin/meow_watchdog|34ddbf0275526eaf9be988134845c3af|应用看门狗 M6.23(亮屏亮度按用户设置 display.brightness 恢复,修 panel ON 固定60跳变)
|
||||||
patch_v2/meow_svcd/meow_svcd|/usr/bin/meow_svcd|605646c4cc93f2baf29071ea6bc870ea|服务管理器 M2.77(+崩溃上报 meow_crash agent-187)
|
patch_v2/meow_svcd/meow_svcd|/usr/bin/meow_svcd|605646c4cc93f2baf29071ea6bc870ea|服务管理器 M2.77(+崩溃上报 meow_crash agent-187)
|
||||||
mips-repo/bins/meow_mdns/bin/meow_mdns|/usr/bin/meow_mdns|d6b0151bda26768f19952dae1342610d|mDNS 应答器 M2.78(启动竞态+换IP重检 agent-194)
|
mips-repo/bins/meow_mdns/bin/meow_mdns|/usr/bin/meow_mdns|d6b0151bda26768f19952dae1342610d|mDNS 应答器 M2.78(启动竞态+换IP重检 agent-194)
|
||||||
@@ -42,8 +42,8 @@ patch_v2/meow_audiod/meow_audiod|/usr/bin/meow_audiod|b122c9b81880b00f705bca30e4
|
|||||||
patch_v2/meow_audiod/meow_audio|/usr/bin/meow_audio|891ab26475ef9e9931bf9147d265dce4|audiod 用户侧 CLI M2.97(+monitor 实时电平表/文本电平条;status 补 rms/peak)
|
patch_v2/meow_audiod/meow_audio|/usr/bin/meow_audio|891ab26475ef9e9931bf9147d265dce4|audiod 用户侧 CLI M2.97(+monitor 实时电平表/文本电平条;status 补 rms/peak)
|
||||||
patch_v2/meow_powermgr/meow_powermgr|/usr/bin/meow_powermgr|1ef2f8afc07acc7aa09dee9f7149aba8|功耗管理专项服务期2 M6.1(期1+history/top/events+mp2731 current_now 实测充电功率+WiFi 未关联档/BT rfkill 细化+系数路径 /data/meow/powermgr/;IPC 0xD0-D7)
|
patch_v2/meow_powermgr/meow_powermgr|/usr/bin/meow_powermgr|1ef2f8afc07acc7aa09dee9f7149aba8|功耗管理专项服务期2 M6.1(期1+history/top/events+mp2731 current_now 实测充电功率+WiFi 未关联档/BT rfkill 细化+系数路径 /data/meow/powermgr/;IPC 0xD0-D7)
|
||||||
patch_v2/meow_powermgr/meow_powermgr|/usr/bin/meow_power|1ef2f8afc07acc7aa09dee9f7149aba8|meow_powermgr 的 CLI 形态(同二进制双形态,argv[0]=meow_power 即 CLI:status/estimate/model/history/top/events,policy 期3桩)
|
patch_v2/meow_powermgr/meow_powermgr|/usr/bin/meow_power|1ef2f8afc07acc7aa09dee9f7149aba8|meow_powermgr 的 CLI 形态(同二进制双形态,argv[0]=meow_power 即 CLI:status/estimate/model/history/top/events,policy 期3桩)
|
||||||
patch_v2/meow_netmgrd/meow_netmgrd|/usr/bin/meow_netmgrd|f5fcba32fbe1537a595757e68d16c13c|网络管理服务期5 M6.27(流量统计细化:实时速率去lo uplink_totals+sock_diag TCP公/内网字节 pub_bytes;agent-389)
|
patch_v2/meow_netmgrd/meow_netmgrd|/usr/bin/meow_netmgrd|9c345b2e43138bc0299c749ce0f6bb5d|网络管理服务期6 M6.28(蓝牙P1 BT_CTL权威收口:hcitool con判连接/paired-devices补名/a2dp详情/connect-disconnect-pair-unpair-scan)
|
||||||
patch_v2/meow_netmgrd/meow_netmgrd|/usr/bin/meow_net|f5fcba32fbe1537a595757e68d16c13c|meow_netmgrd 的 CLI 形态(同二进制双形态,argv[0]=meow_net 即 CLI:status/iface/wifi/bt/rfkill/apps/conns/top/dns/policy/help)
|
patch_v2/meow_netmgrd/meow_netmgrd|/usr/bin/meow_net|9c345b2e43138bc0299c749ce0f6bb5d|meow_netmgrd 的 CLI 形态(同二进制双形态,argv[0]=meow_net 即 CLI:status/iface/wifi/bt/rfkill/apps/conns/top/dns/policy/help;M6.28 蓝牙P1 BT_CTL收口)
|
||||||
|
|
||||||
# ---- 开机 init 脚本(etc/init.d,数字决定启动顺序,按文件名自然序)----
|
# ---- 开机 init 脚本(etc/init.d,数字决定启动顺序,按文件名自然序)----
|
||||||
patch_v2/meow_sysui/S70meow_sysui|/etc/init.d/S70meow_sysui|e435affcb56499e5229728c6ae2524b9|sysui 自启 M2.85(launcher.conf enabled=1+迁移)
|
patch_v2/meow_sysui/S70meow_sysui|/etc/init.d/S70meow_sysui|e435affcb56499e5229728c6ae2524b9|sysui 自启 M2.85(launcher.conf enabled=1+迁移)
|
||||||
|
|||||||
Reference in new issue
Block a user