powermgr 期2: 历史采样环/功耗分解top/电源事件流 + mp2731电流接入 + BT·WiFi分档细化

- history: 30s 节拍内存环形(RING_N=480≈4h)存 pct/est_mw/充电态/屏幕态/亮度,
  不落盘重启清零(板载写磨损不考虑); IPC MSG_PM_HISTORY(0xD4)/D5, CLI history[N]
- top: estimate breakdown 分量实时占比排序视图(2s 刷新, Ctrl-C 退,
  MEOW_PM_TOP_ONCE=1 一次输出供测试); CLI top
- events: 插拔充电器/低电阈值穿越(15%/5%/回20%)/亮灭屏/背光跳变(≥30%);
  App 前后台无可轮询导出点→诚实标注不支持(语言, 期3接 launcher)
  IPC MSG_PM_EVENTS(0xD6)/D7, CLI events[N]
- 模型: 系数路径迁 /data/meow/powermgr/power_model.conf(=/usr/data symlink,
  组件持久化目录定案); WiFi 增未关联(carrier=0→unassoc)分档; BT 细化
  rfkill+hciconfig ACL(up/connected, 有界子进程 900ms 超时, 不可用回退 up)
- mp2731 current_now 接入: 充电态实测 charge_mw=cc_voltage×current/1e9,
  charge_confidence=measured(消费侧无实测节点仍 estimated, 条件写清)
- IPC 0xD4-D7 号段本文件局部定(不动 meow_launcherd/meow_ipc.h, 严守只动本目录)

验证: 宿主 aarch64 54 PASS + qemu-mipsel 54 PASS; 交叉 -Wall -Wextra -Werror 0 warning;
固件阈值 <200KB 达标(133KB); BSS 37.6KB 起见 RSS<1MB 达标
This commit is contained in:
MeowOS committed 2026-09-01 22:13:42 +08:00
1 parent 3b6a73ac10
commit 0cae95c9cc
4 files changed
+604 -53

No files matched your search

