diff --git a/patch_v2/meow_powermgr/meow_powermgr b/patch_v2/meow_powermgr/meow_powermgr index 6912ce9..9558e42 100755 Binary files a/patch_v2/meow_powermgr/meow_powermgr and b/patch_v2/meow_powermgr/meow_powermgr differ diff --git a/patch_v2/meow_powermgr/meow_powermgr.c b/patch_v2/meow_powermgr/meow_powermgr.c index 470aa0d..8589e5c 100644 --- a/patch_v2/meow_powermgr/meow_powermgr.c +++ b/patch_v2/meow_powermgr/meow_powermgr.c @@ -12,6 +12,22 @@ * 模型系数路径迁移到 /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)。 + * 期 3 新增(本文件,POWER_MANAGER_DESIGN §9 期 3): + * ① policy 生效(期 2 桩落地):IPC MSG_PM_POLICY_GET/D2 + MSG_PM_POLICY_SET/D3 + CLI + * `meow_power policy show|set`。策略项:profile 四档(performance/balanced/powersave/ultra) + * + 低电阈值(power.low_pct/critical_pct 读 settings.conf)+ 亮度上限(power.brightness_cap) + * + 息屏上限(power.screen_timeout_cap)。**默认全关**(profile=balanced 无操作=现状,cap=0 不生效), + * 用户手动开(§附 3 只能用户手动开)。**生效方式:powermgr 不直接写系统**——policy 只写 settings.conf + * `power.*` 键(launcher/sysui 期 4 消费;本服务只报告 effective 值,绝不 poke sysfs/背光/息屏)。 + * ② 标定工具(为用户的标定实验备好,§7 项 7/8):`meow_power calibrate start <场景>|stop|list`—— + * 守护进程内 2s 节拍采样 电流(mp2731 current_now)+亮度+CPU+wifi/bt 状态+电量%变化,写 TF + * MeowOS/logs/power_calib_.csv(无卡降级 /tmp);用户实验只需在真机跑 start/stop。 + * ③ 模型 conf 单键 setter:`meow_power model set `(读改写单键 + 原子写 tmp+rename)。 + * ④ events 扩展:App 前后台切换——launcher MSG_ACT_FOCUS 为 app→launcher 单向上报,powermgr 侧无 + * 导出点可接,**诚实标注**:记录「期 4 接 launcher」扩展,本期不编造该事件(延续期 2 口径)。 + * ⑤ 动作级鉴权(POLICY_SET/CALIB_*):SO_PEERCRED→pid→exe→classify(同 svc_power), + * 动作级须 root + 系统身份(os.meow 前缀或受信系统二进制);读级(POLICY_GET)任意调用者。 + * MEOW_PM_AUTH_TEST=1 仅宿主测试关闭动作级鉴权(生产绝不设)。 * * 单一二进制双形态(同 svc_power): * - svcd 空参拉起(板载 services.d/meow_powermgr.conf,trigger=boot/restart=on-fail)→ @@ -61,12 +77,22 @@ /* 期 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_POLICY_GET 0x00D2 /* ↑ 策略读(base 空负载 → 回 MSG_PM_RESULT policy JSON;读级任意调用者) */ +#define MSG_PM_POLICY_SET 0x00D3 /* ↑ 策略写(req: m_pm_policy_set_t;动作级=root+系统身份,期 3) */ +#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 清单 */ +#define MSG_PM_EVENTS 0x00D6 /* ↑ 事件流查询(req: u32 count=N,0=默认) */ +#define MSG_PM_EVENTS_RESULT 0x00D7 /* ↓ 事件 JSON 清单 */ +#define MSG_PM_CALIB_START 0x00D8 /* ↑ 标定启动(req: 场景名 NUL 串;动作级;0x00D8 空闲位,期 3) */ +#define MSG_PM_CALIB_STOP 0x00D9 /* ↑ 标定停止(req 空;动作级)→ 回 MSG_PM_RESULT calib JSON */ #endif +/* 期 3 · 策略写负载(item="profile" / "low-pct" / "crit-pct" / "bri-cap" / "scr-cap")。 */ +typedef struct m_pm_policy_set { + char item[16]; /* 策略项名 */ + char val[32]; /* 值(profile 枚举字符串或数字) */ +} m_pm_policy_set_t; /* 48 B */ + /* ---------------- 路径(全部可经 env 覆盖,宿主/qemu 桩测试用) ---------------- */ 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 */ @@ -80,6 +106,10 @@ static char g_model_conf[160] = "/usr/data/meow/powermgr/power_model.conf"; /* M (= /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_settings[160] = "/usr/data/meow/settings.conf"; /* MEOW_SETTINGS_FILE + (policy power.* 键落点;与 svc_power/launcher 同单点事实源) */ +static char g_log_dir[192] = "/usr/data/mnt/sd_0/MeowOS/logs"; /* MEOW_LOG_DIR(标定 CSV 落点, + 无卡降级 /tmp;同 netmgrd/audiod 日志根) */ static const char *g_sock = "/usr/data/meow_pm.sock"; /* MEOW_PM_SOCK/--sock */ #define SAMPLE_SEC_DEF 30 /* 电池采样/状态刷新节拍(秒;MEOW_PM_SAMPLE_SEC 覆盖,测试用) */ @@ -134,6 +164,166 @@ static void ps_path(char *buf, size_t cap, const char *node) snprintf(buf, cap, "%s/%s", g_ps_dir, node); } +/* ---------------- settings.conf 读写(policy power.* 键;单点事实源与 svc_power/launcher 共用) ---------------- + * 键格式:key=value('=' 切分,key 去首尾空白精确匹配,避免 power.low_pct 误配 power.low_pct_xx + * 或 audio.volume 误配 audio.volume_max——同 svc_power settings_get_int_trim 解析口径)。 + * 写:读原文件→逐字保留非目标行→命中替换/末尾追加→**原子写**(tmp+rename,避免半截文件), + * 比 svc_power 的 O_TRUNC 更稳(settings 与 launcher 同写,防写一半被读到)。 */ +static int settings_get_str(const char *key, char *out, int cap, const char *def) +{ + FILE *f = fopen(g_settings, "r"); + if (!f) { snprintf(out, cap, "%s", def ? def : ""); return 1; } + size_t kl = strlen(key); + char line[256]; + int found = 0; + while (fgets(line, sizeof line, f)) { + char *eq = strchr(line, '='); + if (!eq) continue; + char *k = line; + while (*k == ' ' || *k == '\t') k++; + long n = (long)(eq - k); + while (n > 0 && (k[n - 1] == ' ' || k[n - 1] == '\t')) n--; + if ((size_t)n != kl || strncmp(k, key, kl) != 0) continue; + char *v = eq + 1; + while (*v == ' ' || *v == '\t') v++; + int i = 0; + while (v[i] && v[i] != '\n' && v[i] != '\r' && v[i] != ' ' && v[i] != '\t' && i < cap - 1) { out[i] = v[i]; i++; } + out[i] = 0; + found = 1; + break; + } + fclose(f); + if (!found) { snprintf(out, cap, "%s", def ? def : ""); return 1; } + return 0; +} + +/* 读 settings.conf 某键(找到整数写入 *out)。返回 0=命中。 */ +static int settings_get_int_x(const char *key, int *out) +{ + char buf[32]; + if (settings_get_str(key, buf, sizeof buf, "") != 0 || !buf[0]) return 1; + *out = atoi(buf); + return 0; +} + +/* 原子写 settings.conf(tmp+rename)。text 为完整新内容。成功 0。 */ +static int settings_write_atomic(const char *text) +{ + char tmp[192]; + snprintf(tmp, sizeof tmp, "%s.tmp", g_settings); + int fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd < 0) return -1; + size_t tl = strlen(text); + ssize_t w = write(fd, text, tl); + close(fd); + if (w < 0 || (size_t)w != tl) { unlink(tmp); return -1; } + if (rename(tmp, g_settings) < 0) { unlink(tmp); return -1; } + return 0; +} + +/* 读 settings.conf 全文(≤2KB,settings 为 KB 级小文件)。返回字节数;无文件返回 0。 */ +static size_t settings_read_text(char *buf, size_t cap) +{ + FILE *f = fopen(g_settings, "r"); + if (!f) { buf[0] = 0; return 0; } + size_t n = 0; + int c; + while (n < cap - 1 && (c = fgetc(f)) != EOF) buf[n++] = (char)c; + fclose(f); + buf[n] = 0; + return n; +} + +/* 更新/插入 settings.conf 某键(值字符串;保留头部注释与其余键逐字,原子写)。 */ +static int settings_set_str(const char *key, const char *val) +{ + char txt[2048], out[2048]; + size_t n = settings_read_text(txt, sizeof txt); + size_t kl = strlen(key), o = 0; + int found = 0; + size_t start = 0; + while (start < n) { + size_t i = 0; + while (start + i < n && txt[start + i] != '\n') i++; + char line[256]; + if (i >= sizeof line - 1) i = sizeof line - 2; + memcpy(line, txt + start, i); line[i] = 0; + start += i; + if (start < n && txt[start] == '\n') start++; + char *q = line; + while (*q == ' ' || *q == '\t') q++; + int is_target = (strncmp(q, key, kl) == 0 && (q[kl] == '=' )); + if (is_target) { + int l = snprintf(out + o, sizeof out - o, "%s=%s\n", key, val); + if (l > 0 && o + (size_t)l < sizeof out) o += (size_t)l; + found = 1; + continue; /* 跳过旧行 */ + } + int l = snprintf(out + o, sizeof out - o, "%s\n", line); + if (l > 0 && o + (size_t)l < sizeof out) o += (size_t)l; + } + if (o > 0 && out[o - 1] == '\n') o--; /* 去掉尾多余换行(避免双空行) */ + if (!found) { + int l = snprintf(out + o, sizeof out - o, "%s%s=%s\n", o ? "\n" : "", key, val); + if (l > 0 && o + (size_t)l < sizeof out) o += (size_t)l; + } + out[o] = 0; + return settings_write_atomic(out); +} + +static int settings_set_int(const char *key, int val) +{ + char v[32]; + snprintf(v, sizeof v, "%d", val); + return settings_set_str(key, v); +} + +/* ---------------- 策略(policy)模型:profile 四档 + 低电阈值 + 亮度/息屏上限 ---------------- + * 默认全关(POWER_MANAGER_DESIGN §9 期 3):profile=balanced(无操作=现状),cap=0(不生效)。 + * 用户手动开(§附 3)。**powermgr 不直接写系统**:policy set 只写 settings.conf `power.*` 键, + * launcher/sysui 期 4 消费;本服务只算 effective 值用于报告,绝不 poke 背光/息屏/wlan0/bt。 */ +typedef struct { + char profile[16]; /* performance/balanced/powersave/ultra */ + int low_pct; /* power.low_pct(默认 15) */ + int crit_pct; /* power.critical_pct(默认 5) */ + int bri_cap; /* power.brightness_cap(0=off;量程 0-101 实测) */ + int scr_cap; /* power.screen_timeout_cap(0=off;秒) */ +} pm_policy_t; + +static void policy_defaults(pm_policy_t *p) +{ + snprintf(p->profile, sizeof p->profile, "balanced"); + p->low_pct = 15; p->crit_pct = 5; p->bri_cap = 0; p->scr_cap = 0; +} + +static const char *const k_policy_profiles[] = { "performance", "balanced", "powersave", "ultra", NULL }; + +/* 读 settings.conf 的 power.* 键(键缺失/无文件 → 默认=全关)。 */ +static void policy_load(pm_policy_t *p) +{ + policy_defaults(p); + settings_get_str("power.profile", p->profile, sizeof p->profile, "balanced"); + if (settings_get_int_x("power.low_pct", &p->low_pct)) p->low_pct = 15; + if (settings_get_int_x("power.critical_pct", &p->crit_pct)) p->crit_pct = 5; + if (settings_get_int_x("power.brightness_cap", &p->bri_cap)) p->bri_cap = 0; + if (settings_get_int_x("power.screen_timeout_cap", &p->scr_cap)) p->scr_cap = 0; + /* 背光量程 0-101(POWER_HW_FACTS 项 6);cap 越界 clamp,防手误写 255 造成无效上限。 + * profile 单独存(供期 4 launcher 依据 §5.1 表计算 min(用户值,档位值));本服务不在此推算 + * 档位隐含数值——设计 §5.1 的 180/120 是按 0-255 假定写的旧值,0-101 实测量程下推算会失真, + * 诚实处理:只透传用户显式 cap,档位映射留给 launcher 期 4 定案后落地。 */ + if (p->bri_cap < 0) p->bri_cap = 0; + if (p->bri_cap > 101) p->bri_cap = 101; + if (p->scr_cap < 0) p->scr_cap = 0; + if (p->low_pct < 1) p->low_pct = 1; + if (p->crit_pct < 1) p->crit_pct = 1; +} + +/* 是否已有「被手动开启」的策略(非默认全关态):profile!=balanced 或任一 cap>0。 */ +static int policy_enacted(const pm_policy_t *p) +{ + return (strcmp(p->profile, "balanced") != 0) || p->bri_cap > 0 || p->scr_cap > 0; +} + /* ---------------- 权威充电链(0-diff 照抄 svc_power charging_status_str 口径) ---------------- * mp2731-charger/status(Charging/Full=充电中)为权威;不可读回退 fusb302-typec-source/online==1 * (USB-C PD 控制器插线检测);再回退旧逻辑 battery/status==Charging || ac/online==1。 */ @@ -277,6 +467,90 @@ static int model_load(pm_model_t *m) return 0; } +/* 模型键是否在已知集合(CLI `model set` 校验用,拒绝未知键防写坏 conf)。 */ +static int model_key_is_known(const char *key) +{ + static const char *const keys[] = { + "nominal_mah", "bl_low_mw", "bl_mid_mw", "bl_high_mw", "panel_on_mw", + "wifi_unassoc_mw", "wifi_idle_mw", "wifi_tx_mw", "bt_up_mw", "bt_conn_mw", + "audio_idle_mw", "audio_play_mw", "cpu_full_mw", "baseline_mw", NULL + }; + for (int i = 0; keys[i]; i++) if (!strcmp(key, keys[i])) return 1; + return 0; +} + +static int atomic_write_path(const char *path, const char *text); /* fwd:model_conf_set_key 用它 */ + +/* 模型单键 setter:读改写单键 + **原子写**(tmp+rename,同 netmgrd 口径)。成功 0。 + * 保留其它键与注释行逐字;conf 缺失 → 生成 `key=value`(model_load 缺失键仍走内置默认)。 */ +static int model_conf_set_key(const char *key, int val) +{ + char txt[2048] = "", out[2048]; + size_t n = 0; + FILE *f = fopen(g_model_conf, "r"); + if (f) { + size_t r = 0; + int c; + while (r < sizeof txt - 1 && (c = fgetc(f)) != EOF) txt[r++] = (char)c; + txt[r] = 0; + n = r; + fclose(f); + } + if (!model_key_is_known(key)) return -1; + size_t kl = strlen(key), o = 0; + if (n == 0) { + /* conf 缺失 → 生成注释头 + 键(其余键 model_load 走内置默认) */ + int l = snprintf(out, sizeof out, "# meow_powermgr 经验功耗模型(POWER_MANAGER_DESIGN §3.2)\n" + "# 系数待真机标定(POWER_HW_FACTS §1 项 7/8)\n%s=%d\n", key, val); + if (l > 0) o = (size_t)l; + } else { + int found = 0; + size_t start = 0; + while (start < n) { + size_t i = 0; + while (start + i < n && txt[start + i] != '\n') i++; + char line[256]; + if (i >= sizeof line - 1) i = sizeof line - 2; + memcpy(line, txt + start, i); line[i] = 0; + start += i; + if (start < n && txt[start] == '\n') start++; + char *q = line; + while (*q == ' ' || *q == '\t') q++; + int is_target = (strncmp(q, key, kl) == 0 && q[kl] == '='); + if (is_target) { + int l = snprintf(out + o, sizeof out - o, "%s=%d\n", key, val); + if (l > 0 && o + (size_t)l < sizeof out) o += (size_t)l; + found = 1; + continue; + } + int l = snprintf(out + o, sizeof out - o, "%s\n", line); + if (l > 0 && o + (size_t)l < sizeof out) o += (size_t)l; + } + if (o > 0 && out[o - 1] == '\n') o--; + if (!found) { + int l = snprintf(out + o, sizeof out - o, "\n%s=%d\n", key, val); + if (l > 0 && o + (size_t)l < sizeof out) o += (size_t)l; + } + } + out[o] = 0; + return atomic_write_path(g_model_conf, out); +} + +/* 通用原子写(写 path.tmp → rename(path);同 netmgrd 口径,防半截配置)。成功 0。 */ +static int atomic_write_path(const char *path, const char *text) +{ + char tmp[256]; + snprintf(tmp, sizeof tmp, "%s.tmp", path); + int fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd < 0) return -1; + size_t tl = strlen(text); + ssize_t w = write(fd, text, tl); + close(fd); + if (w < 0 || (size_t)w != tl) { unlink(tmp); return -1; } + if (rename(tmp, path) < 0) { unlink(tmp); return -1; } + return 0; +} + /* ---------------- 采样环(守护进程;容量差分速率 → 续航估算 §3.1/§3.3 derived; * 期 2 同时服务 history 采样点:pct/est_mw/充电态/屏幕态/亮度,内存环形不落盘,重启清零) ---------------- */ typedef struct { @@ -707,10 +981,68 @@ static void build_json(uint32_t subset, int full, const batt_t *b, jadd(&w, "}"); } } - if (subset & PM_SUBSET_POLICY) jadd(&w, ",\"policy\":null"); /* 期 3 桩 */ + if (subset & PM_SUBSET_POLICY) { + /* 期 3:策略快照(读 settings.conf power.* 键)。powermgr 只读+报告 effective,绝不写系统。 */ + pm_policy_t pol; + policy_load(&pol); + jadd(&w, ",\"policy\":{"); + jadd(&w, "\"profile\":\"%s\",", pol.profile); + jadd(&w, "\"low_pct\":%d,\"critical_pct\":%d,", pol.low_pct, pol.crit_pct); + jadd(&w, "\"brightness_cap\":%d,\"screen_timeout_cap\":%d,", pol.bri_cap, pol.scr_cap); + jadd(&w, "\"enacted\":%d,", policy_enacted(&pol) ? 1 : 0); + jadd(&w, "\"note\":\"powermgr 只写 power.* 键,launcher/sysui 期4消费;本服务不直接写系统/绝不poke背光息屏\"}"); + } jadd(&w, "}"); } +/* 策略 JSON(IPC GET 响应 / CLI policy show 共用;纯读 settings,无副作用)。 */ +static void build_policy_json(const pm_policy_t *p, char *out, size_t cap) +{ + jw_t w = { out, cap, 0 }; + out[0] = 0; + jadd(&w, "{\"kind\":\"policy\",\"profile\":\"%s\",\"low_pct\":%d,\"critical_pct\":%d,", + p->profile, p->low_pct, p->crit_pct); + jadd(&w, "\"brightness_cap\":%d,\"screen_timeout_cap\":%d,\"enacted\":%d,", + p->bri_cap, p->scr_cap, policy_enacted(p) ? 1 : 0); + jadd(&w, "\"note\":\"powermgr 只写 power.* 键;launcher/sysui 期4消费生效;本服务不直接写系统\"}"); +} + +/* 应用一条 policy set(item+val → 写 settings.conf power.* 键,原子)。成功 0;失败写 why。 */ +static int policy_set_item(const char *item, const char *val, char *why, int cap) +{ + if (!item || !val) { snprintf(why, cap, "缺少 item/val"); return -1; } + if (!strcmp(item, "profile")) { + for (int i = 0; k_policy_profiles[i]; i++) { + if (!strcmp(k_policy_profiles[i], val)) { + if (settings_set_str("power.profile", val) != 0) { snprintf(why, cap, "写 settings 失败"); return -1; } + return 0; + } + } + snprintf(why, cap, "profile 须为 %s/%s/%s/%s", k_policy_profiles[0], k_policy_profiles[1], + k_policy_profiles[2], k_policy_profiles[3]); + return -1; + } + int v = atoi(val); + if (!strcmp(item, "low-pct")) { + if (v < 1 || v > 99) { snprintf(why, cap, "low-pct 须 1-99"); return -1; } + return settings_set_int("power.low_pct", v); + } + if (!strcmp(item, "crit-pct")) { + if (v < 1 || v > 99) { snprintf(why, cap, "crit-pct 须 1-99"); return -1; } + return settings_set_int("power.critical_pct", v); + } + if (!strcmp(item, "bri-cap")) { + if (v < 0 || v > 101) { snprintf(why, cap, "bri-cap 须 0-101(背光实测量程,0=不生效)"); return -1; } + return settings_set_int("power.brightness_cap", v); + } + if (!strcmp(item, "scr-cap")) { + if (v < 0) { snprintf(why, cap, "scr-cap 须 ≥0 秒(0=不生效)"); return -1; } + return settings_set_int("power.screen_timeout_cap", v); + } + snprintf(why, cap, "未知 item(profile|low-pct|crit-pct|bri-cap|scr-cap)"); + return -1; +} + /* ---------------- IPC 帧读写(同 svc_power 约定,全小端) ---------------- */ static int x_read_full(int fd, uint8_t *buf, size_t n) { @@ -793,6 +1125,22 @@ 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; +/* ---------------- 标定(calibrate)会话态(期 3;守护进程内 2s 节拍,用户跑 start/stop) ---------------- + * 为用户标定实验(§7 项 7/8:纯亮屏/息屏待机/放音乐/放视频四类场景)采集样本流: + * ts,cap_pct,vol_mv,current_ua,charge_mw,brightness_pct,cpu_util_pct,wifi_state,bt_state,scene + * 写 TF MeowOS/logs/power_calib_.csv(无卡降级 /tmp)。2 行一节拍逐行 fflush(突发掉电不丢)。 */ +#define CALIB_SEC 2 /* 标定采样节拍(秒) */ +#define CALIB_SCENE_MAX 48 /* 场景名上限(UTF-8 字节) */ +static int g_calib_active = 0; +static char g_calib_scene[CALIB_SCENE_MAX]; +static FILE *g_calib_fp = NULL; +static char g_calib_csv[384]; +static long g_calib_rows = 0; +static long g_calib_first_pct = -1, g_calib_last_pct = -1; +static long g_calib_start_mono = 0; +static long g_calib_cpu_pt = -1, g_calib_cpu_pi = 0; /* 标定专用 CPU 差分基线(与 history 无关) */ +static long g_calib_prx = -1, g_calib_ptx = 0; /* 标定专用 netdev 增量基线(判 wifi tx 档) */ + static int boot_mode_is_meow(void) { char buf[16] = ""; @@ -802,12 +1150,12 @@ static int boot_mode_is_meow(void) /* ---------------- 电源事件流(守护进程;内存环形,重启清零,不落盘) ---------------- * 可检测:① 充电器插拔(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% 触发恢复事件) */ + * ③ 屏幕亮灭 ④ 背光跳变(≥30% 点)。⚠ App 前后台切换:launcher MSG_ACT_FOCUS 为 app→launcher 单向上报, + * powermgr 侧**无导出点可接**(launcher 未对外提供当前顶层 activity 查询;/tmp/meow_*_active 是 watchdog + * 让位的行为标记而非 App 前后台),**诚实标注**:记录「期 4 接 launcher」扩展,本期不编造该事件(延续期 2 口径)。 */ +#define EV_LOW_PCT 15 /* 低电阈值默认(期 3 改由 settings power.low_pct 覆盖,见 detect_events) */ +#define EV_CRIT_PCT 5 /* 极低电阈值默认(期 3 改由 settings power.critical_pct 覆盖) */ +#define EV_LOW_REC 20 /* 恢复阈值默认(=low+5) */ #define EV_BRI_JUMP 30 /* 背光跳变阈值(百分点) */ typedef struct { @@ -836,6 +1184,13 @@ static void ev_push(int type, int arg, int pct, const char *fmt, ...) static void detect_events(const batt_t *b, const est_t *e) { + /* 期 3:低电阈值读 settings power.low_pct/critical_pct(§5.2;键缺失 → 默认 15/5,恢复=low+5)。 */ + pm_policy_t pol; + policy_load(&pol); + int low = pol.low_pct, crit = pol.crit_pct, rec = pol.low_pct + 5; + if (low < 1) low = EV_LOW_PCT; + if (crit < 1) crit = EV_CRIT_PCT; + if (rec <= crit) rec = crit + 1; /* ① 充电器插拔 */ if (g_prev_charging >= 0 && g_prev_charging != b->charging) { if (b->charging) ev_push(0, 1, b->pct, "插上充电器,开始充电 (state=%s)", b->state); @@ -844,13 +1199,13 @@ static void detect_events(const batt_t *b, const est_t *e) /* ② 低电阈值(放电下降沿触发;充电回阈值恢复) */ 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 >= low && b->pct < low) + ev_push(1, 0, b->pct, "电量低 %d%%(阈值 %d%%)", b->pct, low); + if (g_prev_pct >= crit && b->pct < crit) + ev_push(1, 1, b->pct, "电量极低 %d%%(阈值 %d%%)", b->pct, crit); } - 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); + if (g_prev_pct < rec && b->pct >= rec) + ev_push(1, 2, b->pct, "电量回到 %d%%(≥%d%% 恢复)", b->pct, rec); } /* ③ 屏幕亮灭 / ④ 背光跳变(只读模式 screen=-1 → 跳过;息屏时不算背光跳变) */ if (e->screen_on >= 0) { @@ -878,6 +1233,8 @@ static void daemon_tick(void) g_boot_mode, fm ? "=meow" : "!=meow/missing"); g_full_mode = fm; } + /* 期 3:每拍重载模型 conf(`model set`/手工改系数即时生效;conf 缺失走内置默认,读侧无害)。 */ + if (g_full_mode) model_load(&g_model); batt_t b; batt_read(&b); long t = -1, i = 0; @@ -891,6 +1248,146 @@ static void daemon_tick(void) detect_events(&b, &e); } +/* ---------------- 标定(calibrate)实现(守护进程内;用户 CLI start/stop 驱动) ---------------- */ +static void calib_csv_path(void) +{ + time_t t = time(NULL); + char ts[32]; + snprintf(ts, sizeof ts, "%ld", (long)t); + /* TF 可写 → MeowOS/logs;否则 /tmp(同 netmgrd 无卡降级)。路径去重(同秒 → _N)。 */ + if (access(g_log_dir, W_OK) != 0) { + char d[200]; + snprintf(d, sizeof d, "%s", g_log_dir); + char *s = strrchr(d, '/'); + if (s) *s = 0; + mkdir(d, 0755); /* 建父层(如 /usr/data/mnt/sd_0/MeowOS);外层已挂载则瞬时成功 */ + mkdir(g_log_dir, 0755); + } + const char *base = (access(g_log_dir, W_OK) == 0) ? g_log_dir : "/tmp"; + char p[384]; + for (int n = 0; n < 100; n++) { + if (n == 0) snprintf(p, sizeof p, "%s/power_calib_%s.csv", base, ts); + else snprintf(p, sizeof p, "%s/power_calib_%s_%d.csv", base, ts, n); + if (access(p, F_OK) != 0) break; + } + snprintf(g_calib_csv, sizeof g_calib_csv, "%s", p); +} + +static void calib_write_header(const char *scene) +{ + if (!g_calib_fp) return; + fprintf(g_calib_fp, "# meow_powermgr calibrate scene=%s ts=%ld\n", scene, (long)time(NULL)); + fprintf(g_calib_fp, "ts,cap_pct,vol_mv,current_ua,charge_mw,brightness_pct,cpu_util_pct,wifi_state,bt_state\n"); + fflush(g_calib_fp); +} + +/* 启动标定会话(CLI calibrate start 经 IPC;已启动 → 报错)。返回 0=启动成功。 */ +static int calib_start(const char *scene) +{ + if (g_calib_active) return -1; + if (!scene || !scene[0]) return -1; + if (!g_full_mode) { svc_log("calib: readonly 模式不采样(HibyOS 降级)"); return -1; } + calib_csv_path(); + g_calib_fp = fopen(g_calib_csv, "w"); + if (!g_calib_fp) { svc_log("calib: open %s fail: %s", g_calib_csv, strerror(errno)); return -1; } + snprintf(g_calib_scene, sizeof g_calib_scene, "%s", scene); + calib_write_header(scene); + g_calib_active = 1; + g_calib_rows = 0; + g_calib_first_pct = -1; g_calib_last_pct = -1; + g_calib_start_mono = mono_now(); + g_calib_cpu_pt = -1; g_calib_cpu_pi = 0; + g_calib_prx = -1; g_calib_ptx = 0; + svc_log("calib: start scene=%s -> %s", scene, g_calib_csv); + return 0; +} + +/* 单次标定采样(CALIB_SEC 节拍):电流(μA)/充电功率/亮度/CPU 差分/wifi/bt/电量%。 */ +static void calib_tick(void) +{ + if (!g_calib_active || !g_calib_fp) return; + batt_t b; + batt_read(&b); + /* CPU 差分(独立基线,勿动 history 的 g_cpu_prev_*) */ + long t = -1, i = 0; + int cu = cpu_util_pct(g_calib_cpu_pt, g_calib_cpu_pi, &t, &i); + if (t >= 0) { g_calib_cpu_pt = t; g_calib_cpu_pi = i; } + if (cu < 0) cu = -1; + /* 亮度/面板、wifi 状态、bt 状态 */ + int on = -1; + int bp = bl_read_pct(&on); + long rx = -1, tx = -1; + int ws = wifi_read_state(&rx, &tx); + if (ws == 1 && g_calib_prx >= 0 && rx >= 0 && (rx - g_calib_prx) + (tx - g_calib_ptx) > 4096) ws = 2; + g_calib_prx = rx; g_calib_ptx = tx; + int bs = bt_read_state(); + /* 充电功率(mp2731 实测;放电恒 0 无实测轨) */ + int cmw = pm_charge_mw(&b); + if (g_calib_rows == 0) g_calib_first_pct = b.pct; + g_calib_last_pct = b.pct; + long now = mono_now() - g_calib_start_mono; + fprintf(g_calib_fp, "%ld,%d,%d,%d,%d,%d,%d,%d,%d\n", + now, b.pct, b.mv, b.chg_cur_ua, cmw, bp, cu, ws, bs); + fflush(g_calib_fp); + g_calib_rows++; +} + +/* 停止标定会话:写尾注 + 关文件;返回样本数。 */ +static long calib_stop(void) +{ + if (!g_calib_active) return -1; + if (g_calib_fp) { + fprintf(g_calib_fp, "# end rows=%ld duration_s=%ld cap=%ld->%ld\n", + g_calib_rows, mono_now() - g_calib_start_mono, + g_calib_first_pct, g_calib_last_pct); + fclose(g_calib_fp); + g_calib_fp = NULL; + } + long rows = g_calib_rows; + g_calib_active = 0; + char scene[CALIB_SCENE_MAX]; + snprintf(scene, sizeof scene, "%s", g_calib_scene); + svc_log("calib: stop scene=%s rows=%ld csv=%s", scene, rows, g_calib_csv); + return rows; +} + +/* 守护退出时收尾(SIGTERM/INT → g_stop):若标定未停则落地收尾,防半截/丢缓冲。 */ +static void calib_finalize_on_exit(void) +{ + if (g_calib_active && g_calib_fp) calib_stop(); +} + +/* 标定 JSON(IPC 响应/CLI 用):列出指定目录下的 power_calib_*.csv + 当前会话态。 */ +static void build_calib_json(char *out, size_t cap) +{ + jw_t w = { out, cap, 0 }; + out[0] = 0; + jadd(&w, "{\"kind\":\"calib\",\"active\":%d,", g_calib_active ? 1 : 0); + if (g_calib_active) { + jadd(&w, "\"scene\":\"%s\",\"csv\":\"%s\",\"rows\":%ld,", g_calib_scene, g_calib_csv, g_calib_rows); + } else { + jadd(&w, "\"scene\":null,\"csv\":null,\"rows\":0,"); + } + jadd(&w, "\"files\":["); + DIR *d = opendir(g_log_dir); + int first = 1; + if (d) { + struct dirent *de; + while ((de = readdir(d))) { + if (strncmp(de->d_name, "power_calib_", 12) != 0 || !strstr(de->d_name, ".csv")) continue; + char p[512]; + snprintf(p, sizeof p, "%s/%s", g_log_dir, de->d_name); + struct stat st; + if (stat(p, &st) == 0) { + jadd(&w, "%s\"%s\"", first ? "" : ",", de->d_name); + first = 0; + } + } + closedir(d); + } + jadd(&w, "]}"); +} + /* 期 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) @@ -956,8 +1453,82 @@ static void build_events_json(uint32_t count, char *out, size_t cap) jadd(&w, "]}"); } +/* ---------------- 动作级鉴权(期 3 POLICY_SET/CALIB_*;同 svc_power 模型,0-diff 可移植) ---------------- + * 读级(POLICY_GET/QUERY/HISTORY/EVENTS)任意调用者;动作级(POLICY_SET/CALIB_START/CALIB_STOP) + * 须 root(uid==0) + 系统身份(CALL_SYS 或 os.meow.* 包)。 + * MEOW_PM_AUTH_TEST=1(仅宿主测试)关闭动作级鉴权——生产绝不设置。 */ +typedef enum { CALL_OTHER, CALL_PKG, CALL_SYS } caller_kind_t; +typedef struct { + caller_kind_t kind; + uid_t uid; + char pkg[MEOW_APP_ID_MAX]; +} caller_t; + +static void classify_exe(const char *exe, caller_t *out) +{ + static const char *mp = "/usr/data/mnt/sd_0/MeowAPPs/"; + if (strncmp(exe, mp, strlen(mp)) == 0) { + const char *p = exe + strlen(mp); + if (*p == '/') p++; + const char *end = strchr(p, '/'); + int plen = end ? (int)(end - p) : (int)strlen(p); + if (plen > 0 && plen < MEOW_APP_ID_MAX) { + memcpy(out->pkg, p, (size_t)plen); out->pkg[plen] = 0; + out->kind = CALL_PKG; + return; + } + } + char tmp[MEOW_APP_ID_MAX]; + if (strcmp(exe, "/usr/bin/meow_launcherd") == 0) { + snprintf(tmp, sizeof tmp, "%s", "os.meow.launcher"); + } else if (strcmp(exe, "/usr/data/mnt/sd_0/MeowOS/opt/MeowPanel/meowpanel") == 0) { + snprintf(tmp, sizeof tmp, "%s", "os.meow.panel"); + } else if (strncmp(exe, "/usr/bin/meow_", 14) == 0) { + size_t l = strlen(exe + 11); + if (l >= sizeof tmp) l = sizeof tmp - 1; + memcpy(tmp, exe + 11, l); tmp[l] = 0; + } else if (strncmp(exe, "/usr/data/meow/", 15) == 0) { + snprintf(tmp, sizeof tmp, "%s", "os.meow.core"); + } else { + return; /* CALL_OTHER */ + } + snprintf(out->pkg, sizeof out->pkg, "%s", tmp); + out->kind = CALL_SYS; +} + +static void classify_caller(int fd, caller_t *out) +{ + memset(out, 0, sizeof *out); + out->uid = (uid_t)-1; + struct ucred cred; + socklen_t cl = sizeof cred; + if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &cl) != 0) return; + out->uid = cred.uid; + char procpath[64], exe[256]; + snprintf(procpath, sizeof procpath, "/proc/%d/exe", (int)cred.pid); + ssize_t n = readlink(procpath, exe, sizeof exe - 1); + if (n <= 0) return; + exe[n] = 0; + classify_exe(exe, out); +} + +static int is_sys_identity(const caller_t *c) +{ + if (c->kind == CALL_SYS) return 1; + if (c->kind == CALL_PKG && !strncmp(c->pkg, "os.meow.", 8)) return 1; + return 0; +} + +static int caller_is_action_allowed(const caller_t *c) +{ + if (getenv("MEOW_PM_AUTH_TEST")) return 1; /* 宿主测试专用:关闭动作级鉴权 */ + return (c->uid == 0) && is_sys_identity(c); +} + static void handle_conn(int c) { + caller_t caller; + classify_caller(c, &caller); /* 连接级身份(pid→exe→classify);期 3 动作级鉴权用 */ for (;;) { uint8_t hdr[MEOW_IPC_HDR_LEN]; if (x_read_full(c, hdr, sizeof hdr) < 0) break; @@ -1023,7 +1594,82 @@ static void handle_conn(int c) continue; } - send_err(c, req_id, MEOW_ERR_INVAL, "unknown type (期1 PM_QUERY / 期2 HISTORY/EVENTS)"); + if (type == MSG_PM_POLICY_GET) { + /* 期 3:策略读(读级任意调用者)。回 policy JSON(MSG_PM_RESULT)。 */ + pm_policy_t p; + policy_load(&p); + char json[MEOW_IPC_MAX]; + build_policy_json(&p, json, sizeof json); + send_pm_result(c, MSG_PM_RESULT, req_id, PM_SUBSET_POLICY, json); + continue; + } + + if (type == MSG_PM_POLICY_SET) { + /* 期 3:策略写(动作级=root+系统身份)。req: m_pm_policy_set_t{item,val}。 + * 生效方式:只写 settings.conf power.* 键(原子),powermgr 不直接写系统。 */ + if (!caller_is_action_allowed(&caller)) { + send_err(c, req_id, MEOW_ERR_PERM, "policy set 需 root+系统身份"); + continue; + } + m_pm_policy_set_t s; + memset(&s, 0, sizeof s); + if (rlen > sizeof s) rlen = sizeof s; + memcpy(&s, payload, rlen); + s.item[sizeof s.item - 1] = 0; s.val[sizeof s.val - 1] = 0; + char why[128] = ""; + int rc = policy_set_item(s.item, s.val, why, sizeof why); + pm_policy_t p; + policy_load(&p); + char json[MEOW_IPC_MAX]; + if (rc != 0) { + snprintf(json, sizeof json, "{\"kind\":\"policy\",\"ok\":false,\"why\":\"%s\"}", why); + } else { + build_policy_json(&p, json, sizeof json); + } + send_pm_result(c, MSG_PM_RESULT, req_id, rc == 0 ? PM_SUBSET_POLICY : 0, json); + continue; + } + + if (type == MSG_PM_CALIB_START) { + /* 期 3:标定启动(动作级)。req: 场景名 NUL 串。 */ + if (!caller_is_action_allowed(&caller)) { + send_err(c, req_id, MEOW_ERR_PERM, "calib start 需 root+系统身份"); + continue; + } + char scene[CALIB_SCENE_MAX]; + memset(scene, 0, sizeof scene); + if (rlen > sizeof scene - 1) rlen = sizeof scene - 1; + memcpy(scene, payload, rlen); + if (calib_start(scene) != 0) { + send_err(c, req_id, MEOW_ERR_BUSY, g_calib_active ? "calib 已启动" : "calib 启动失败(只读或打不开csv)"); + continue; + } + char json[MEOW_IPC_MAX]; + build_calib_json(json, sizeof json); + send_pm_result(c, MSG_PM_RESULT, req_id, 0, json); + continue; + } + + if (type == MSG_PM_CALIB_STOP) { + /* 期 3:标定停止(动作级)。req 空。 */ + if (!caller_is_action_allowed(&caller)) { + send_err(c, req_id, MEOW_ERR_PERM, "calib stop 需 root+系统身份"); + continue; + } + if (!g_calib_active) { + send_err(c, req_id, MEOW_ERR_BUSY, "calib 未启动"); + continue; + } + long rows = calib_stop(); + char json[MEOW_IPC_MAX]; + snprintf(json, sizeof json, + "{\"kind\":\"calib\",\"stopped\":true,\"rows\":%ld,\"csv\":\"%s\",\"scene\":\"%s\"}", + rows, g_calib_csv, g_calib_scene); + send_pm_result(c, MSG_PM_RESULT, req_id, 0, json); + continue; + } + + send_err(c, req_id, MEOW_ERR_INVAL, "unknown type (期1 QUERY / 期2 HISTORY/EVENTS / 期3 POLICY/CALIB)"); break; } } @@ -1057,14 +1703,23 @@ static int run_daemon(const char *sock) sock, g_full_mode ? "full" : "readonly", g_sample_sec, access(g_model_conf, R_OK) == 0 ? g_model_conf : "builtin-default(待标定)"); - long next_tick = 0; /* 立即首拍 */ + long next_tick = 0; /* 立即首拍 */ + long next_calib = 0; /* 标定节拍(仅 g_calib_active 时推进) */ while (!g_stop) { long now = mono_now(); if (now >= next_tick) { daemon_tick(); next_tick = now + g_sample_sec; } - int wait_ms = (int)((next_tick - mono_now()) * 1000); + if (g_calib_active && now >= next_calib) { + calib_tick(); + next_calib = now + CALIB_SEC; + } + /* poll 休眠取「下一事件」最近者:采样拍、标定拍(活动时)。上界 g_sample_sec 冗余保护。 */ + long d_tick = next_tick - mono_now(); + long d_calib = g_calib_active ? (next_calib - mono_now()) : d_tick; + long dmin = d_tick < d_calib ? d_tick : d_calib; + int wait_ms = (int)(dmin * 1000); if (wait_ms < 50) wait_ms = 50; if (wait_ms > g_sample_sec * 1000) wait_ms = g_sample_sec * 1000; struct pollfd pfd = { lfd, POLLIN, 0 }; @@ -1074,6 +1729,7 @@ static int run_daemon(const char *sock) if (c >= 0) { handle_conn(c); close(c); } } } + calib_finalize_on_exit(); /* 期 3:守护退出前收尾标定(SIGTERM 等) */ svc_log("meow_powermgr exit"); close(lfd); unlink(sock); @@ -1329,8 +1985,30 @@ static int cli_estimate(void) return 0; } -static int cli_model(void) +static int cli_model(int n, char **v) { + /* 期 3:`model set `——读改写单键 + 原子写(标定后即时改系数)。 */ + if (n >= 1 && !strcmp(v[0], "set")) { + if (n >= 3) { + const char *key = v[1]; + int mw = atoi(v[2]); + if (!model_key_is_known(key)) { + fprintf(stderr, "model set: 未知键 %s(可用键见 model 输出)\n", key); + return 1; + } + if (mw < 0 || mw > 1000000) { fprintf(stderr, "model set: mw 须 0-1000000\n"); return 1; } + if (model_conf_set_key(key, mw) != 0) { + fprintf(stderr, "model set: 写 %s 失败(原子写)\n", g_model_conf); + return 1; + } + pm_model_t m; + model_load(&m); + printf("model set %s=%d(%s)✓\n", key, mw, g_model_conf); + return 0; + } + fprintf(stderr, "usage: meow_power model set \n"); + return 1; + } pm_model_t m; int def = model_load(&m); printf("# meow_powermgr 经验功耗模型(POWER_MANAGER_DESIGN §3.2)\n"); @@ -1354,6 +2032,109 @@ static int cli_model(void) return 0; } +/* 期 3 · policy:show|set。策略数据源 = settings.conf power.* 键;powermgr 只写键不写系统。 */ +static int cli_policy(int n, char **v) +{ + if (n >= 1 && !strcmp(v[0], "show")) { + /* 优先 IPC(守护 view),不可达降级直读 settings.conf(读侧无副作用、口径一致)。 */ + char js[MEOW_IPC_MAX]; + if (pm_ipc_json(MSG_PM_POLICY_GET, NULL, 0, MSG_PM_RESULT, js, sizeof js) != 0) { + pm_policy_t p; + policy_load(&p); + build_policy_json(&p, js, sizeof js); + fprintf(stderr, "note: powermgr 守护不可达,直读 settings.conf(无 daemon 侧差)\n"); + } + char prof[16]; + int low = -1, crit = -1, bcap = -1, scap = -1, enacted = -1; + jstr(js, "profile", prof, sizeof prof); + jint(js, "low_pct", &low); + jint(js, "critical_pct", &crit); + jint(js, "brightness_cap", &bcap); + jint(js, "screen_timeout_cap", &scap); + jint(js, "enacted", &enacted); + printf("policy: profile=%s low_pct=%d critical_pct=%d\n", prof[0] ? prof : "?", low, crit); + printf(" brightness_cap=%d screen_timeout_cap=%d enact=%s\n", bcap, scap, enacted == 1 ? "on" : "off"); + printf("生效方式: powermgr 只写 settings.conf power.* 键,launcher/sysui 期4消费(本服务不直接写系统," + "绝不poke背光/息屏/wlan0/bt)\n"); + printf("默认全关: profile=balanced 无操作=现状; cap=0 不生效; 用户手动开(只能用户手动开)\n"); + if (enacted == 1) + printf("注意: 已开启策略档——但 launcher 未接入前仅持久化,不实际生效(期4接 launcher)\n"); + return 0; + } + if (n >= 2 && !strcmp(v[0], "set")) { + const char *item = v[1], *val = n >= 3 ? v[2] : ""; + char why[128] = ""; + int rc = policy_set_item(item, val, why, sizeof why); + if (rc != 0) { fprintf(stderr, "policy set %s %s: %s\n", item, val, why[0] ? why : "写 settings 失败"); return 1; } + printf("policy set %s=%s ✓(settings.conf power.* 键;launcher 期4消费)\n", item, val); + return 0; + } + fprintf(stderr, "usage: meow_power policy show| set profile <档>|set low-pct |set crit-pct |" + "set bri-cap <0-101>|set scr-cap <秒>\n"); + return 1; +} + +/* 期 3 · calibrate:start|stop|list(用户标定实验;start/stop 经 IPC 驱动守护 2s 采样写 CSV)。 */ +static int cli_calibrate(int n, char **v) +{ + if (n >= 1 && !strcmp(v[0], "start")) { + if (n < 2) { fprintf(stderr, "usage: meow_power calibrate start <场景名>\n"); return 1; } + char js[MEOW_IPC_MAX]; + if (pm_ipc_json(MSG_PM_CALIB_START, v[1], (uint32_t)strlen(v[1]), MSG_PM_RESULT, js, sizeof js) != 0) { + fprintf(stderr, "powermgr 守护不可达:calibrate 需 meow_powermgr 常驻(trigger=boot)\n"); + return 1; + } + if (strstr(js, "\"active\":1") || strstr(js, "\"active\": true")) { + char csv[260] = "", sc[64] = ""; + jstr(js, "csv", csv, sizeof csv); jstr(js, "scene", sc, sizeof sc); + printf("calibrate 已启动: scene=%s csv=%s(2s 节拍采样,实验后 `meow_power calibrate stop` 停止)\n", + sc[0] ? sc : v[1], csv[0] ? csv : "?"); + return 0; + } + fprintf(stderr, "calibrate start 失败: %s\n", strstr(js, "why") ? js : "未知"); + return 1; + } + if (n >= 1 && !strcmp(v[0], "stop")) { + char js[MEOW_IPC_MAX]; + if (pm_ipc_json(MSG_PM_CALIB_STOP, NULL, 0, MSG_PM_RESULT, js, sizeof js) != 0) { + fprintf(stderr, "powermgr 守护不可达:calibrate stop 需 meow_powermgr 常驻\n"); + return 1; + } + long rows = -1; char csv[260] = "", sc[64] = ""; + int rr = 0; + if (jint(js, "rows", &rr) == 0) rows = rr; + jstr(js, "csv", csv, sizeof csv); + jstr(js, "scene", sc, sizeof sc); + if (rows >= 0) + printf("calibrate 已停止: scene=%s rows=%ld csv=%s\n", sc, rows, csv); + else + printf("calibrate 停止: %s\n", strstr(js, "未启动") ? "calib 未启动" : js); + return 0; + } + if (n >= 1 && !strcmp(v[0], "list")) { + DIR *d = opendir(g_log_dir); + int cnt = 0; + printf("calibrate 列表(%s):\n", g_log_dir); + if (!d) { printf(" (日志目录不可读,无卡降级?)\n"); return 0; } + struct dirent *de; + while ((de = readdir(d))) { + if (strncmp(de->d_name, "power_calib_", 12) != 0 || !strstr(de->d_name, ".csv")) continue; + char p[512]; + snprintf(p, sizeof p, "%s/%s", g_log_dir, de->d_name); + struct stat st; + long kb = 0; + if (stat(p, &st) == 0) kb = (long)st.st_size; + printf(" %s (%ldB)\n", de->d_name, kb); + cnt++; + } + closedir(d); + if (cnt == 0) printf(" (无标定记录)\n"); + return 0; + } + fprintf(stderr, "usage: meow_power calibrate start <场景名>| stop| list\n"); + return 1; +} + /* 期 2 · history:最近 N 个采样点 + 全环窗口汇总(不落盘,重启清零) */ static int cli_history(int n, char **v) { @@ -1419,7 +2200,8 @@ static int cli_events(int n, char **v) return 1; } int cnt = 0; jint(js, "count", &cnt); - printf("events: %d 条(内存环形,重启清零;App 前后台切换无可轮询导出点——期2诚实标注不支持)\n", cnt); + printf("events: %d 条(内存环形,重启清零;App 前后台切换——launcher MSG_ACT_FOCUS 为单向 app→launcher 上报," + "powermgr 侧无导出点可接,期 4 接 launcher 扩展)\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; @@ -1499,15 +2281,9 @@ static int cli_top(void) return 0; } -static int cli_stub(const char *what, const char *phase) -{ - printf("meow_power %s: 期 %s 落地,当前未实现(POWER_MANAGER_DESIGN §9 分期)\n", what, phase); - return 1; -} - static const char *cli_usage(void) { - return "status|estimate|model|history[N]|top|events[N]|policy(history/top/events 期2已落地;policy 期3桩)"; + return "status|estimate|model[set ]|history[N]|top|events[N]|policy show|set ...|calibrate start|stop|list"; } static int cli_run(int n, char **v) @@ -1515,11 +2291,12 @@ static int cli_run(int n, char **v) const char *cmd = v[0]; if (!strcmp(cmd, "status")) return cli_status(); if (!strcmp(cmd, "estimate")) return cli_estimate(); - if (!strcmp(cmd, "model")) return cli_model(); + if (!strcmp(cmd, "model")) return cli_model(n - 1, v + 1); if (!strcmp(cmd, "history")) return cli_history(n - 1, v + 1); if (!strcmp(cmd, "top")) return cli_top(); 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_policy(n - 1, v + 1); + if (!strcmp(cmd, "calibrate")) return cli_calibrate(n - 1, v + 1); fprintf(stderr, "usage: meow_power <%s>\n", cli_usage()); return 1; } @@ -1537,6 +2314,8 @@ int main(int argc, char **argv) if ((e = getenv("MEOW_PROC_STAT")) && e[0]) snprintf(g_proc_stat, sizeof g_proc_stat, "%s", e); if ((e = getenv("MEOW_BOOT_MODE")) && e[0]) snprintf(g_boot_mode, sizeof g_boot_mode, "%s", e); if ((e = getenv("MEOW_PM_MODEL")) && e[0]) snprintf(g_model_conf, sizeof g_model_conf, "%s", e); + if ((e = getenv("MEOW_SETTINGS_FILE")) && e[0]) snprintf(g_settings, sizeof g_settings, "%s", e); + if ((e = getenv("MEOW_LOG_DIR")) && e[0]) snprintf(g_log_dir, sizeof g_log_dir, "%s", e); if ((e = getenv("MEOW_AUDIO_SOCK")) && e[0]) snprintf(g_audio_sock, sizeof g_audio_sock, "%s", e); if ((e = getenv("MEOW_PM_SOCK")) && e[0]) g_sock = e; if ((e = getenv("MEOW_PM_SAMPLE_SEC")) && atoi(e) > 0) g_sample_sec = atoi(e); diff --git a/patch_v2/meow_powermgr/meow_powermgr_host b/patch_v2/meow_powermgr/meow_powermgr_host index d9054e2..42eb4e4 100755 Binary files a/patch_v2/meow_powermgr/meow_powermgr_host and b/patch_v2/meow_powermgr/meow_powermgr_host differ diff --git a/patch_v2/meow_powermgr/test_powermgr.sh b/patch_v2/meow_powermgr/test_powermgr.sh index 2cd90d6..812f49a 100755 --- a/patch_v2/meow_powermgr/test_powermgr.sh +++ b/patch_v2/meow_powermgr/test_powermgr.sh @@ -42,10 +42,15 @@ EOF echo "cpu 100 0 100 800 0 0 0 0 0 0" > "$T/proc_stat" echo meow > "$T/boot_mode" +mkdir -p "$T/logs" # 标定 CSV 落点(MEOW_LOG_DIR) +cat > "$T/settings.conf" <<'SEOF' +# meow-settings 1 (测试桩:policy power.* 键落点) +SEOF 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_PM_MODEL=$T/model.conf MEOW_AUDIO_SOCK=$T/no_audio.sock MEOW_PM_SOCK=$T/pm.sock - MEOW_PM_NO_HCICONFIG=1" + MEOW_PM_MODEL=$T/model.conf MEOW_SETTINGS_FILE=$T/settings.conf MEOW_LOG_DIR=$T/logs + MEOW_AUDIO_SOCK=$T/no_audio.sock MEOW_PM_SOCK=$T/pm.sock + MEOW_PM_NO_HCICONFIG=1 MEOW_PM_AUTH_TEST=1" ln -sf "$(readlink -f "$BIN")" "$T/meow_power" # argv[0] 双形态 # ---- T1: 守护直读(无守护 → 降级直读) ---- @@ -143,8 +148,65 @@ 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) -ck T9e "期 3" "$OUT" +# ---- T9e: 期 3 policy show(默认全关:profile=balanced / cap=0 / enact=off)---- +OUT=$(env $ENVS $Q "$T/meow_power" policy show 2>&1) +ck T9e "profile=balanced low_pct=15 critical_pct=5" "$OUT" +ck T9e2 "brightness_cap=0 screen_timeout_cap=0 enact=off" "$OUT" +ck T9e3 "默认全关" "$OUT" + +# ---- T9f: 期 3 policy set profile powersave(写入 settings.conf;enact=on)---- +OUT=$(env $ENVS $Q "$T/meow_power" policy set profile powersave 2>&1) +ck T9f "policy set profile=powersave" "$OUT" +OUT=$(env $ENVS $Q "$T/meow_power" policy show 2>&1) +ck T9f2 "profile=powersave" "$OUT" +ck T9f3 "enact=on" "$OUT" +grep -q "power.profile=powersave" "$T/settings.conf" && { PASS=$((PASS+1)); echo "PASS T9f4 settings 键"; } || { FAIL=$((FAIL+1)); echo "FAIL T9f4: $(cat "$T/settings.conf")"; } + +# ---- T9g: 期 3 低电阈值读 settings.conf(power.low_pct=20 生效于事件判定)---- +env $ENVS $Q "$T/meow_power" policy set low-pct 20 >/dev/null 2>&1 +echo 19 > "$PS/battery/capacity" # 从 93 跳到 19 < 20 → 低电事件(阈值 20%) +sleep 4 # 等守护节拍采样 +OUT=$(env $ENVS $Q "$T/meow_power" policy show 2>&1) +ck T9g "low_pct=20" "$OUT" +OUT=$(env $ENVS $Q "$T/meow_power" events 2>&1) +ck T9g2 "阈值 20%" "$OUT" +echo 93 > "$PS/battery/capacity" # 恢复 +env $ENVS $Q "$T/meow_power" policy set low-pct 15 >/dev/null 2>&1 +env $ENVS $Q "$T/meow_power" policy set profile balanced >/dev/null 2>&1 + +# ---- T9h: 期 3 policy set bri-cap(0-101 越界拒绝)---- +OUT=$(env $ENVS $Q "$T/meow_power" policy set bri-cap 88 2>&1) +ck T9h "policy set bri-cap=88" "$OUT" +OUT=$(env $ENVS $Q "$T/meow_power" policy set bri-cap 255 2>&1) +ck T9h2 "0-101" "$OUT" +env $ENVS $Q "$T/meow_power" policy set bri-cap 0 >/dev/null 2>&1 + +# ---- T9i: 期 3 标定工具 calibrate start/stop/list 出 CSV ---- +OUT=$(env $ENVS $Q "$T/meow_power" calibrate start 纯亮屏 2>&1) +ck T9i "calibrate 已启动" "$OUT" +ck T9i2 "power_calib_" "$OUT" +sleep 5 # 等 2s×2 拍 +OUT=$(env $ENVS $Q "$T/meow_power" calibrate stop 2>&1) +ck T9i3 "calibrate 已停止" "$OUT" +ck T9i4 "rows=" "$OUT" +CSV=$(ls "$T/logs"/power_calib_*.csv 2>/dev/null | head -1) +if [ -n "$CSV" ]; then PASS=$((PASS+1)); echo "PASS T9i5 csv exists ($CSV)"; else FAIL=$((FAIL+1)); echo "FAIL T9i5 CSV 未生成"; fi +[ -n "$CSV" ] && grep -q "ts,cap_pct,vol_mv,current_ua,charge_mw,brightness_pct,cpu_util_pct,wifi_state,bt_state" "$CSV" && { PASS=$((PASS+1)); echo "PASS T9i6 csv header"; } || { FAIL=$((FAIL+1)); echo "FAIL T9i6 csv header"; } +[ -n "$CSV" ] && grep -q "scene=纯亮屏" "$CSV" && { PASS=$((PASS+1)); echo "PASS T9i7 csv scene"; } || { FAIL=$((FAIL+1)); echo "FAIL T9i7 csv scene"; } +OUT=$(env $ENVS $Q "$T/meow_power" calibrate list 2>&1) +ck T9i8 "power_calib_" "$OUT" + +# ---- T9j: 期 3 模型单键 setter model set (守护每拍重载生效)---- +OUT=$(env $ENVS $Q "$T/meow_power" model set baseline_mw 999 2>&1) +ck T9j "model set baseline_mw=999" "$OUT" +OUT=$(env $ENVS $Q "$T/meow_power" model 2>/dev/null) +ck T9j2 "baseline_mw=999" "$OUT" +sleep 2 # 等守护重载 model +OUT=$(env $ENVS $Q "$T/meow_power" estimate 2>/dev/null) +ck T9j3 "baseline=999" "$OUT" # 守护侧 estimate 用新系数 +OUT=$(env $ENVS $Q "$T/meow_power" model set bad_key 5 2>&1) +ck T9j4 "未知键" "$OUT" +env $ENVS $Q "$T/meow_power" model set baseline_mw 300 >/dev/null 2>&1 # ---- T10: meow_powermgr 名直调子命令也走 CLI ---- OUT=$(env $ENVS $Q "$BIN" model 2>/dev/null)