Binary file not shown.
+539 -46
View File
@@ -1,11 +1,17 @@
#define _GNU_SOURCE /* struct ucred / SO_PEERCRED、asprintf 等 GNU/musl 扩展 */ #define _GNU_SOURCE /* struct ucred / SO_PEERCRED、asprintf 等 GNU/musl 扩展 */
/* meow_powermgr.c — MeowOS 功耗管理专项服务(devdocs/plan/POWER_MANAGER_DESIGN.md,期 1 骨架)。 /* meow_powermgr.c — MeowOS 功耗管理专项服务(devdocs/plan/POWER_MANAGER_DESIGN.md,期 2)。
* *
* 定位(三服务职责划分,§1):svc_power=一次性系统动作;svc_perf=性能只读聚合; * 定位(三服务职责划分,§1):svc_power=一次性系统动作;svc_perf=性能只读聚合;
* **powermgr=功耗**——电池深加工(state/cap/mV/续航估算)、经验功耗模型估算与分解。 * **powermgr=功耗**——电池深加工(state/cap/mV/续航估算)、经验功耗模型估算与分解。
* 期 1 范围(§9):svcd 托管骨架 + IPC(MSG_PM_QUERY/RESULT) + CLI status/estimate/model + * 期 1 范围(§9):svcd 托管骨架 + IPC(MSG_PM_QUERY/RESULT) + CLI status/estimate/model +
* 权威充电链复用 + 经验模型;history/top/events/policy 为期 2/3 桩。 * 权威充电链复用 + 经验模型。
* 期 2 新增(本文件):历史采样环(30s 节拍,pct/est_mw/充电态/屏幕态/亮度,内存环形不落盘)、
* 功耗分解 top 排序视图、电源事件流(插拔/低电阈值/背光变化;App 前后台无口径可采→诚实标注不支持)、
* mp2731 current_now 充电功率接入(充电态下 charge_mw 为实测,consume est_mw 仍为模型 estimated)、
* 模型系数路径迁移到 /data/meow/powermgr/power_model.conf(=/usr/data symlink,组件持久化目录定案)。
* IPC 新增 MSG_PM_HISTORY/D5 + MSG_PM_EVENTS/D7(号段 0x00D0-0x00D7 剩余位;数字本文件局部定,
* meow_ipc.h 仅注释保留——只动本目录,不碰 meow_launcherd/meow_ipc.h)。
* *
* 单一二进制双形态(同 svc_power): * 单一二进制双形态(同 svc_power):
* - svcd 空参拉起(板载 services.d/meow_powermgr.conf,trigger=boot/restart=on-fail)→ * - svcd 空参拉起(板载 services.d/meow_powermgr.conf,trigger=boot/restart=on-fail)→
@@ -48,9 +54,19 @@
#include <sys/socket.h> #include <sys/socket.h>
#include <sys/un.h> #include <sys/un.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <sys/wait.h>
#include "meow_ipc.h" /* 帧格式/消息号 + MSG_PM 系列 + PM_SUBSET_* + m_pm_*(号段 0x00D0-0x00D7) */ #include "meow_ipc.h" /* 帧格式/消息号 + MSG_PM 系列 + PM_SUBSET_* + m_pm_*(号段 0x00D0-0x00D7) */
/* 期 2 消息号:meow_ipc.h 仅以注释保留(0x00D4-0x00D7),本文件局部定义,避免动 meow_launcherd/。
* 响应帧复用 m_pm_result_t(code+subset+json_len+json),type 区分 HISTORY_RESULT/EVENTS_RESULT。 */
#ifndef MSG_PM_HISTORY
#define MSG_PM_HISTORY 0x00D4 /* ↑ 历史采样查询(req: u32 count=N,0=默认) */
#define MSG_PM_HISTORY_RESULT 0x00D5 /* ↓ 历史 JSON(采样点清单 + 窗口汇总) */
#define MSG_PM_EVENTS 0x00D6 /* ↑ 事件流查询(req: u32 count=N,0=默认) */
#define MSG_PM_EVENTS_RESULT 0x00D7 /* ↓ 事件 JSON 清单 */
#endif
/* ---------------- 路径(全部可经 env 覆盖,宿主/qemu 桩测试用) ---------------- */ /* ---------------- 路径(全部可经 env 覆盖,宿主/qemu 桩测试用) ---------------- */
static char g_ps_dir[128] = "/sys/class/power_supply"; /* MEOW_PS_DIR */ static char g_ps_dir[128] = "/sys/class/power_supply"; /* MEOW_PS_DIR */
static char g_bl_dir[128] = "/sys/class/backlight/backlight_pwm0"; /* MEOW_BL_DIR */ static char g_bl_dir[128] = "/sys/class/backlight/backlight_pwm0"; /* MEOW_BL_DIR */
@@ -60,13 +76,17 @@ static char g_bt_dir[128] = "/sys/class/bluetooth"; /* MEOW_
static char g_netdev[160] = "/proc/net/dev"; /* MEOW_NETDEV */ static char g_netdev[160] = "/proc/net/dev"; /* MEOW_NETDEV */
static char g_proc_stat[160] = "/proc/stat"; /* MEOW_PROC_STAT */ static char g_proc_stat[160] = "/proc/stat"; /* MEOW_PROC_STAT */
static char g_boot_mode[160] = "/tmp/meow_boot_mode"; /* MEOW_BOOT_MODE */ static char g_boot_mode[160] = "/tmp/meow_boot_mode"; /* MEOW_BOOT_MODE */
static char g_model_conf[160] = "/usr/data/meow/power_model.conf"; /* MEOW_PM_MODEL */ static char g_model_conf[160] = "/usr/data/meow/powermgr/power_model.conf"; /* MEOW_PM_MODEL
(= /data/meow/powermgr/power_model.conf,/data=/usr/data symlink,
组件持久化目录定案 SPECIAL_REF §1.1;存量平铺件不动) */
static char g_audio_sock[128] = "/usr/data/meow_audio.sock"; /* MEOW_AUDIO_SOCK */ static char g_audio_sock[128] = "/usr/data/meow_audio.sock"; /* MEOW_AUDIO_SOCK */
static const char *g_sock = "/usr/data/meow_pm.sock"; /* MEOW_PM_SOCK/--sock */ static const char *g_sock = "/usr/data/meow_pm.sock"; /* MEOW_PM_SOCK/--sock */
#define SAMPLE_SEC_DEF 30 /* 电池采样/状态刷新节拍(秒;MEOW_PM_SAMPLE_SEC 覆盖,测试用) */ #define SAMPLE_SEC_DEF 30 /* 电池采样/状态刷新节拍(秒;MEOW_PM_SAMPLE_SEC 覆盖,测试用) */
#define RATE_MIN_SPAN_DEF 120 /* 容量差分速率的最小时间窗(秒;MEOW_PM_MIN_SPAN_SEC 覆盖) */ #define RATE_MIN_SPAN_DEF 120 /* 容量差分速率的最小时间窗(秒;MEOW_PM_MIN_SPAN_SEC 覆盖) */
#define RING_N 240 /* 采样环容量(30s×240=2h) */ #define RING_N 480 /* 采样环容量(30s×480=4h;内存环形不落盘,重启清零——板载 flash 写磨损不考虑) */
#define HIST_MAX_REPLY 80 /* history IPC 单帧最多回采样点(MEOW_IPC_MAX=4096 下单帧可承载上限) */
#define EVENT_RING_N 64 /* 事件环容量(内存环形,重启清零) */
static int g_sample_sec = SAMPLE_SEC_DEF; static int g_sample_sec = SAMPLE_SEC_DEF;
static int g_rate_min_span = RATE_MIN_SPAN_DEF; static int g_rate_min_span = RATE_MIN_SPAN_DEF;
@@ -145,6 +165,8 @@ typedef struct {
int temp_c10; /* battery/temp(本机无 → -1 → null) */ int temp_c10; /* battery/temp(本机无 → -1 → null) */
int charger_temp_c10; /* mp2731-charger/temp(-1=无);÷10=℃ */ int charger_temp_c10; /* mp2731-charger/temp(-1=无);÷10=℃ */
int cycle; /* battery/cycle_count(本机无 → -1 → null) */ int cycle; /* battery/cycle_count(本机无 → -1 → null) */
int chg_cur_ua; /* mp2731-charger/current_now(µA;放电恒 0,充电=充电电流;-1=无) */
int chg_vol_uv; /* mp2731-charger/constant_charge_voltage(µV;-1=无)→ 充电功率实测 */
} batt_t; } batt_t;
static void batt_read(batt_t *b) static void batt_read(batt_t *b)
@@ -152,6 +174,7 @@ static void batt_read(batt_t *b)
char p[192]; char p[192];
memset(b, 0, sizeof *b); memset(b, 0, sizeof *b);
b->pct = -1; b->mv = -1; b->temp_c10 = -1; b->charger_temp_c10 = -1; b->cycle = -1; b->pct = -1; b->mv = -1; b->temp_c10 = -1; b->charger_temp_c10 = -1; b->cycle = -1;
b->chg_cur_ua = -1; b->chg_vol_uv = -1;
ps_path(p, sizeof p, "battery/capacity"); ps_path(p, sizeof p, "battery/capacity");
b->pct = read_file_int_def(p, -1); b->pct = read_file_int_def(p, -1);
ps_path(p, sizeof p, "battery/voltage_now"); ps_path(p, sizeof p, "battery/voltage_now");
@@ -169,6 +192,10 @@ static void batt_read(batt_t *b)
b->charger_temp_c10 = read_file_int_def(p, -1); b->charger_temp_c10 = read_file_int_def(p, -1);
ps_path(p, sizeof p, "battery/cycle_count"); ps_path(p, sizeof p, "battery/cycle_count");
b->cycle = read_file_int_def(p, -1); b->cycle = read_file_int_def(p, -1);
ps_path(p, sizeof p, "mp2731-charger/current_now");
b->chg_cur_ua = read_file_int_def(p, -1);
ps_path(p, sizeof p, "mp2731-charger/constant_charge_voltage");
b->chg_vol_uv = read_file_int_def(p, -1);
} }
/* ---------------- 经验功耗模型(§3.2;系数全部经验初值,待真机标定) ---------------- */ /* ---------------- 经验功耗模型(§3.2;系数全部经验初值,待真机标定) ---------------- */
@@ -250,9 +277,17 @@ static int model_load(pm_model_t *m)
return 0; return 0;
} }
/* ---------------- 采样环(守护进程;容量差分速率 → 续航估算,§3.1/§3.3 derived) ---------------- */ /* ---------------- 采样环(守护进程;容量差分速率 → 续航估算 §3.1/§3.3 derived;
typedef struct { long mono; int pct; int charging; } sample_t; * 期 2 同时服务 history 采样点:pct/est_mw/充电态/屏幕态/亮度,内存环形不落盘,重启清零) ---------------- */
static sample_t g_ring[RING_N]; typedef struct {
long mono; /* 采样时刻(monotonic 秒) */
int pct; /* 电量 % */
int est_mw; /* est_mw(-1=只读/未知) */
int charging; /* 0/1 */
int screen; /* 0=off 1=on(-1=未知) */
int brightness_pct; /* -1=未知 */
} hist_sample_t;
static hist_sample_t g_ring[RING_N];
static int g_rcount = 0; /* 已入环条数(≤RING_N;覆盖写最旧) */ static int g_rcount = 0; /* 已入环条数(≤RING_N;覆盖写最旧) */
static long mono_now(void) static long mono_now(void)
@@ -262,13 +297,14 @@ static long mono_now(void)
return tp.tv_sec; return tp.tv_sec;
} }
static void ring_push(const batt_t *b) static void ring_push(const batt_t *b, int est_mw, int screen, int bri)
{ {
if (b->pct < 0) return; if (b->pct < 0) return;
sample_t s; hist_sample_t s;
s.mono = mono_now(); s.mono = mono_now();
s.pct = b->pct; s.pct = b->pct;
s.charging = b->charging; s.charging = b->charging;
s.est_mw = est_mw; s.screen = screen; s.brightness_pct = bri;
if (g_rcount < RING_N) g_ring[g_rcount++] = s; if (g_rcount < RING_N) g_ring[g_rcount++] = s;
else { memmove(g_ring, g_ring + 1, sizeof g_ring - sizeof g_ring[0]); g_ring[RING_N - 1] = s; } else { memmove(g_ring, g_ring + 1, sizeof g_ring - sizeof g_ring[0]); g_ring[RING_N - 1] = s; }
} }
@@ -279,8 +315,8 @@ static void ring_push(const batt_t *b)
static int ring_rate(int *rate_x100) static int ring_rate(int *rate_x100)
{ {
if (g_rcount < 2) return 0; if (g_rcount < 2) return 0;
const sample_t *nw = &g_ring[g_rcount - 1]; const hist_sample_t *nw = &g_ring[g_rcount - 1];
const sample_t *ol = NULL; const hist_sample_t *ol = NULL;
for (int i = 0; i < g_rcount - 1; i++) for (int i = 0; i < g_rcount - 1; i++)
if (g_ring[i].charging == nw->charging) { ol = &g_ring[i]; break; } if (g_ring[i].charging == nw->charging) { ol = &g_ring[i]; break; }
if (!ol) return 0; if (!ol) return 0;
@@ -313,16 +349,59 @@ static int bl_read_pct(int *on)
return v * 100 / mx; return v * 100 / mx;
} }
/* WiFi:0=off(接口不存在/operstate 非 up)1=idle 2=tx(netdev 增量>阈值)。 /* 有界子进程命令(fork+pipe+poll+超时 SIGKILL,绝不阻塞采样拍;netgr 同源性)。
* 关联/未关联档 v1 无判据(要 wpa_cli,期 2 再细化)——up 即按 idle 起算,诚实标注。 */ * 返回读到字节数(含尾 NUL 截断);-1=失败/超时。out 在 cap 内 NUL 结尾。
* ⚠ 仅 meow 全功能模式、每拍至多一两次(wpa_cli/hciconfig);只读模式绝不调用(门控红线)。 */
static int run_cmd_bounded(const char *cmd, char *out, size_t cap, int tmo_ms)
{
if (cap < 2) return -1;
int pfd[2];
if (pipe(pfd) != 0) return -1;
pid_t pid = fork();
if (pid < 0) { close(pfd[0]); close(pfd[1]); return -1; }
if (pid == 0) {
close(pfd[0]);
dup2(pfd[1], STDOUT_FILENO);
close(pfd[1]);
int nul = open("/dev/null", O_WRONLY);
if (nul >= 0) { dup2(nul, STDERR_FILENO); if (nul > 2) close(nul); }
execl("/bin/sh", "sh", "-c", cmd, (char *)NULL);
_exit(127);
}
close(pfd[1]);
size_t off = 0;
struct pollfd pf = { pfd[0], POLLIN, 0 };
for (;;) {
int pr = poll(&pf, 1, tmo_ms);
if (pr < 0) { if (errno == EINTR) continue; break; }
if (pr == 0) break; /* 超时 */
ssize_t r = read(pfd[0], out + off, cap - 1 - off);
if (r > 0) { off += (size_t)r; if (off >= cap - 1) break; }
else if (r == 0) break; /* EOF */
else if (errno == EINTR) continue;
else break;
}
close(pfd[0]);
kill(pid, SIGKILL); /* 已退出则 0,未退出强杀 */
waitpid(pid, NULL, 0); /* 收尸彻底 */
out[off] = 0;
return off > 0 ? (int)off : -1;
}
/* WiFi:0=off(接口不存在/operstate 非 up)1=idle 2=tx(netdev 增量>阈值)3=unassoc(up 但未关联)。
* 关联判定:/sys/class/net/wlan0/carrier(0=未关联,1=已关联,不可读则默认已关联——同 wpa 常态,
* 保留期 1 语义:up 即 idle 起算)。tx 由 netdev 计数器增量判(期 1 惯例)。 */
static int wifi_read_state(long *rx_out, long *tx_out) static int wifi_read_state(long *rx_out, long *tx_out)
{ {
char p[192]; char p[192];
snprintf(p, sizeof p, "%s/wlan0/operstate", g_net_dir); snprintf(p, sizeof p, "%s/wlan0/operstate", g_net_dir);
char os[32] = ""; char os[32] = "";
if (read_file_txt(p, os, sizeof os) <= 0 || strcmp(os, "up") != 0) return 0; if (read_file_txt(p, os, sizeof os) <= 0 || strcmp(os, "up") != 0) return 0;
/* carrier 关联判定(0=未关联→unassoc;1 或不可读→已关联向下走 idle/tx 档) */
snprintf(p, sizeof p, "%s/wlan0/carrier", g_net_dir);
int carrier = read_file_int_def(p, 1); /* 缺省 1=已关联(期 1 语义) */
FILE *f = fopen(g_netdev, "r"); FILE *f = fopen(g_netdev, "r");
if (!f) return 1; if (!f) { if (rx_out) *rx_out = -1; if (tx_out) *tx_out = -1; return carrier == 0 ? 3 : 1; }
char line[256]; char line[256];
long rx = -1, tx = -1; long rx = -1, tx = -1;
while (fgets(line, sizeof line, f)) { while (fgets(line, sizeof line, f)) {
@@ -333,9 +412,6 @@ static int wifi_read_state(long *rx_out, long *tx_out)
while (*n == ' ' || *n == '\t') n++; while (*n == ' ' || *n == '\t') n++;
if (strcmp(n, "wlan0") != 0) continue; if (strcmp(n, "wlan0") != 0) continue;
rx = strtol(c + 1, NULL, 10); rx = strtol(c + 1, NULL, 10);
char *tk = c + 1;
for (int i = 0; i < 8; i++) { tk = strchr(tk, ' '); if (!tk) break; while (*tk == ' ') tk++; }
/* 第 9 列 = tx bytes */
char *save = NULL; char *save = NULL;
char *w = strtok_r(c + 1, " \t", &save); char *w = strtok_r(c + 1, " \t", &save);
for (int i = 0; w && i < 8; i++) w = strtok_r(NULL, " \t", &save); for (int i = 0; w && i < 8; i++) w = strtok_r(NULL, " \t", &save);
@@ -345,10 +421,11 @@ static int wifi_read_state(long *rx_out, long *tx_out)
fclose(f); fclose(f);
if (rx_out) *rx_out = rx; if (rx_out) *rx_out = rx;
if (tx_out) *tx_out = tx; if (tx_out) *tx_out = tx;
return 1; if (carrier == 0) return 3;
return 1; /* 传送档由调用方按增量判 2 */
} }
/* BT:rfkill soft 块→off;hci0 存在且未被块→up。连接数判据期 2。 */ /* BT:rfkill soft 块→off;hci0 存在且未被块→up;连接数档(期 2 细化见下)。 */
static int bt_read_state(void) static int bt_read_state(void)
{ {
char p[448]; /* rfkill_dir + "/" + d_name(≤255) + "/type|/soft" 充裕 */ char p[448]; /* rfkill_dir + "/" + d_name(≤255) + "/type|/soft" 充裕 */
@@ -372,7 +449,22 @@ static int bt_read_state(void)
if (blocked) return 0; if (blocked) return 0;
if (!hci0 && !seen) return 0; /* 无任何 BT 踪迹 → off */ if (!hci0 && !seen) return 0; /* 无任何 BT 踪迹 → off */
if (!hci0) return 0; if (!hci0) return 0;
return 1; /* up 空闲档(连接判定期 2) */ /* hciconfig 实测 UP/RUNNING + ACL 连接数(期 1 只据 sysfs 存在→up 偏粗,期 2 细化)。
* hciconfig 不可用(宿主/qemu 桩无该命令)或 MEOW_PM_NO_HCICONFIG=1(宿主测试桩,防宿主机
* 自带 hciconfig/蓝牙控制器干扰断言)→ 回退 up(1),连接数无法判(诚实标注,期 3 接 hcitool)。 */
char out[320];
int r = -1;
if (!getenv("MEOW_PM_NO_HCICONFIG"))
r = run_cmd_bounded("PATH=/bin:/sbin:/usr/bin:/usr/sbin hciconfig -a hci0 2>/dev/null",
out, sizeof out, 900);
if (r > 0) {
if (strstr(out, "DOWN") && !strstr(out, "UP RUNNING")) return 0; /* 电源开但接口未 up */
const char *ah = strcasestr(out, "acl:"); /* 连接数 "ACL: N" / "acl: N" */
if (!ah) ah = strcasestr(out, "acl ");
if (ah) { int n = atoi(ah + 4); if (n > 0) return 2; } /* 有 ACL 连接 → connected */
return 1; /* up 空闲 */
}
return 1; /* hciconfig 不可用 → sysfs 存在=up(1) */
} }
/* 音频链:meow 模式下经 audiod IPC(MSG_AUDIO_QUERY→MSG_AUDIO_STATE 文本)判 active。 /* 音频链:meow 模式下经 audiod IPC(MSG_AUDIO_QUERY→MSG_AUDIO_STATE 文本)判 active。
@@ -443,8 +535,9 @@ typedef struct {
int baseline_mw, cpu_mw, bl_mw, panel_mw, wifi_mw, bt_mw, audio_mw; int baseline_mw, cpu_mw, bl_mw, panel_mw, wifi_mw, bt_mw, audio_mw;
int cpu_util; /* -1=未知 */ int cpu_util; /* -1=未知 */
int brightness_pct; /* -1=未知 */ int brightness_pct; /* -1=未知 */
int wifi_state; /* 0/1/2 */ int screen_on; /* 0=off 1=on -1=未知(actual_brightness>0 代理,期 2 历史用) */
int bt_state; /* 0/1 */ int wifi_state; /* 0=off 1=idle 2=tx 3=unassoc */
int bt_state; /* 0=off 1=up 2=connected */
int audio_state; /* -1/0/1 */ int audio_state; /* -1/0/1 */
int degraded; /* 1=Hiby 只读模式:不碰 wlan0/bt/声卡,分量全 0 */ int degraded; /* 1=Hiby 只读模式:不碰 wlan0/bt/声卡,分量全 0 */
} est_t; } est_t;
@@ -456,30 +549,34 @@ static void est_compute(const pm_model_t *m, int full, int cpu_util,
long *prev_rx, long *prev_tx, est_t *e) long *prev_rx, long *prev_tx, est_t *e)
{ {
memset(e, 0, sizeof *e); memset(e, 0, sizeof *e);
e->cpu_util = -1; e->brightness_pct = -1; e->audio_state = -1; e->cpu_util = -1; e->brightness_pct = -1; e->audio_state = -1; e->screen_on = -1;
if (!full) { e->degraded = 1; return; } if (!full) { e->degraded = 1; return; }
e->baseline_mw = m->baseline_mw; e->baseline_mw = m->baseline_mw;
/* 屏幕 */ /* 屏幕 */
int on = -1; int on = -1;
int bp = bl_read_pct(&on); int bp = bl_read_pct(&on);
e->brightness_pct = bp; e->brightness_pct = bp;
e->screen_on = on;
if (on == 1) { if (on == 1) {
e->panel_mw = m->panel_on_mw; e->panel_mw = m->panel_on_mw;
e->bl_mw = bp <= 33 ? m->bl_low_mw : (bp <= 66 ? m->bl_mid_mw : m->bl_high_mw); e->bl_mw = bp <= 33 ? m->bl_low_mw : (bp <= 66 ? m->bl_mid_mw : m->bl_high_mw);
} }
/* WiFi */ /* WiFi(0=off 1=idle 2=tx 3=unassoc;tx 仅对已关联档判) */
long rx = -1, tx = -1; long rx = -1, tx = -1;
e->wifi_state = wifi_read_state(&rx, &tx); e->wifi_state = wifi_read_state(&rx, &tx);
if (e->wifi_state > 0) { if (e->wifi_state == 1) {
if (prev_rx && *prev_rx >= 0 && rx >= 0 && if (prev_rx && *prev_rx >= 0 && rx >= 0 &&
(rx - *prev_rx) + (tx - *prev_tx) > 4096) (rx - *prev_rx) + (tx - *prev_tx) > 4096)
e->wifi_state = 2; e->wifi_state = 2;
e->wifi_mw = e->wifi_state == 2 ? m->wifi_tx_mw : m->wifi_idle_mw;
} }
e->wifi_mw = e->wifi_state == 2 ? m->wifi_tx_mw
: (e->wifi_state == 3 ? m->wifi_unassoc_mw
: (e->wifi_state == 1 ? m->wifi_idle_mw : 0));
if (prev_rx) { *prev_rx = rx; *prev_tx = tx; } if (prev_rx) { *prev_rx = rx; *prev_tx = tx; }
/* BT */ /* BT(0=off 1=up 2=connected) */
e->bt_state = bt_read_state(); e->bt_state = bt_read_state();
e->bt_mw = e->bt_state ? m->bt_up_mw : 0; e->bt_mw = e->bt_state == 2 ? m->bt_conn_mw
: (e->bt_state == 1 ? m->bt_up_mw : 0);
/* 音频链 */ /* 音频链 */
e->audio_state = audio_query_active(); e->audio_state = audio_query_active();
e->audio_mw = e->audio_state == 1 ? m->audio_play_mw e->audio_mw = e->audio_state == 1 ? m->audio_play_mw
@@ -537,6 +634,18 @@ static void jadd_rate(jw_t *w, const batt_t *b, int have_rate, int rate_x100)
jadd(w, "\"rate_confidence\":\"derived\","); jadd(w, "\"rate_confidence\":\"derived\",");
} }
/* 充电功率(mp2731 current_now 实测轨;POWER_HW_FACTS §0/项 2,期 2 接入 §3.3):
* charge_mw = constant_charge_voltage(µV) × current_now(µA) / 1e9。
* 仅当「权威链判定 charging && current_now>0 && constant_charge_voltage>0」时返回实测 mW,
* 否则返回 -1(≠ 容量差分/经验模型,诚实标注——充电功率可测,消费功耗不可测)。 */
static int pm_charge_mw(const batt_t *b)
{
if (!b->charging) return -1;
if (b->chg_cur_ua <= 0 || b->chg_vol_uv <= 0) return -1;
long long v = (long long)b->chg_vol_uv * b->chg_cur_ua;
return (int)(v / 1000000000LL);
}
/* 聚合 JSON(subset 位选段;0=全部)。守护/CLI 降级直读共用。 /* 聚合 JSON(subset 位选段;0=全部)。守护/CLI 降级直读共用。
* have_rate/cpu_util/prev_rx/prev_tx 由调用方提供(守护=环/差分态;直读=瞬测)。 */ * have_rate/cpu_util/prev_rx/prev_tx 由调用方提供(守护=环/差分态;直读=瞬测)。 */
static void build_json(uint32_t subset, int full, const batt_t *b, static void build_json(uint32_t subset, int full, const batt_t *b,
@@ -558,15 +667,23 @@ static void build_json(uint32_t subset, int full, const batt_t *b,
jadd_int_or_null(&w, "temp_c10", b->temp_c10); jadd_int_or_null(&w, "temp_c10", b->temp_c10);
jadd_int_or_null(&w, "charger_temp_c10", b->charger_temp_c10); jadd_int_or_null(&w, "charger_temp_c10", b->charger_temp_c10);
jadd_int_or_null(&w, "cycle_count", b->cycle); jadd_int_or_null(&w, "cycle_count", b->cycle);
jadd_int_or_null(&w, "chg_current_ua", b->chg_cur_ua);
jadd_int_or_null(&w, "chg_voltage_uv", b->chg_vol_uv);
jadd_rate(&w, b, have_rate, rate_x100); jadd_rate(&w, b, have_rate, rate_x100);
jadd(&w, "\"nominal_mah_note\":\"nominal_mah 为模型系数(默认1600,待标定)\"}"); jadd(&w, "\"nominal_mah_note\":\"nominal_mah 为模型系数(默认1600,待标定)\"}");
} }
if (subset & PM_SUBSET_ESTIMATE) { if (subset & PM_SUBSET_ESTIMATE) {
jadd(&w, ",\"estimate\":{"); jadd(&w, ",\"estimate\":{");
if (e->degraded) { if (e->degraded) {
jadd(&w, "\"est_mw\":null,\"confidence\":\"none\",\"degraded\":true}"); jadd(&w, "\"est_mw\":null,\"confidence\":\"none\",\"degraded\":true,"
"\"charge_mw\":null,\"charge_confidence\":null}");
} else { } else {
jadd(&w, "\"est_mw\":%d,\"confidence\":\"estimated\",\"calibrated\":false}", e->total_mw); int cmw = pm_charge_mw(b);
jadd(&w, "\"est_mw\":%d,\"confidence\":\"estimated\",\"calibrated\":false,", e->total_mw);
if (cmw >= 0)
jadd(&w, "\"charge_mw\":%d,\"charge_confidence\":\"measured\"}", cmw);
else
jadd(&w, "\"charge_mw\":null,\"charge_confidence\":null}");
} }
} }
if (subset & PM_SUBSET_BREAKDOWN) { if (subset & PM_SUBSET_BREAKDOWN) {
@@ -580,8 +697,11 @@ static void build_json(uint32_t subset, int full, const batt_t *b,
jadd(&w, "\"backlight_mw\":%d,\"panel_mw\":%d,", e->bl_mw, e->panel_mw); jadd(&w, "\"backlight_mw\":%d,\"panel_mw\":%d,", e->bl_mw, e->panel_mw);
jadd_int_or_null(&w, "brightness_pct", e->brightness_pct); jadd_int_or_null(&w, "brightness_pct", e->brightness_pct);
jadd(&w, "\"wifi_mw\":%d,\"wifi_state\":\"%s\",", e->wifi_mw, jadd(&w, "\"wifi_mw\":%d,\"wifi_state\":\"%s\",", e->wifi_mw,
e->wifi_state == 2 ? "tx" : (e->wifi_state == 1 ? "idle" : "off")); e->wifi_state == 2 ? "tx"
jadd(&w, "\"bt_mw\":%d,\"bt_state\":\"%s\",", e->bt_mw, e->bt_state ? "up" : "off"); : (e->wifi_state == 3 ? "unassoc"
: (e->wifi_state == 1 ? "idle" : "off")));
jadd(&w, "\"bt_mw\":%d,\"bt_state\":\"%s\",", e->bt_mw,
e->bt_state == 2 ? "connected" : (e->bt_state == 1 ? "up" : "off"));
jadd(&w, "\"audio_mw\":%d,\"audio_state\":\"%s\"", e->audio_mw, jadd(&w, "\"audio_mw\":%d,\"audio_state\":\"%s\"", e->audio_mw,
e->audio_state == 1 ? "playing" : (e->audio_state == 0 ? "idle" : "unknown")); e->audio_state == 1 ? "playing" : (e->audio_state == 0 ? "idle" : "unknown"));
jadd(&w, "}"); jadd(&w, "}");
@@ -649,7 +769,7 @@ static int send_err(int fd, uint32_t req_id, uint32_t code, const char *msg)
return send_frame(fd, MSG_ERR, req_id, &e, sizeof e); return send_frame(fd, MSG_ERR, req_id, &e, sizeof e);
} }
static int send_pm_result(int fd, uint32_t req_id, uint32_t subset, const char *json) static int send_pm_result(int fd, uint16_t rtype, uint32_t req_id, uint32_t subset, const char *json)
{ {
m_pm_result_t r; m_pm_result_t r;
memset(&r, 0, sizeof r); memset(&r, 0, sizeof r);
@@ -660,15 +780,19 @@ static int send_pm_result(int fd, uint32_t req_id, uint32_t subset, const char *
r.json_len = (uint16_t)jl; r.json_len = (uint16_t)jl;
memcpy(r.json, json, jl); memcpy(r.json, json, jl);
r.json[jl] = 0; r.json[jl] = 0;
return send_frame(fd, MSG_PM_RESULT, req_id, &r, (uint32_t)(12 + jl + 1)); return send_frame(fd, rtype, req_id, &r, (uint32_t)(12 + jl + 1));
} }
/* ---------------- 守护进程态 ---------------- */ /* ---------------- 守护进程态 ---------------- */
static int g_full_mode = 0; /* meow=1 全功能;0=只读降级(hiby/缺失) */ static int g_full_mode = 0; /* meow=1 全功能;0=只读降级(hiby/缺失) */
static long g_cpu_prev_total = -1, g_cpu_prev_idle = 0; static long g_cpu_prev_total = -1, g_cpu_prev_idle = 0;
static long g_wifi_prev_rx = -1, g_wifi_prev_tx = 0; static long g_wifi_prev_rx = -1, g_wifi_prev_tx = 0; /* PM_QUERY 实时估计算的 WiFi 基线(查询驱动) */
static long g_hist_wifi_prev_rx = -1, g_hist_wifi_prev_tx = 0; /* 守护节拍估计算的 WiFi 基线(history 采样) */
static pm_model_t g_model; static pm_model_t g_model;
/* 上一拍状态(事件检测用;-1=首拍未初始化,跳过跃迁判定) */
static int g_prev_charging = -1, g_prev_pct = -1, g_prev_screen = -1, g_prev_bri_pct = -1;
static int boot_mode_is_meow(void) static int boot_mode_is_meow(void)
{ {
char buf[16] = ""; char buf[16] = "";
@@ -676,7 +800,76 @@ static int boot_mode_is_meow(void)
return strcmp(buf, "meow") == 0; return strcmp(buf, "meow") == 0;
} }
/* 采样拍:模式热切换 + 电池入环 + CPU 差分基准推进。(WiFi 计数基准由 est_compute 回存。) */ /* ---------------- 电源事件流(守护进程;内存环形,重启清零,不落盘) ----------------
* 可检测:① 充电器插拔(charging 位翻转)② 低电阈值穿越(低 15%/极低 5%/回到 20% 恢复,§5.2 默认)
* ③ 屏幕亮灭 ④ 背光跳变(≥30% 点)。⚠ App 前后台切换:期 2 无可轮询的 fg/bg 导出点(launcher 对外
* 无 App 前台态;/tmp/meow_*_active 是 watchdog 让位的行为标记而非 App 前后台),**诚实标注不支持**,
* 不编造事件——留期 3 接 launcher MSG_ACT_FOCUS 后再扩。 */
#define EV_LOW_PCT 15 /* 低电阈值(§5.2 power.low_pct 默认 15,期 3 接 settings) */
#define EV_CRIT_PCT 5 /* 极低电阈值(§5.2 power.critical_pct 默认 5) */
#define EV_LOW_REC 20 /* 恢复阈值(low+5;电量回到 ≥20% 触发恢复事件) */
#define EV_BRI_JUMP 30 /* 背光跳变阈值(百分点) */
typedef struct {
long mono; /* 事件时刻(monotonic 秒) */
int type; /* 0=charger 1=lowbat 2=screen 3=brightness */
int arg; /* charger: new charging(0/1);lowbat: 0低 1极低 2恢复;screen: on(0/1);bri: new pct */
int pct; /* 事件时电量 % */
char txt[112]; /* 可读描述 */
} pm_event_t;
static pm_event_t g_events[EVENT_RING_N];
static int g_ev_count = 0; /* 累计事件数含滚出;展示取 min(count, EVENT_RING_N) */
static void ev_push(int type, int arg, int pct, const char *fmt, ...)
{
pm_event_t *dst;
if (g_ev_count < EVENT_RING_N) dst = &g_events[g_ev_count];
else { memmove(g_events, g_events + 1, sizeof g_events - sizeof g_events[0]); dst = &g_events[EVENT_RING_N - 1]; }
g_ev_count++;
memset(dst, 0, sizeof *dst);
dst->mono = mono_now(); dst->type = type; dst->arg = arg; dst->pct = pct;
va_list ap;
va_start(ap, fmt);
vsnprintf(dst->txt, sizeof dst->txt, fmt, ap);
va_end(ap);
}
static void detect_events(const batt_t *b, const est_t *e)
{
/* ① 充电器插拔 */
if (g_prev_charging >= 0 && g_prev_charging != b->charging) {
if (b->charging) ev_push(0, 1, b->pct, "插上充电器,开始充电 (state=%s)", b->state);
else ev_push(0, 0, b->pct, "拔出充电器,停止充电 (state=%s)", b->state);
}
/* ② 低电阈值(放电下降沿触发;充电回阈值恢复) */
if (b->pct >= 0 && g_prev_pct >= 0) {
if (!b->charging) {
if (g_prev_pct >= EV_LOW_PCT && b->pct < EV_LOW_PCT)
ev_push(1, 0, b->pct, "电量低 %d%%(阈值 %d%%)", b->pct, EV_LOW_PCT);
if (g_prev_pct >= EV_CRIT_PCT && b->pct < EV_CRIT_PCT)
ev_push(1, 1, b->pct, "电量极低 %d%%(阈值 %d%%)", b->pct, EV_CRIT_PCT);
}
if (g_prev_pct < EV_LOW_REC && b->pct >= EV_LOW_REC)
ev_push(1, 2, b->pct, "电量回到 %d%%(≥%d%% 恢复)", b->pct, EV_LOW_REC);
}
/* ③ 屏幕亮灭 / ④ 背光跳变(只读模式 screen=-1 → 跳过;息屏时不算背光跳变) */
if (e->screen_on >= 0) {
if (g_prev_screen >= 0 && g_prev_screen != e->screen_on)
ev_push(2, e->screen_on, b->pct, e->screen_on ? "亮屏" : "息屏");
if (e->screen_on == 1 && e->brightness_pct >= 0 && g_prev_bri_pct >= 0 &&
((e->brightness_pct > g_prev_bri_pct ? e->brightness_pct - g_prev_bri_pct
: g_prev_bri_pct - e->brightness_pct)) >= EV_BRI_JUMP)
ev_push(3, e->brightness_pct, b->pct, "背光跳变 %d%%→%d%%(≥%d%%)",
g_prev_bri_pct, e->brightness_pct, EV_BRI_JUMP);
}
g_prev_charging = b->charging;
if (b->pct >= 0) g_prev_pct = b->pct;
g_prev_screen = e->screen_on;
g_prev_bri_pct = e->brightness_pct;
}
/* 采样拍:模式热切换 + 电池入环 + CPU 差分基准推进 + 守护节拍估算(history/events 用)。
* (PM_QUERY 的实时估算在 handle_conn 独立重算,基线 g_wifi_prev_* 查询驱动——保持期 1 已测语义。) */
static void daemon_tick(void) static void daemon_tick(void)
{ {
int fm = boot_mode_is_meow(); int fm = boot_mode_is_meow();
@@ -687,10 +880,80 @@ static void daemon_tick(void)
} }
batt_t b; batt_t b;
batt_read(&b); batt_read(&b);
ring_push(&b);
long t = -1, i = 0; long t = -1, i = 0;
cpu_util_pct(g_cpu_prev_total, g_cpu_prev_idle, &t, &i); int cu = cpu_util_pct(g_cpu_prev_total, g_cpu_prev_idle, &t, &i);
g_cpu_prev_total = t; g_cpu_prev_idle = i; g_cpu_prev_total = t; g_cpu_prev_idle = i;
est_t e;
est_compute(&g_model, g_full_mode, cu, &g_hist_wifi_prev_rx, &g_hist_wifi_prev_tx, &e);
ring_push(&b, e.degraded ? -1 : e.total_mw,
e.degraded ? -1 : e.screen_on,
e.degraded ? -1 : e.brightness_pct);
detect_events(&b, &e);
}
/* 期 2 · history:从采样环出最近最多 count 条 + 全环窗口汇总(内存环形,重启清零)。
* JSON: {kind:history,count,n_samples,window_sec,start_pct,end_pct,min_pct,max_pct,avg_mw,samples:[[ts,pct,est_mw,chg,scr,bri],...]} */
static void build_history_json(uint32_t count, char *out, size_t cap)
{
if (count == 0 || count > HIST_MAX_REPLY) count = HIST_MAX_REPLY;
int n = g_rcount < (int)count ? g_rcount : (int)count;
if (n < 0) n = 0;
int start_pct = -1, end_pct = -1, min_pct = 101, max_pct = -1, sum_mw = 0, mwden = 0;
long first_mono = 0, last_mono = 0;
if (g_rcount > 0) {
start_pct = g_ring[0].pct;
end_pct = g_ring[g_rcount - 1].pct;
first_mono = g_ring[0].mono;
last_mono = g_ring[g_rcount - 1].mono;
for (int i = 0; i < g_rcount; i++) {
if (g_ring[i].pct >= 0) {
if (g_ring[i].pct < min_pct) min_pct = g_ring[i].pct;
if (g_ring[i].pct > max_pct) max_pct = g_ring[i].pct;
}
if (g_ring[i].est_mw >= 0) { sum_mw += g_ring[i].est_mw; mwden++; }
}
if (min_pct == 101) min_pct = -1;
}
jw_t w = { out, cap, 0 };
out[0] = 0;
jadd(&w, "{\"kind\":\"history\",\"count\":%d,\"n_samples\":%d,\"window_sec\":%ld,"
"\"start_pct\":%d,\"end_pct\":%d,\"min_pct\":%d,\"max_pct\":%d,\"avg_mw\":%d,\"samples\":[",
n, g_rcount, g_rcount > 0 ? last_mono - first_mono : 0,
start_pct, end_pct, min_pct, max_pct, mwden ? sum_mw / mwden : -1);
int start = g_rcount - n;
for (int i = start; i < g_rcount; i++) {
const hist_sample_t *s = &g_ring[i];
jadd(&w, "%s[%ld,%d,%d,%d,%d,%d]", i > start ? "," : "",
s->mono, s->pct, s->est_mw, s->charging, s->screen, s->brightness_pct);
}
jadd(&w, "]}");
}
/* 期 2 · events:从事件环出最近最多 count 条(内存环形,重启清零)。
* JSON: {kind:events,count,events:[[ts,type,pct,txt],...]}
* 单事件最坏 ~160B(txt 长 ≤112)→ 回帧上限收紧到 EV_REPLY_MAX,防 JSON 撑爆 MEOW_IPC_MAX。 */
#define EV_REPLY_MAX ((int)((MEOW_IPC_MAX - 12 - 64) / 160))
static void build_events_json(uint32_t count, char *out, size_t cap)
{
if (count == 0) count = 20;
if ((int)count > EVENT_RING_N) count = EVENT_RING_N;
if ((int)count > EV_REPLY_MAX) count = (uint32_t)EV_REPLY_MAX;
int tot = g_ev_count < EVENT_RING_N ? g_ev_count : EVENT_RING_N;
int n = tot < (int)count ? tot : (int)count;
if (n < 0) n = 0;
int start = tot - n;
jw_t w = { out, cap, 0 };
out[0] = 0;
jadd(&w, "{\"kind\":\"events\",\"count\":%d,\"events\":[", n);
for (int i = start; i < tot; i++) {
const pm_event_t *ev = &g_events[i];
const char *tn = ev->type == 0 ? "charger"
: (ev->type == 1 ? "lowbat"
: (ev->type == 2 ? "screen" : "brightness"));
jadd(&w, "%s[%ld,\"%s\",%d,\"%s\"]", i > start ? "," : "",
ev->mono, tn, ev->pct, ev->txt);
}
jadd(&w, "]}");
} }
static void handle_conn(int c) static void handle_conn(int c)
@@ -736,10 +999,31 @@ static void handle_conn(int c)
char json[MEOW_IPC_MAX]; char json[MEOW_IPC_MAX];
build_json(q.subset, g_full_mode, &b, have_rate, rate_x100, &e, build_json(q.subset, g_full_mode, &b, have_rate, rate_x100, &e,
json, sizeof json); json, sizeof json);
send_pm_result(c, req_id, q.subset, json); send_pm_result(c, MSG_PM_RESULT, req_id, q.subset, json);
continue; continue;
} }
send_err(c, req_id, MEOW_ERR_INVAL, "unknown type (期1仅 PM_QUERY)");
if (type == MSG_PM_HISTORY) {
/* 期 2:历史采样查询(读级任意调用者)。req: u32 count */
uint32_t count = 0;
if (rlen >= 4) memcpy(&count, payload, 4);
char json[MEOW_IPC_MAX];
build_history_json(count, json, sizeof json);
send_pm_result(c, MSG_PM_HISTORY_RESULT, req_id, count, json);
continue;
}
if (type == MSG_PM_EVENTS) {
/* 期 2:电源事件查询(读级任意调用者)。req: u32 count */
uint32_t count = 0;
if (rlen >= 4) memcpy(&count, payload, 4);
char json[MEOW_IPC_MAX];
build_events_json(count, json, sizeof json);
send_pm_result(c, MSG_PM_EVENTS_RESULT, req_id, count, json);
continue;
}
send_err(c, req_id, MEOW_ERR_INVAL, "unknown type (期1 PM_QUERY / 期2 HISTORY/EVENTS)");
break; break;
} }
} }
@@ -890,6 +1174,59 @@ direct:
} }
} }
/* 期 2:history/events 通用 IPC 客户端(HELLO→ACK→req→RESULT 提取 JSON)。
* 返回 0=守护返回;-1=守护不可达/协议错。history/events 无直读降级——采样环/事件环在守护内存。 */
static int pm_ipc_json(uint16_t reqtype, const void *req, uint32_t reqlen,
uint16_t wanttype, char *out, size_t cap)
{
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) return -1;
struct timeval tv = { 1, 500000 };
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
struct sockaddr_un a;
memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX;
strncpy(a.sun_path, g_sock, sizeof a.sun_path - 1);
int rc = -1;
if (connect(fd, (struct sockaddr *)&a, sizeof a) != 0) { close(fd); return -1; }
uint8_t hdr[MEOW_IPC_HDR_LEN];
uint8_t pl[sizeof(m_pm_result_t)];
if (send_frame(fd, MSG_HELLO, 1, NULL, 0) != 0) goto out;
if (x_read_full(fd, hdr, sizeof hdr) != 0) goto out; /* MSG_ACK 头 */
{ uint32_t al; memcpy(&al, hdr + 12, 4); if (al <= sizeof pl && al && x_read_full(fd, pl, al) != 0) goto out; } /* 丢 ACK 载荷 */
if (send_frame(fd, reqtype, 2, req, reqlen) != 0) goto out;
if (x_read_full(fd, hdr, sizeof hdr) != 0) goto out;
uint16_t rt; uint32_t rl;
memcpy(&rt, hdr + 6, 2);
memcpy(&rl, hdr + 12, 4);
if (rt != wanttype || rl < 12 || rl > sizeof pl) goto out;
if (x_read_full(fd, pl, rl) != 0) goto out;
{
m_pm_result_t r;
memset(&r, 0, sizeof r);
memcpy(&r, pl, rl);
size_t jl = r.json_len;
if (jl >= cap) jl = cap - 1;
memcpy(out, r.json, jl);
out[jl] = 0;
rc = 0;
}
out:
close(fd);
return rc;
}
/* 极简数组解析:跳跃分隔符后取一个整数,推进指针(自产 JSON 固定形状 [a,b,c])。 */
static long next_int2(const char **pp)
{
const char *p = *pp;
while (*p == ',' || *p == ' ' || *p == ']') p++;
long v = strtol(p, (char **)&p, 10);
*pp = p;
return v;
}
/* ---------------- CLI 子命令 ---------------- */ /* ---------------- CLI 子命令 ---------------- */
static void fmt_min(char *out, size_t cap, int min) static void fmt_min(char *out, size_t cap, int min)
{ {
@@ -970,6 +1307,17 @@ static int cli_estimate(void)
jstr(js, "audio_state", as, sizeof as); jstr(js, "audio_state", as, sizeof as);
printf("est_mw=%d confidence=%s calibrated=false (系数全部经验初值,待真机标定)\n", printf("est_mw=%d confidence=%s calibrated=false (系数全部经验初值,待真机标定)\n",
total, conf[0] ? conf : "estimated"); total, conf[0] ? conf : "estimated");
/* 期 2:mp2731 current_now 实测充电功率(仅充电态且 current_now>0 → measured;否则 null) */
{
int cmw = -1; char cc[16];
jint(js, "charge_mw", &cmw);
jstr(js, "charge_confidence", cc, sizeof cc);
if (cmw >= 0)
printf("charge=+%dmW confidence=%s (mp2731 current_now×constant_charge_voltage 实测;系统消费侧无实测节点仍为 estimated)\n",
cmw, cc[0] ? cc : "measured");
else
printf("charge=null confidence=null (非充电态或无 current_now——充电功率无实测轨)\n");
}
char cu_s[16], bp_s[16]; char cu_s[16], bp_s[16];
if (cu >= 0) snprintf(cu_s, sizeof cu_s, "%d%%", cu); else snprintf(cu_s, sizeof cu_s, "null"); if (cu >= 0) snprintf(cu_s, sizeof cu_s, "%d%%", cu); else snprintf(cu_s, sizeof cu_s, "null");
if (bp >= 0) snprintf(bp_s, sizeof bp_s, "%d%%", bp); else snprintf(bp_s, sizeof bp_s, "null"); if (bp >= 0) snprintf(bp_s, sizeof bp_s, "%d%%", bp); else snprintf(bp_s, sizeof bp_s, "null");
@@ -986,7 +1334,8 @@ static int cli_model(void)
pm_model_t m; pm_model_t m;
int def = model_load(&m); int def = model_load(&m);
printf("# meow_powermgr 经验功耗模型(POWER_MANAGER_DESIGN §3.2)\n"); printf("# meow_powermgr 经验功耗模型(POWER_MANAGER_DESIGN §3.2)\n");
printf("# 系数来源: %s\n", def ? "内置默认(conf 缺失)" : g_model_conf); printf("# 系数来源: %s\n", g_model_conf);
printf("# %s\n", def ? "⚠ conf 缺失 → 以上为内置默认(待标定)" : "conf 读取成功");
printf("# ⚠ 全部系数为经验初值,待真机标定(POWER_HW_FACTS §1 项 7/8 标定实验未做)\n"); printf("# ⚠ 全部系数为经验初值,待真机标定(POWER_HW_FACTS §1 项 7/8 标定实验未做)\n");
printf("nominal_mah=%d\n", m.nominal_mah); printf("nominal_mah=%d\n", m.nominal_mah);
printf("baseline_mw=%d\n", m.baseline_mw); printf("baseline_mw=%d\n", m.baseline_mw);
@@ -1005,6 +1354,151 @@ static int cli_model(void)
return 0; return 0;
} }
/* 期 2 · history:最近 N 个采样点 + 全环窗口汇总(不落盘,重启清零) */
static int cli_history(int n, char **v)
{
uint32_t count = 0;
if (n >= 1) { int c = atoi(v[0]); if (c > 0) count = (uint32_t)c; }
char js[MEOW_IPC_MAX];
if (pm_ipc_json(MSG_PM_HISTORY, &count, sizeof count, MSG_PM_HISTORY_RESULT, js, sizeof js) != 0) {
fprintf(stderr, "powermgr 守护不可达:history 采样环在守护内存,需 meow_powermgr 常驻\n");
return 1;
}
int cnt = 0, ns = 0, sp = -1, ep = -1, mnp = -1, mxp = -1, avg = -1;
long win = 0;
jint(js, "count", &cnt); jint(js, "n_samples", &ns);
jint(js, "start_pct", &sp); jint(js, "end_pct", &ep);
jint(js, "min_pct", &mnp); jint(js, "max_pct", &mxp); jint(js, "avg_mw", &avg);
{ int wi = 0; jint(js, "window_sec", &wi); win = wi; }
printf("history: %d 条采样点(环共 %d,窗口 %lds=%.2fh)\n", cnt, ns, win, win / 3600.0);
if (sp >= 0)
printf(" 电量: 起 %d%% 止 %d%% 低 %d%% 高 %d%% 平均功耗 %dmW(estimated/derived,系数未标定)\n",
sp, ep, mnp, mxp, avg);
long ts[80]; int pc[80], mw[80], cg[80], sc[80], br[80];
int m = 0;
const char *p = strstr(js, "\"samples\":[");
if (p) {
p += 11; /* "\"samples\":[" 长 11 */
while (m < 80 && *p == '[') {
p++; /* '[' */
ts[m] = next_int2(&p);
pc[m] = (int)next_int2(&p);
mw[m] = (int)next_int2(&p);
cg[m] = (int)next_int2(&p);
sc[m] = (int)next_int2(&p);
br[m] = (int)next_int2(&p);
m++;
if (*p == ']') { p++; if (*p == ',') p++; }
}
}
printf("\n%-10s %5s %6s %5s %5s %6s\n", "time", "cap%", "est_mW", "chg", "scr", "bri");
long newest = m > 0 ? ts[m - 1] : 0;
for (int i = 0; i < m; i++) {
char tb[24], bs[16], bri_s[16];
long off = newest - ts[i]; /* 距最新采样(秒,≥0) */
if (off >= 7200) snprintf(tb, sizeof tb, "-%ldh%02ldm", off / 3600, (off % 3600) / 60);
else snprintf(tb, sizeof tb, "-%ldmin", off / 60);
snprintf(bs, sizeof bs, "%s", cg[i] ? "Chg" : "Dis");
if (br[i] < 0) snprintf(bri_s, sizeof bri_s, "?"); else snprintf(bri_s, sizeof bri_s, "%d%%", br[i]);
printf("%-10s %5d %6d %5s %5s %6s\n", tb, pc[i], mw[i], bs,
sc[i] == 1 ? "On" : (sc[i] == 0 ? "Off" : "?"), bri_s);
}
fprintf(stderr, "note: est_mW/scr/bri 为守护 %ds 节拍估算;只读模式下为 -1/off/未知(只采样电池)\n",
g_sample_sec);
return 0;
}
/* 期 2 · events:电源事件流(内存环形,重启清零) */
static int cli_events(int n, char **v)
{
uint32_t count = 0;
if (n >= 1) { int c = atoi(v[0]); if (c > 0) count = (uint32_t)c; }
char js[MEOW_IPC_MAX];
if (pm_ipc_json(MSG_PM_EVENTS, &count, sizeof count, MSG_PM_EVENTS_RESULT, js, sizeof js) != 0) {
fprintf(stderr, "powermgr 守护不可达:events 事件环在守护内存,需 meow_powermgr 常驻\n");
return 1;
}
int cnt = 0; jint(js, "count", &cnt);
printf("events: %d 条(内存环形,重启清零;App 前后台切换无可轮询导出点——期2诚实标注不支持)\n", cnt);
if (cnt == 0) { printf(" (无事件)\n"); return 0; }
long ts[64]; char tn[64][16]; int pct[64]; char txt[64][128];
int m = 0;
const char *p = strstr(js, "\"events\":[");
if (p) {
p += 10; /* "\"events\":[" 长 10 */
while (m < 64 && *p == '[') {
p++; /* '[' */
ts[m] = next_int2(&p);
while (*p && *p != '"') p++;
if (*p == '"') p++;
int k = 0;
while (*p && *p != '"' && k < 15) tn[m][k++] = *p++;
tn[m][k] = 0; if (*p == '"') p++;
if (*p == ',') p++;
pct[m] = (int)next_int2(&p);
while (*p && *p != '"') p++;
if (*p == '"') p++;
k = 0;
while (*p && *p != '"' && k < 127) txt[m][k++] = *p++;
txt[m][k] = 0; if (*p == '"') p++;
m++;
if (*p == ']') { p++; if (*p == ',') p++; }
}
}
long newest = m > 0 ? ts[m - 1] : 0;
for (int i = m - 1; i >= 0; i--) { /* 最新在前 */
char tb[24];
long off = newest - ts[i];
if (off >= 7200) snprintf(tb, sizeof tb, "-%ldh%02ldm", off / 3600, (off % 3600) / 60);
else snprintf(tb, sizeof tb, "-%ldmin", off / 60);
printf("%-8s %-10s %3d%% %s\n", tb, tn[i], pct[i], txt[i]);
}
return 0;
}
/* 期 2 · top:功耗分解实时排序视图(2s 刷新,Ctrl-C 退;MEOW_PM_TOP_ONCE=1 一次输出用于测试)。 */
static int cli_top(void)
{
static const char *name[6] = { "baseline", "screen", "wifi", "bt", "audio", "cpu" };
signal(SIGINT, on_sig);
signal(SIGTERM, on_sig);
int once = getenv("MEOW_PM_TOP_ONCE") != NULL;
do {
char js[MEOW_IPC_MAX];
pm_query_json(PM_SUBSET_BREAKDOWN, js, sizeof js);
if (strstr(js, "\"degraded\":true")) {
printf("degraded: HibyOS 只读模式——不施功耗分解(只采样电池)\n");
return 0;
}
int b = 0, cpu = 0, w = 0, bt = 0, au = 0, bl = 0, panel = 0;
jint(js, "baseline_mw", &b); jint(js, "cpu_mw", &cpu);
jint(js, "wifi_mw", &w); jint(js, "bt_mw", &bt);
jint(js, "audio_mw", &au); jint(js, "backlight_mw", &bl); jint(js, "panel_mw", &panel);
int mw[6] = { b, bl + panel, w, bt, au, cpu, };
int total = 0;
for (int i = 0; i < 6; i++) total += mw[i];
int idx[6] = { 0, 1, 2, 3, 4, 5 };
for (int i = 0; i < 5; i++)
for (int j = i + 1; j < 6; j++)
if (mw[idx[j]] > mw[idx[i]]) { int t = idx[i]; idx[i] = idx[j]; idx[j] = t; }
if (!once) printf("\033[2J\033[H"); /* 清屏(仅终端) */
printf("=== meow_power top 功耗分解 总 %dmW (confidence=estimated, 系数待真机标定) ===\n", total);
for (int i = 0; i < 6; i++) {
int k = idx[i];
int pctv = total > 0 ? mw[k] * 100 / total : 0;
int nb = pctv * 40 / 100;
char bar[41];
for (int x = 0; x < nb; x++) bar[x] = '#';
bar[nb] = 0;
printf("%-9s %6dmW %3d%% |%-40s|\n", name[k], mw[k], pctv, bar);
}
fflush(stdout);
if (once) break;
for (int sl = 0; sl < 20 && !g_stop; sl++) usleep(100 * 1000); /* 2s,可中断 */
} while (!g_stop);
return 0;
}
static int cli_stub(const char *what, const char *phase) static int cli_stub(const char *what, const char *phase)
{ {
printf("meow_power %s: 期 %s 落地,当前未实现(POWER_MANAGER_DESIGN §9 分期)\n", what, phase); printf("meow_power %s: 期 %s 落地,当前未实现(POWER_MANAGER_DESIGN §9 分期)\n", what, phase);
@@ -1013,19 +1507,18 @@ static int cli_stub(const char *what, const char *phase)
static const char *cli_usage(void) static const char *cli_usage(void)
{ {
return "status|estimate|model|history[小时]|top|events|policy(后四者期2/3桩)"; return "status|estimate|model|history[N]|top|events[N]|policy(history/top/events 期2已落地;policy 期3桩)";
} }
static int cli_run(int n, char **v) static int cli_run(int n, char **v)
{ {
const char *cmd = v[0]; const char *cmd = v[0];
(void)n; /* 子命令参数期 2/3 才用(history[小时] 等) */
if (!strcmp(cmd, "status")) return cli_status(); if (!strcmp(cmd, "status")) return cli_status();
if (!strcmp(cmd, "estimate")) return cli_estimate(); if (!strcmp(cmd, "estimate")) return cli_estimate();
if (!strcmp(cmd, "model")) return cli_model(); if (!strcmp(cmd, "model")) return cli_model();
if (!strcmp(cmd, "history")) return cli_stub("history", "2"); if (!strcmp(cmd, "history")) return cli_history(n - 1, v + 1);
if (!strcmp(cmd, "top")) return cli_stub("top", "2"); if (!strcmp(cmd, "top")) return cli_top();
if (!strcmp(cmd, "events")) return cli_stub("events", "2"); if (!strcmp(cmd, "events")) return cli_events(n - 1, v + 1);
if (!strcmp(cmd, "policy")) return cli_stub("policy", "3"); if (!strcmp(cmd, "policy")) return cli_stub("policy", "3");
fprintf(stderr, "usage: meow_power <%s>\n", cli_usage()); fprintf(stderr, "usage: meow_power <%s>\n", cli_usage());
return 1; return 1;
Binary file not shown.
+65 -7
View File
@@ -17,7 +17,7 @@ ck() { # ck <名> <期望子串> <实际>
# ---- 搭假节点 ---- # ---- 搭假节点 ----
PS="$T/ps"; BL="$T/bl"; NET="$T/net"; RF="$T/rfkill"; BT="$T/bt" PS="$T/ps"; BL="$T/bl"; NET="$T/net"; RF="$T/rfkill"; BT="$T/bt"
mkdir -p "$PS/battery" "$PS/mp2731-charger" "$PS/fusb302-typec-source" "$PS/ac" \ mkdir -p "$PS/battery" "$PS/mp2731-charger" "$PS/fusb302-typec-source" "$PS/ac" \
"$BL" "$NET/wlan0" "$RF/rfkill0" "$BT" "$BL" "$NET/wlan0" "$RF/rfkill0" "$BT/hci0"
echo 94 > "$PS/battery/capacity" echo 94 > "$PS/battery/capacity"
echo Discharging > "$PS/battery/status" echo Discharging > "$PS/battery/status"
echo 4275000 > "$PS/battery/voltage_now" echo 4275000 > "$PS/battery/voltage_now"
@@ -31,6 +31,7 @@ echo 101 > "$BL/max_brightness" # R3 Pro II 实测量程 0-101
echo 50 > "$BL/actual_brightness" # 中档(49%) echo 50 > "$BL/actual_brightness" # 中档(49%)
echo 50 > "$BL/brightness" echo 50 > "$BL/brightness"
echo up > "$NET/wlan0/operstate" echo up > "$NET/wlan0/operstate"
echo 1 > "$NET/wlan0/carrier" # 已关联(carrier 0=未关联)
echo bluetooth > "$RF/rfkill0/type" echo bluetooth > "$RF/rfkill0/type"
echo 1 > "$RF/rfkill0/soft" # BT 被 soft 块 → off echo 1 > "$RF/rfkill0/soft" # BT 被 soft 块 → off
cat > "$T/netdev" <<'EOF' cat > "$T/netdev" <<'EOF'
@@ -43,7 +44,8 @@ echo meow > "$T/boot_mode"
ENVS="MEOW_PS_DIR=$PS MEOW_BL_DIR=$BL MEOW_NET_DIR=$NET MEOW_RFKILL_DIR=$RF MEOW_BT_DIR=$BT ENVS="MEOW_PS_DIR=$PS MEOW_BL_DIR=$BL MEOW_NET_DIR=$NET MEOW_RFKILL_DIR=$RF MEOW_BT_DIR=$BT
MEOW_NETDEV=$T/netdev MEOW_PROC_STAT=$T/proc_stat MEOW_BOOT_MODE=$T/boot_mode MEOW_NETDEV=$T/netdev MEOW_PROC_STAT=$T/proc_stat MEOW_BOOT_MODE=$T/boot_mode
MEOW_PM_MODEL=$T/model.conf MEOW_AUDIO_SOCK=$T/no_audio.sock MEOW_PM_SOCK=$T/pm.sock" MEOW_PM_MODEL=$T/model.conf MEOW_AUDIO_SOCK=$T/no_audio.sock MEOW_PM_SOCK=$T/pm.sock
MEOW_PM_NO_HCICONFIG=1"
ln -sf "$(readlink -f "$BIN")" "$T/meow_power" # argv[0] 双形态 ln -sf "$(readlink -f "$BIN")" "$T/meow_power" # argv[0] 双形态
# ---- T1: 守护直读(无守护 → 降级直读) ---- # ---- T1: 守护直读(无守护 → 降级直读) ----
@@ -92,6 +94,20 @@ sleep 3
OUT=$(env $ENVS $Q "$T/meow_power" estimate 2>/dev/null) OUT=$(env $ENVS $Q "$T/meow_power" estimate 2>/dev/null)
ck T5a "wifi=600(tx)" "$OUT" ck T5a "wifi=600(tx)" "$OUT"
# ---- T5b: wifi 未关联(carrier=0)→ unassoc 档 ----
echo 0 > "$NET/wlan0/carrier"
sleep 2
OUT=$(env $ENVS $Q "$T/meow_power" estimate 2>/dev/null)
ck T5b "wifi=80(unassoc)" "$OUT"
echo 1 > "$NET/wlan0/carrier"
# ---- T5c: BT up(rfkill 未软块 + hci0 存在;host 无 hciconfig → 回退 up) ----
echo 0 > "$RF/rfkill0/soft"
sleep 2
OUT=$(env $ENVS $Q "$T/meow_power" estimate 2>/dev/null)
ck T5c "bt=60(up)" "$OUT"
echo 1 > "$RF/rfkill0/soft"
# ---- T6: 息屏(actual_brightness=0)→ 背光/面板归零 ---- # ---- T6: 息屏(actual_brightness=0)→ 背光/面板归零 ----
echo 0 > "$BL/actual_brightness" echo 0 > "$BL/actual_brightness"
OUT=$(env $ENVS $Q "$T/meow_power" estimate 2>/dev/null) OUT=$(env $ENVS $Q "$T/meow_power" estimate 2>/dev/null)
@@ -120,17 +136,59 @@ sleep 2
OUT=$(env $ENVS $Q "$T/meow_power" estimate 2>/dev/null) OUT=$(env $ENVS $Q "$T/meow_power" estimate 2>/dev/null)
ck T8d "est_mw=" "$OUT" # 热切换回全功能 ck T8d "est_mw=" "$OUT" # 热切换回全功能
# ---- T9: CLI 桩(期 2/3) ---- # ---- T9: 期 2 CLI — history(守护采样环)/policy(期3桩) ----
OUT=$(env $ENVS $Q "$T/meow_power" history 2>&1); RC=$? sleep 1 # 再采几拍,history 有样本
ck T9a "期 2" "$OUT" OUT=$(env $ENVS $Q "$T/meow_power" history 2>&1)
[ $RC -eq 1 ] && { PASS=$((PASS+1)); echo "PASS T9b rc=1"; } || { FAIL=$((FAIL+1)); echo "FAIL T9b rc=$RC"; } ck T9a "history:" "$OUT"
ck T9b "采样点" "$OUT"
ck T9c "est_mW" "$OUT"
echo "$OUT" | grep -q "守护不可达" && { FAIL=$((FAIL+1)); echo "FAIL T9d 应走守护"; } || { PASS=$((PASS+1)); echo "PASS T9d via-daemon"; }
OUT=$(env $ENVS $Q "$T/meow_power" policy 2>&1) OUT=$(env $ENVS $Q "$T/meow_power" policy 2>&1)
ck T9c "期 3" "$OUT" ck T9e "期 3" "$OUT"
# ---- T10: meow_powermgr 名直调子命令也走 CLI ---- # ---- T10: meow_powermgr 名直调子命令也走 CLI ----
OUT=$(env $ENVS $Q "$BIN" model 2>/dev/null) OUT=$(env $ENVS $Q "$BIN" model 2>/dev/null)
ck T10a "nominal_mah=1600" "$OUT" ck T10a "nominal_mah=1600" "$OUT"
# ---- T13: mp2731 current_now 充电功率实测接入(期 2 项 5)----
echo Charging > "$PS/mp2731-charger/status"
echo 1500000 > "$PS/mp2731-charger/current_now"
echo 4200000 > "$PS/mp2731-charger/constant_charge_voltage"
sleep 2
OUT=$(env $ENVS $Q "$T/meow_power" estimate 2>/dev/null)
ck T13a "charge=+6300mW" "$OUT" # 4.2V×1.5A=6.3W
ck T13b "confidence=measured" "$OUT" # 充电功率升 measured
OUT=$(env $ENVS $Q "$T/meow_power" status 2>/dev/null)
ck T13c "state=Charging" "$OUT" # 权威链 mp2731=Charging
echo Discharging > "$PS/mp2731-charger/status"
echo 0 > "$PS/mp2731-charger/current_now"
sleep 2
OUT=$(env $ENVS $Q "$T/meow_power" estimate 2>/dev/null)
ck T13d "charge=null" "$OUT" # 放电 → 无实测轨
# ---- T14: 电源事件流(充电器插拔 + 息屏,App 前后台诚实标注不支持)----
OUT=$(env $ENVS $Q "$T/meow_power" events 2>&1)
ck T14a "events:" "$OUT"
ck T14b "charger" "$OUT" # 插/拔充电器事件 tag
echo 0 > "$BL/actual_brightness" # 息屏 → screen 事件
sleep 2
OUT=$(env $ENVS $Q "$T/meow_power" events 2>&1)
ck T14c "screen" "$OUT"
ck T14d "息屏" "$OUT"
echo 50 > "$BL/actual_brightness" # 恢复亮屏(供后测)
# ---- T15: top 功耗分解实时排序(一次输出)----
OUT=$(env $ENVS MEOW_PM_TOP_ONCE=1 $Q "$T/meow_power" top 2>/dev/null)
ck T15a "功耗分解" "$OUT"
ck T15b "screen" "$OUT"
ck T15c "baseline" "$OUT"
echo "$OUT" | grep -q "cpu" && { PASS=$((PASS+1)); echo "PASS T15d cpu行"; } || { FAIL=$((FAIL+1)); echo "FAIL T15d"; }
# ---- T16: 模型系数路径(新定案 /data/meow/powermgr/power_model.conf)----
ENVS_NM=$(echo "$ENVS" | tr ' ' '\n' | grep -v '^MEOW_PM_MODEL=' | tr '\n' ' ')
OUT=$(env $ENVS_NM $Q "$T/meow_power" model 2>/dev/null)
ck T16a "powermgr/power_model.conf" "$OUT"
# ---- T11: 守护停 → CLI 自动降级直读不崩 ---- # ---- T11: 守护停 → CLI 自动降级直读不崩 ----
kill $DPID 2>/dev/null; wait $DPID 2>/dev/null; DPID="" kill $DPID 2>/dev/null; wait $DPID 2>/dev/null; DPID=""
sleep 1 sleep 1