Files
MeowOS-For-R3P2/patch_v2/meow_powermgr/meow_powermgr.c
T
AskaEth 5663acfbb9 feat(powermgr): 期3 policy 生效 + 标定工具 + 模型单键 setter + events 说明
- policy:IPC 0xD2/0xD3 GET/SET(SET 动作级鉴权=root+系统身份,SO_PEERCRED→pid→exe→classify,同 svc_power)
  + 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 不生效,
  只能用户手动开)。生效方式: powermgr 不直接写系统——只写 settings.conf power.* 键(原子),
  launcher/sysui 期4 消费; 本服务绝不 poke 背光/息屏/wlan0/bt。低电/极低阈值改从 settings.conf
  读, 事件检测即时生效。
- calibrate:`meow_power calibrate start <场景>|stop|list`——守护内 2s 节拍采样 电流(mp2731
  current_now)+亮度+CPU+wifi/bt 状态+电量%变化,写 TF MeowOS/logs/power_calib_<epoch>.csv
  (无卡降级 /tmp,逐行 fflush),用户实验中 start/stop 两命令即可。
- model:`meow_power model set <key> <mw>` 读改写单键+原子写(tmp+rename),守护每拍重载 conf。
- events:App 前后台切换——launcher MSG_ACT_FOCUS 为单向 app→launcher 上报,powermgr 侧无导出点
  可接,诚实标注「期4 接 launcher」扩展,本期不编造该事件(延续期2 口径)。
- 验证:宿主+qemu 全量 76/76 PASS(期1/2 全过+新增 policy/calibrate/model-set 用例),mipsel
  静态交叉 0 warning,二进制 151516B(<200KB 阈值)。md5 1ef2f8afc07acc7aa09dee9f7149aba8。
2026-09-02 16:25:51 +08:00

2344 lines
105 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#define _GNU_SOURCE /* struct ucred / SO_PEERCRED、asprintf 等 GNU/musl 扩展 */
/* meow_powermgr.c — MeowOS 功耗管理专项服务(devdocs/plan/POWER_MANAGER_DESIGN.md,期 2)。
*
* 定位(三服务职责划分,§1):svc_power=一次性系统动作;svc_perf=性能只读聚合;
* **powermgr=功耗**——电池深加工(state/cap/mV/续航估算)、经验功耗模型估算与分解。
* 期 1 范围(§9):svcd 托管骨架 + IPC(MSG_PM_QUERY/RESULT) + CLI status/estimate/model +
* 权威充电链复用 + 经验模型。
* 期 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)。
* 期 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_<epoch>.csv(无卡降级 /tmp);用户实验只需在真机跑 start/stop。
* ③ 模型 conf 单键 setter:`meow_power model set <key> <mw>`(读改写单键 + 原子写 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)→
* IPC 服务常驻:bind /usr/data/meow_pm.sock(--sock/MEOW_PM_SOCK 覆盖),
* MSG_HELLO→MSG_ACK、MSG_PM_QUERY(0x00D0)→MSG_PM_RESULT(0x00D1, JSON)。
* - `meow_power <cmd>`(argv[0] 双形态)或 `meow_powermgr <cmd>` → 一次性 CLI(root 免鉴权)。
* CLI 优先走 IPC(拿守护进程的容量差分速率),守护不可达降级为直读 sysfs(rate=null)。
*
* HibyOS 只读降级(用户定案 2026-09-01 §附 1):trigger=boot + 进程内读 /tmp/meow_boot_mode:
* "meow"=全功能;否则(hiby/缺失)=只采样电池,绝不施策略、不碰 wlan0/bt/声卡(§2 门控语义,
* 与 audiod 的 trigger=manual+exit(0) 不同:本服务读侧无害故常驻只读)。每采样拍重判,可热切换。
*
* 权威充电链(0-diff 照抄 svc_power charging_status_str,真机实测 2026-09-01):
* mp2731-charger/status(权威)→ fusb302-typec-source/online → battery/status+ac/online 兜底。
*
* 硬件事实(devdocs/plan/POWER_HW_FACTS.md,期 0 实测):
* - battery/ 无 current_now/charge_full/cycle_count/temp → 实时功率不可 measured,
* 续航=容量差分 derived + 功耗=经验模型 estimated(confidence 如实标注,不得呈现为实测);
* - 背光量程 0-101(max_brightness 实测,非 255);fb0/blank 不可读 → 息屏=actual_brightness==0;
* - 无 cpufreq(省电档无降频控制点);无 thermal zones;rfkill 存在(期 3 候选控制点)。
*
* 经验模型系数:板载 /usr/data/meow/power_model.conf(key=value,缺失用内置默认);
* **全部系数为经验初值,待真机标定**(§7 项 7/8 标定实验未做)。
*
* 编译(mipsel 静态):build_powermgr.sh;宿主:build_powermgr.sh host。
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <poll.h>
#include <time.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/stat.h>
#include <sys/wait.h>
#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_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_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 */
static char g_net_dir[128] = "/sys/class/net"; /* MEOW_NET_DIR */
static char g_rfkill_dir[128] = "/sys/class/rfkill"; /* MEOW_RFKILL_DIR */
static char g_bt_dir[128] = "/sys/class/bluetooth"; /* MEOW_BT_DIR */
static char g_netdev[160] = "/proc/net/dev"; /* MEOW_NETDEV */
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_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_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 覆盖,测试用) */
#define RATE_MIN_SPAN_DEF 120 /* 容量差分速率的最小时间窗(秒;MEOW_PM_MIN_SPAN_SEC 覆盖) */
#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_rate_min_span = RATE_MIN_SPAN_DEF;
/* ---------------- 极简日志(stderr;svcd supervisor 捕获) ---------------- */
static void svc_log(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
fprintf(stderr, "[meow_powermgr] ");
vfprintf(stderr, fmt, ap);
fprintf(stderr, "\n");
va_end(ap);
}
/* ---------------- 文件小工具(同 svc_power) ---------------- */
static int read_file_txt(const char *path, char *buf, int cap)
{
int fd = open(path, O_RDONLY);
if (fd < 0) return -1;
int n = 0;
while (n < cap - 1) {
ssize_t r = read(fd, buf + n, (size_t)(cap - 1 - n));
if (r <= 0) break;
n += (int)r;
}
close(fd);
buf[n] = 0;
while (n > 0 && (buf[n - 1] == '\n' || buf[n - 1] == '\r')) buf[--n] = 0;
return n;
}
static int read_file_int_def(const char *path, int def)
{
char buf[32];
if (read_file_txt(path, buf, sizeof buf) <= 0) return def;
char *p = buf;
while (*p == ' ' || *p == '\t') p++;
return atoi(p);
}
/* 拼 power_supply 节点路径:ps_path(b, cap, "battery/capacity") */
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。 */
static void charging_status_str(char *st, int cap)
{
char p[192];
st[0] = 0;
ps_path(p, sizeof p, "mp2731-charger/status");
if (read_file_txt(p, st, cap) > 0 && st[0]) return; /* 权威源直用 */
ps_path(p, sizeof p, "fusb302-typec-source/online");
int on = read_file_int_def(p, -1);
if (on >= 0) { snprintf(st, cap, "%s", on == 1 ? "Charging" : "Discharging"); return; }
ps_path(p, sizeof p, "battery/status");
read_file_txt(p, st, cap);
if (!strstr(st, "Charging") && !strstr(st, "Full")) {
ps_path(p, sizeof p, "ac/online");
if (read_file_int_def(p, 0) == 1) snprintf(st, cap, "Charging");
}
}
/* ---------------- 电池深加工(POWER_MANAGER_DESIGN §3.1 + POWER_HW_FACTS 项 1) ---------------- */
typedef struct {
int pct; /* -1=未知 */
int mv; /* -1=未知(voltage_now µV→mV) */
char state[32]; /* Charging/Discharging/Full(权威链) */
int charging; /* state 含 Charging/Full */
char health[16]; /* battery/health 字符串(如 Good);""=无 → null */
char tech[24]; /* battery/technology;""=无 → null */
int temp_c10; /* battery/temp(本机无 → -1 → null) */
int charger_temp_c10; /* mp2731-charger/temp(-1=无);÷10=℃ */
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;
static void batt_read(batt_t *b)
{
char p[192];
memset(b, 0, sizeof *b);
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");
b->pct = read_file_int_def(p, -1);
ps_path(p, sizeof p, "battery/voltage_now");
int uv = read_file_int_def(p, -1);
if (uv >= 0) b->mv = uv / 1000;
charging_status_str(b->state, sizeof b->state);
b->charging = (strstr(b->state, "Charging") || strstr(b->state, "Full")) ? 1 : 0;
ps_path(p, sizeof p, "battery/health");
read_file_txt(p, b->health, sizeof b->health);
ps_path(p, sizeof p, "battery/technology");
read_file_txt(p, b->tech, sizeof b->tech);
ps_path(p, sizeof p, "battery/temp");
b->temp_c10 = read_file_int_def(p, -1);
ps_path(p, sizeof p, "mp2731-charger/temp");
b->charger_temp_c10 = read_file_int_def(p, -1);
ps_path(p, sizeof p, "battery/cycle_count");
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;系数全部经验初值,待真机标定) ---------------- */
typedef struct {
int nominal_mah; /* 标称容量 mAh(默认 1600:axp energy_full_design 旁证,待标定) */
int bl_low_mw; /* 背光低档(亮度 1-33%) */
int bl_mid_mw; /* 背光中档(34-66%) */
int bl_high_mw; /* 背光高档(67-100%) */
int panel_on_mw; /* 面板亮(背光>0 时附加) */
int wifi_unassoc_mw; /* WiFi up 未关联(v1 状态机无此档判据,预留) */
int wifi_idle_mw; /* WiFi 关联空闲 */
int wifi_tx_mw; /* WiFi 传输中(netdev 增量判) */
int bt_up_mw; /* BT up 空闲 */
int bt_conn_mw; /* BT 有连接(连接数判据期 2,预留) */
int audio_idle_mw; /* 音频链无流(FPGA 静态) */
int audio_play_mw; /* 音频链播放(DAC+FPGA) */
int cpu_full_mw; /* CPU 满载功耗(util%×此项) */
int baseline_mw; /* 基线(SoC/DDR/PMIC 静态) */
} pm_model_t;
static void model_defaults(pm_model_t *m)
{
/* 经验初值,全部待真机标定(POWER_HW_FACTS §1 项 7/8:标定实验未做)。 */
m->nominal_mah = 1600;
m->bl_low_mw = 150;
m->bl_mid_mw = 450;
m->bl_high_mw = 900;
m->panel_on_mw = 80;
m->wifi_unassoc_mw = 80;
m->wifi_idle_mw = 200;
m->wifi_tx_mw = 600;
m->bt_up_mw = 60;
m->bt_conn_mw = 200;
m->audio_idle_mw = 50;
m->audio_play_mw = 250;
m->cpu_full_mw = 400;
m->baseline_mw = 300;
}
typedef struct { const char *key; int *ptr; } model_ent_t;
/* 读模型 conf(key=value,# 注释;缺失键留默认)。返回 0=用了 conf,1=conf 缺失(纯内置默认)。 */
static int model_load(pm_model_t *m)
{
model_defaults(m);
model_ent_t ents[] = {
{ "nominal_mah", &m->nominal_mah },
{ "bl_low_mw", &m->bl_low_mw },
{ "bl_mid_mw", &m->bl_mid_mw },
{ "bl_high_mw", &m->bl_high_mw },
{ "panel_on_mw", &m->panel_on_mw },
{ "wifi_unassoc_mw", &m->wifi_unassoc_mw },
{ "wifi_idle_mw", &m->wifi_idle_mw },
{ "wifi_tx_mw", &m->wifi_tx_mw },
{ "bt_up_mw", &m->bt_up_mw },
{ "bt_conn_mw", &m->bt_conn_mw },
{ "audio_idle_mw", &m->audio_idle_mw },
{ "audio_play_mw", &m->audio_play_mw },
{ "cpu_full_mw", &m->cpu_full_mw },
{ "baseline_mw", &m->baseline_mw },
};
FILE *f = fopen(g_model_conf, "r");
if (!f) return 1;
char line[160];
while (fgets(line, sizeof line, f)) {
char *p = line;
while (*p == ' ' || *p == '\t') p++;
if (*p == '#' || *p == '\n' || !*p) continue;
char *eq = strchr(p, '=');
if (!eq) continue;
*eq = 0;
char *kend = eq;
while (kend > p && (kend[-1] == ' ' || kend[-1] == '\t')) *--kend = 0;
int v = atoi(eq + 1);
for (size_t i = 0; i < sizeof ents / sizeof ents[0]; i++)
if (!strcmp(p, ents[i].key)) { *ents[i].ptr = v; break; }
}
fclose(f);
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 {
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 long mono_now(void)
{
struct timespec tp;
clock_gettime(CLOCK_MONOTONIC, &tp);
return tp.tv_sec;
}
static void ring_push(const batt_t *b, int est_mw, int screen, int bri)
{
if (b->pct < 0) return;
hist_sample_t s;
s.mono = mono_now();
s.pct = b->pct;
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;
else { memmove(g_ring, g_ring + 1, sizeof g_ring - sizeof g_ring[0]); g_ring[RING_N - 1] = s; }
}
/* 容量差分平均速率:rate_x100 = (Δpct×100)/min,放电为负、充电为正。
* 用环内最旧↔最新同向(同 charging 态)样本对;跨度 < g_rate_min_span 或 |Δpct|<1 → 不可得。
* 返回 1=有速率。 */
static int ring_rate(int *rate_x100)
{
if (g_rcount < 2) return 0;
const hist_sample_t *nw = &g_ring[g_rcount - 1];
const hist_sample_t *ol = NULL;
for (int i = 0; i < g_rcount - 1; i++)
if (g_ring[i].charging == nw->charging) { ol = &g_ring[i]; break; }
if (!ol) return 0;
long span = nw->mono - ol->mono;
int dpct = nw->pct - ol->pct;
if (span < g_rate_min_span || dpct == 0) return 0;
*rate_x100 = (int)((long)dpct * 100 * 60 / span);
if (*rate_x100 == 0) return 0;
return 1;
}
/* ---------------- 子系统状态读取(经验模型输入;全部只读) ---------------- */
/* 背光/面板:actual_brightness 归一 0-100%(量程读 max_brightness,本机 0-101,HW_FACTS 项 6)。
* 返回亮度%(-1=未知);*on = 面板亮(亮度>0;fb0/blank 不可读故以此代理息屏判定)。 */
static int bl_read_pct(int *on)
{
char p[192];
snprintf(p, sizeof p, "%s/max_brightness", g_bl_dir);
int mx = read_file_int_def(p, -1);
if (mx <= 0) { *on = -1; return -1; }
snprintf(p, sizeof p, "%s/actual_brightness", g_bl_dir);
int v = read_file_int_def(p, -1);
if (v < 0) {
snprintf(p, sizeof p, "%s/brightness", g_bl_dir);
v = read_file_int_def(p, -1);
}
if (v < 0) { *on = -1; return -1; }
if (v > mx) v = mx;
*on = v > 0 ? 1 : 0;
return v * 100 / mx;
}
/* 有界子进程命令(fork+pipe+poll+超时 SIGKILL,绝不阻塞采样拍;netgr 同源性)。
* 返回读到字节数(含尾 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)
{
char p[192];
snprintf(p, sizeof p, "%s/wlan0/operstate", g_net_dir);
char os[32] = "";
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");
if (!f) { if (rx_out) *rx_out = -1; if (tx_out) *tx_out = -1; return carrier == 0 ? 3 : 1; }
char line[256];
long rx = -1, tx = -1;
while (fgets(line, sizeof line, f)) {
char *c = strchr(line, ':');
if (!c) continue;
*c = 0;
char *n = line;
while (*n == ' ' || *n == '\t') n++;
if (strcmp(n, "wlan0") != 0) continue;
rx = strtol(c + 1, NULL, 10);
char *save = NULL;
char *w = strtok_r(c + 1, " \t", &save);
for (int i = 0; w && i < 8; i++) w = strtok_r(NULL, " \t", &save);
if (w) tx = strtol(w, NULL, 10);
break;
}
fclose(f);
if (rx_out) *rx_out = rx;
if (tx_out) *tx_out = tx;
if (carrier == 0) return 3;
return 1; /* 传送档由调用方按增量判 2 */
}
/* BT:rfkill soft 块→off;hci0 存在且未被块→up;连接数档(期 2 细化见下)。 */
static int bt_read_state(void)
{
char p[448]; /* rfkill_dir + "/" + d_name(≤255) + "/type|/soft" 充裕 */
DIR *d = opendir(g_rfkill_dir);
int blocked = 0, seen = 0;
if (d) {
struct dirent *de;
while ((de = readdir(d))) {
if (strncmp(de->d_name, "rfkill", 6) != 0) continue;
char t[32] = "";
snprintf(p, sizeof p, "%s/%s/type", g_rfkill_dir, de->d_name);
if (read_file_txt(p, t, sizeof t) <= 0 || strcmp(t, "bluetooth") != 0) continue;
seen = 1;
snprintf(p, sizeof p, "%s/%s/soft", g_rfkill_dir, de->d_name);
if (read_file_int_def(p, 0) == 1) blocked = 1;
}
closedir(d);
}
snprintf(p, sizeof p, "%s/hci0", g_bt_dir);
int hci0 = access(p, F_OK) == 0;
if (blocked) return 0;
if (!hci0 && !seen) return 0; /* 无任何 BT 踪迹 → off */
if (!hci0) return 0;
/* 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。
* -1=audiod 不可达(unknown→0mW) 0=无流 1=播放中。Hiby 只读模式绝不调用(门控红线)。
* 注意:audiod dispatch 无 MSG_HELLO 分支(查询无需握手),直接发 QUERY。 */
static int x_write_full(int fd, const uint8_t *buf, size_t n);
static int x_read_full(int fd, uint8_t *buf, size_t n);
static int audio_query_active(void)
{
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) return -1;
struct timeval tv = { 1, 0 };
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_audio_sock, sizeof a.sun_path - 1);
if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return -1; }
/* 直接 QUERY → STATE(无 HELLO 握手,见上注) */
uint8_t hdr[MEOW_IPC_HDR_LEN];
uint32_t magic = MEOW_IPC_MAGIC, zero = 0, rid = 1;
uint16_t ver = MEOW_IPC_VERSION, t = MSG_AUDIO_QUERY;
memcpy(hdr + 0, &magic, 4); memcpy(hdr + 4, &ver, 2); memcpy(hdr + 6, &t, 2);
memcpy(hdr + 8, &rid, 4); memcpy(hdr + 12, &zero, 4);
if (x_write_full(fd, hdr, sizeof hdr) < 0) { close(fd); return -1; }
if (x_read_full(fd, hdr, sizeof hdr) < 0) { close(fd); return -1; }
uint32_t rlen;
uint16_t rt;
memcpy(&rt, hdr + 6, 2);
memcpy(&rlen, hdr + 12, 4);
if (rt != MSG_AUDIO_STATE || rlen > MEOW_IPC_MAX) { close(fd); return -1; }
uint8_t pl[MEOW_IPC_MAX];
if (rlen && x_read_full(fd, pl, rlen) < 0) { close(fd); return -1; }
close(fd);
pl[rlen < MEOW_IPC_MAX ? rlen : MEOW_IPC_MAX - 1] = 0;
return strstr((const char *)pl, "active=1") ? 1 : 0;
}
/* CPU util%(/proc/stat 差分;首次/失败 → -1) */
static int cpu_util_pct(long prev_total, long prev_idle, long *out_total, long *out_idle)
{
FILE *f = fopen(g_proc_stat, "r");
if (!f) return -1;
char line[256];
if (!fgets(line, sizeof line, f)) { fclose(f); return -1; }
fclose(f);
if (strncmp(line, "cpu ", 4) != 0) return -1;
long long v[8] = {0};
int n = sscanf(line + 4, "%lld %lld %lld %lld %lld %lld %lld %lld",
&v[0], &v[1], &v[2], &v[3], &v[4], &v[5], &v[6], &v[7]);
if (n < 4) return -1;
long idle = (long)(v[3] + v[4]); /* idle + iowait */
long total = 0;
for (int i = 0; i < 8; i++) total += (long)v[i];
*out_total = total; *out_idle = idle;
if (prev_total < 0 || total <= prev_total) return -1;
long dt = total - prev_total, di = idle - prev_idle;
if (dt <= 0) return -1;
int u = (int)((dt - di) * 100 / dt);
return u < 0 ? 0 : (u > 100 ? 100 : u);
}
/* ---------------- 估算结果 ---------------- */
typedef struct {
int total_mw;
int baseline_mw, cpu_mw, bl_mw, panel_mw, wifi_mw, bt_mw, audio_mw;
int cpu_util; /* -1=未知 */
int brightness_pct; /* -1=未知 */
int screen_on; /* 0=off 1=on -1=未知(actual_brightness>0 代理,期 2 历史用) */
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 degraded; /* 1=Hiby 只读模式:不碰 wlan0/bt/声卡,分量全 0 */
} est_t;
/* 经验模型合成。full=0(Hiby 只读模式)→ 分量全 0 + degraded 标记(不碰 wlan0/bt/声卡)。
* prev_rx/prev_tx 非 NULL 时:作 netdev 增量基准(>4096B/拍→tx 档)并回存新计数(守护用);
* NULL(CLI 降级直读)→ 无基准,up 即 idle。 */
static void est_compute(const pm_model_t *m, int full, int cpu_util,
long *prev_rx, long *prev_tx, est_t *e)
{
memset(e, 0, sizeof *e);
e->cpu_util = -1; e->brightness_pct = -1; e->audio_state = -1; e->screen_on = -1;
if (!full) { e->degraded = 1; return; }
e->baseline_mw = m->baseline_mw;
/* 屏幕 */
int on = -1;
int bp = bl_read_pct(&on);
e->brightness_pct = bp;
e->screen_on = on;
if (on == 1) {
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);
}
/* WiFi(0=off 1=idle 2=tx 3=unassoc;tx 仅对已关联档判) */
long rx = -1, tx = -1;
e->wifi_state = wifi_read_state(&rx, &tx);
if (e->wifi_state == 1) {
if (prev_rx && *prev_rx >= 0 && rx >= 0 &&
(rx - *prev_rx) + (tx - *prev_tx) > 4096)
e->wifi_state = 2;
}
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; }
/* BT(0=off 1=up 2=connected) */
e->bt_state = bt_read_state();
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_mw = e->audio_state == 1 ? m->audio_play_mw
: (e->audio_state == 0 ? m->audio_idle_mw : 0);
/* CPU */
e->cpu_util = cpu_util;
if (cpu_util >= 0) e->cpu_mw = cpu_util * m->cpu_full_mw / 100;
e->total_mw = e->baseline_mw + e->cpu_mw + e->bl_mw + e->panel_mw
+ e->wifi_mw + e->bt_mw + e->audio_mw;
}
/* ---------------- JSON 构建(vsnprintf 助手法,截断分析免疫) ---------------- */
typedef struct { char *buf; size_t cap, off; } jw_t;
static void jadd(jw_t *w, const char *fmt, ...)
{
if (w->off >= w->cap - 1) return;
va_list ap;
va_start(ap, fmt);
int n = vsnprintf(w->buf + w->off, w->cap - w->off, fmt, ap);
va_end(ap);
if (n > 0) {
w->off += (size_t)n;
if (w->off >= w->cap) w->off = w->cap - 1;
}
}
static void jadd_int_or_null(jw_t *w, const char *key, int v)
{
if (v < 0) jadd(w, "\"%s\":null,", key);
else jadd(w, "\"%s\":%d,", key, v);
}
static void jadd_str_or_null(jw_t *w, const char *key, const char *s)
{
if (!s || !s[0]) jadd(w, "\"%s\":null,", key);
else jadd(w, "\"%s\":\"%s\",", key, s);
}
/* 续航/速率段(battery 子集内)。rate_x100 不可得 → 三字段全 null。 */
static void jadd_rate(jw_t *w, const batt_t *b, int have_rate, int rate_x100)
{
if (!have_rate || b->pct < 0) {
jadd(w, "\"rate_pct_min_x100\":null,\"est_minutes_left\":null,"
"\"est_minutes_full\":null,\"rate_confidence\":null,");
return;
}
jadd(w, "\"rate_pct_min_x100\":%d,", rate_x100);
if (rate_x100 < 0) { /* 放电 → 剩余分钟 */
jadd(w, "\"est_minutes_left\":%d,\"est_minutes_full\":null,",
b->pct * 100 / -rate_x100);
} else { /* 充电 → 充满分钟 */
jadd(w, "\"est_minutes_left\":null,\"est_minutes_full\":%d,",
(100 - b->pct) * 100 / rate_x100);
}
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 降级直读共用。
* have_rate/cpu_util/prev_rx/prev_tx 由调用方提供(守护=环/差分态;直读=瞬测)。 */
static void build_json(uint32_t subset, int full, const batt_t *b,
int have_rate, int rate_x100, const est_t *e,
char *out, size_t cap)
{
if (subset == 0) subset = PM_SUBSET_BATTERY | PM_SUBSET_ESTIMATE
| PM_SUBSET_BREAKDOWN | PM_SUBSET_POLICY;
jw_t w = { out, cap, 0 };
out[0] = 0;
jadd(&w, "{\"mode\":\"%s\",\"ts\":%ld,", full ? "meow" : "readonly", (long)time(NULL));
if (subset & PM_SUBSET_BATTERY) {
jadd(&w, "\"battery\":{");
jadd_str_or_null(&w, "state", b->state);
jadd_int_or_null(&w, "pct", b->pct);
jadd_int_or_null(&w, "mv", b->mv);
jadd_str_or_null(&w, "health", b->health);
jadd_str_or_null(&w, "technology", b->tech);
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, "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(&w, "\"nominal_mah_note\":\"nominal_mah 为模型系数(默认1600,待标定)\"}");
}
if (subset & PM_SUBSET_ESTIMATE) {
jadd(&w, ",\"estimate\":{");
if (e->degraded) {
jadd(&w, "\"est_mw\":null,\"confidence\":\"none\",\"degraded\":true,"
"\"charge_mw\":null,\"charge_confidence\":null}");
} else {
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) {
jadd(&w, ",\"breakdown\":{");
if (e->degraded) {
jadd(&w, "\"degraded\":true}");
} else {
jadd(&w, "\"baseline_mw\":%d,", e->baseline_mw);
jadd(&w, "\"cpu_mw\":%d,", e->cpu_mw);
jadd_int_or_null(&w, "cpu_util_pct", e->cpu_util);
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(&w, "\"wifi_mw\":%d,\"wifi_state\":\"%s\",", e->wifi_mw,
e->wifi_state == 2 ? "tx"
: (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,
e->audio_state == 1 ? "playing" : (e->audio_state == 0 ? "idle" : "unknown"));
jadd(&w, "}");
}
}
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)
{
size_t off = 0;
while (off < n) {
ssize_t r = read(fd, buf + off, n - off);
if (r <= 0) return -1;
off += (size_t)r;
}
return 0;
}
static int x_write_full(int fd, const uint8_t *buf, size_t n)
{
size_t off = 0;
while (off < n) {
ssize_t w = write(fd, buf + off, n - off);
if (w <= 0) return -1;
off += (size_t)w;
}
return 0;
}
static int send_frame(int fd, uint16_t type, uint32_t req_id, const void *payload, uint32_t len)
{
uint8_t hdr[MEOW_IPC_HDR_LEN];
uint32_t magic = MEOW_IPC_MAGIC, rlen = len;
uint16_t ver = MEOW_IPC_VERSION, t = type;
memcpy(hdr + 0, &magic, 4);
memcpy(hdr + 4, &ver, 2);
memcpy(hdr + 6, &t, 2);
memcpy(hdr + 8, &req_id, 4);
memcpy(hdr + 12, &rlen, 4);
if (x_write_full(fd, hdr, MEOW_IPC_HDR_LEN) < 0) return -1;
if (len && payload && x_write_full(fd, payload, len) < 0) return -1;
return 0;
}
static int send_ack(int fd, uint32_t req_id, uint32_t code, uint32_t status)
{
m_ack_t a;
memset(&a, 0, sizeof a);
a.code = code;
a.status = status;
return send_frame(fd, MSG_ACK, req_id, &a, sizeof a);
}
static int send_err(int fd, uint32_t req_id, uint32_t code, const char *msg)
{
m_err_t e;
memset(&e, 0, sizeof e);
e.code = code;
uint16_t ml = msg ? (uint16_t)strlen(msg) : 0;
if (ml > 63) ml = 63;
e.msg_len = ml;
if (ml) memcpy(e.msg, msg, ml);
return send_frame(fd, MSG_ERR, req_id, &e, sizeof e);
}
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;
memset(&r, 0, sizeof r);
r.code = MEOW_OK;
r.subset = subset;
size_t jl = strlen(json);
if (jl > sizeof r.json - 1) jl = sizeof r.json - 1;
r.json_len = (uint16_t)jl;
memcpy(r.json, json, jl);
r.json[jl] = 0;
return send_frame(fd, rtype, req_id, &r, (uint32_t)(12 + jl + 1));
}
/* ---------------- 守护进程态 ---------------- */
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_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;
/* 上一拍状态(事件检测用;-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_<epoch>.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] = "";
if (read_file_txt(g_boot_mode, buf, sizeof buf) <= 0) return 0; /* 缺失=只读等落地 */
return strcmp(buf, "meow") == 0;
}
/* ---------------- 电源事件流(守护进程;内存环形,重启清零,不落盘) ----------------
* 可检测:① 充电器插拔(charging 位翻转)② 低电阈值穿越(低 15%/极低 5%/回到 20% 恢复,§5.2 默认)
* ③ 屏幕亮灭 ④ 背光跳变(≥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 {
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)
{
/* 期 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);
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 >= 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 < rec && b->pct >= rec)
ev_push(1, 2, b->pct, "电量回到 %d%%(≥%d%% 恢复)", b->pct, 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)
{
int fm = boot_mode_is_meow();
if (fm != g_full_mode) {
svc_log("mode -> %s (%s %s)", fm ? "full(meow)" : "readonly",
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;
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;
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);
}
/* ---------------- 标定(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 <scene> 经 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)
{
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, "]}");
}
/* ---------------- 动作级鉴权(期 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;
uint32_t magic, rlen, req_id;
uint16_t ver, type;
memcpy(&magic, hdr, 4);
memcpy(&ver, hdr + 4, 2);
memcpy(&type, hdr + 6, 2);
memcpy(&req_id, hdr + 8, 4);
memcpy(&rlen, hdr + 12, 4);
if (magic != MEOW_IPC_MAGIC || ver != MEOW_IPC_VERSION) {
send_err(c, req_id, MEOW_ERR_INVAL, "bad magic/ver"); break;
}
if (rlen > MEOW_IPC_MAX) {
send_err(c, req_id, MEOW_ERR_INVAL, "payload too big"); break;
}
uint8_t payload[MEOW_IPC_MAX];
if (rlen && x_read_full(c, payload, rlen) < 0) break;
if (type == MSG_HELLO) { send_ack(c, req_id, MEOW_OK, 0); continue; }
if (type == MSG_PM_QUERY) {
/* 读级:任意调用者放行(§6.1 鉴权分级:QUERY=读级;POLICY_SET 期 3 才接动作级)。
* 查询不推进采样环(速率由节拍样本积累,防查询刷屏污染时间窗);电池现读保证新鲜。 */
m_pm_query_t q;
memset(&q, 0, sizeof q);
if (rlen > sizeof q) rlen = sizeof q;
memcpy(&q, payload, rlen);
batt_t b;
batt_read(&b);
int rate_x100 = 0;
int have_rate = ring_rate(&rate_x100);
long t = -1, i = 0;
int cpu_util = cpu_util_pct(g_cpu_prev_total, g_cpu_prev_idle, &t, &i);
if (t >= 0) { g_cpu_prev_total = t; g_cpu_prev_idle = i; }
est_t e;
est_compute(&g_model, g_full_mode, cpu_util,
&g_wifi_prev_rx, &g_wifi_prev_tx, &e);
char json[MEOW_IPC_MAX];
build_json(q.subset, g_full_mode, &b, have_rate, rate_x100, &e,
json, sizeof json);
send_pm_result(c, MSG_PM_RESULT, req_id, q.subset, json);
continue;
}
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;
}
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;
}
}
static volatile sig_atomic_t g_stop = 0;
static void on_sig(int sig) { (void)sig; g_stop = 1; }
static int run_daemon(const char *sock)
{
signal(SIGPIPE, SIG_IGN);
signal(SIGTERM, on_sig);
signal(SIGINT, on_sig);
int lfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (lfd < 0) { svc_log("socket fail: %s", strerror(errno)); return 1; }
struct sockaddr_un a;
memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX;
strncpy(a.sun_path, sock, sizeof a.sun_path - 1);
unlink(sock);
if (bind(lfd, (struct sockaddr *)&a, sizeof a) < 0) {
svc_log("bind %s fail: %s", sock, strerror(errno));
close(lfd);
return 1;
}
chmod(sock, 0666);
if (listen(lfd, 8) < 0) { svc_log("listen fail: %s", strerror(errno)); close(lfd); return 1; }
model_load(&g_model);
g_full_mode = boot_mode_is_meow();
svc_log("meow_powermgr listening on %s (mode=%s, sample=%ds, model=%s)",
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_calib = 0; /* 标定节拍(仅 g_calib_active 时推进) */
while (!g_stop) {
long now = mono_now();
if (now >= next_tick) {
daemon_tick();
next_tick = now + g_sample_sec;
}
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 };
int pr = poll(&pfd, 1, wait_ms);
if (pr > 0 && (pfd.revents & POLLIN)) {
int c = accept(lfd, NULL, NULL);
if (c >= 0) { handle_conn(c); close(c); }
}
}
calib_finalize_on_exit(); /* 期 3:守护退出前收尾标定(SIGTERM 等) */
svc_log("meow_powermgr exit");
close(lfd);
unlink(sock);
return 0;
}
/* ---------------- CLI:极简 JSON 字段提取(自产 JSON 固定形状,不做通用解析) ---------------- */
static const char *jfind(const char *js, const char *key)
{
char pat[48];
snprintf(pat, sizeof pat, "\"%s\":", key);
const char *p = strstr(js, pat);
return p ? p + strlen(pat) : NULL;
}
/* 0=取到整数;1=字段为 null/缺失(*out 不动) */
static int jint(const char *js, const char *key, int *out)
{
const char *p = jfind(js, key);
if (!p || strncmp(p, "null", 4) == 0) return 1;
*out = atoi(p);
return 0;
}
static void jstr(const char *js, const char *key, char *out, size_t cap)
{
const char *p = jfind(js, key);
if (!p || strncmp(p, "null", 4) == 0 || *p != '"') { out[0] = 0; return; }
p++;
size_t n = 0;
while (p[n] && p[n] != '"' && n < cap - 1) { out[n] = p[n]; n++; }
out[n] = 0;
}
/* CLI 查询:优先 IPC(守护有容量差分速率);守护不可达 → 直读 sysfs 降级(rate=null,
* cpu_util 用 200ms 双采样瞬测,wifi 无增量基准则 idle)。返回 0=来自守护,1=降级直读。 */
static int pm_query_json(uint32_t subset, char *out, size_t cap)
{
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd >= 0) {
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);
if (connect(fd, (struct sockaddr *)&a, sizeof a) == 0) {
if (send_frame(fd, MSG_HELLO, 1, NULL, 0) == 0) {
uint8_t hdr[MEOW_IPC_HDR_LEN];
uint8_t pl[sizeof(m_pm_result_t)];
if (x_read_full(fd, hdr, sizeof hdr) == 0) {
uint32_t rl;
memcpy(&rl, hdr + 12, 4);
if (rl > sizeof pl) rl = sizeof pl;
if (rl && x_read_full(fd, pl, rl) < 0) { close(fd); goto direct; }
m_pm_query_t q;
memset(&q, 0, sizeof q);
q.subset = subset;
if (send_frame(fd, MSG_PM_QUERY, 2, &q, sizeof q) == 0 &&
x_read_full(fd, hdr, sizeof hdr) == 0) {
uint16_t rt;
memcpy(&rt, hdr + 6, 2);
memcpy(&rl, hdr + 12, 4);
if (rt == MSG_PM_RESULT && rl >= 12 && rl <= sizeof pl &&
x_read_full(fd, pl, rl) == 0) {
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;
close(fd);
return 0;
}
}
}
}
}
close(fd);
}
direct:
{
batt_t b;
batt_read(&b);
pm_model_t m;
model_load(&m);
long t1 = -1, i1 = 0, t2, i2;
cpu_util_pct(-1, 0, &t1, &i1);
usleep(200 * 1000);
int cu = cpu_util_pct(t1, i1, &t2, &i2);
int full = boot_mode_is_meow();
est_t e;
est_compute(&m, full, cu, NULL, NULL, &e);
build_json(subset, full, &b, 0, 0, &e, out, cap);
return 1;
}
}
/* 期 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 子命令 ---------------- */
static void fmt_min(char *out, size_t cap, int min)
{
if (min >= 120) snprintf(out, cap, "%dh%02dm", min / 60, min % 60);
else snprintf(out, cap, "%dmin", min);
}
static int cli_status(void)
{
char js[MEOW_IPC_MAX];
int via = pm_query_json(PM_SUBSET_BATTERY, js, sizeof js);
char state[32], health[16], tech[24];
int pct = -1, mv = -1, rate = 0, el = -1, ef = -1, ctemp = -1, cycle = -1, temp = -1;
jstr(js, "state", state, sizeof state);
jstr(js, "health", health, sizeof health);
jstr(js, "technology", tech, sizeof tech);
jint(js, "pct", &pct);
jint(js, "mv", &mv);
jint(js, "temp_c10", &temp);
jint(js, "charger_temp_c10", &ctemp);
jint(js, "cycle_count", &cycle);
int have_rate = jint(js, "rate_pct_min_x100", &rate) == 0;
jint(js, "est_minutes_left", &el);
jint(js, "est_minutes_full", &ef);
char mode[16];
jstr(js, "mode", mode, sizeof mode);
printf("state=%s cap=%d%%", state[0] ? state : "?", pct);
if (mv >= 0) printf(" mv=%d", mv);
printf(" health=%s", health[0] ? health : "null");
printf(" tech=%s", tech[0] ? tech : "null");
if (temp >= 0) printf(" temp=%d.%dC", temp / 10, temp % 10);
else printf(" temp=null");
if (ctemp >= 0) printf(" charger_temp=%d.%dC", ctemp / 10, ctemp % 10);
if (cycle >= 0) printf(" cycle=%d", cycle);
if (have_rate) {
printf(" rate=%s%d.%02d%%/min", rate < 0 ? "-" : "+",
rate < 0 ? -rate / 100 : rate / 100,
rate < 0 ? -rate % 100 : rate % 100);
char tb[24];
if (el >= 0) { fmt_min(tb, sizeof tb, el); printf(" est_left=%s", tb); }
else printf(" est_left=null");
if (ef >= 0) { fmt_min(tb, sizeof tb, ef); printf(" est_full=%s", tb); }
else printf(" est_full=null");
printf(" conf=derived");
} else {
printf(" rate=null est_left=null est_full=null");
}
printf(" mode=%s src=%s\n", mode[0] ? mode : "?", via == 0 ? "daemon" : "direct");
if (via != 0)
fprintf(stderr, "note: powermgr 守护不可达,降级直读(无容量差分速率 → rate=null)\n");
return 0;
}
static int cli_estimate(void)
{
char js[MEOW_IPC_MAX];
int via = pm_query_json(PM_SUBSET_ESTIMATE | PM_SUBSET_BREAKDOWN, js, sizeof js);
if (strstr(js, "\"degraded\":true")) {
printf("degraded: HibyOS 只读模式——只采样电池,不施功耗估算(不碰 wlan0/bt/声卡)\n");
return 0;
}
int total = -1, base = 0, cpu = 0, cu = -1, bl = 0, panel = 0, bp = -1, wifi = 0, bt = 0, audio = 0;
char conf[24], ws[12], bs[12], as[12];
jint(js, "est_mw", &total);
jstr(js, "confidence", conf, sizeof conf);
jint(js, "baseline_mw", &base);
jint(js, "cpu_mw", &cpu);
jint(js, "cpu_util_pct", &cu);
jint(js, "backlight_mw", &bl);
jint(js, "panel_mw", &panel);
jint(js, "brightness_pct", &bp);
jint(js, "wifi_mw", &wifi);
jint(js, "bt_mw", &bt);
jint(js, "audio_mw", &audio);
jstr(js, "wifi_state", ws, sizeof ws);
jstr(js, "bt_state", bs, sizeof bs);
jstr(js, "audio_state", as, sizeof as);
printf("est_mw=%d confidence=%s calibrated=false (系数全部经验初值,待真机标定)\n",
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];
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");
printf("baseline=%d cpu=%d(util=%s) backlight=%d panel=%d(bri=%s) wifi=%d(%s) bt=%d(%s) audio=%d(%s)\n",
base, cpu, cu_s, bl, panel, bp_s,
wifi, ws[0] ? ws : "?", bt, bs[0] ? bs : "?", audio, as[0] ? as : "?");
if (via != 0)
fprintf(stderr, "note: powermgr 守护不可达,降级直读(cpu_util=200ms 瞬测,wifi 无增量判 tx)\n");
return 0;
}
static int cli_model(int n, char **v)
{
/* 期 3:`model set <key> <mw>`——读改写单键 + 原子写(标定后即时改系数)。 */
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 <key> <mw>\n");
return 1;
}
pm_model_t m;
int def = model_load(&m);
printf("# meow_powermgr 经验功耗模型(POWER_MANAGER_DESIGN §3.2)\n");
printf("# 系数来源: %s\n", g_model_conf);
printf("# %s\n", def ? "⚠ conf 缺失 → 以上为内置默认(待标定)" : "conf 读取成功");
printf("# ⚠ 全部系数为经验初值,待真机标定(POWER_HW_FACTS §1 项 7/8 标定实验未做)\n");
printf("nominal_mah=%d\n", m.nominal_mah);
printf("baseline_mw=%d\n", m.baseline_mw);
printf("bl_low_mw=%d\n", m.bl_low_mw);
printf("bl_mid_mw=%d\n", m.bl_mid_mw);
printf("bl_high_mw=%d\n", m.bl_high_mw);
printf("panel_on_mw=%d\n", m.panel_on_mw);
printf("wifi_unassoc_mw=%d\n", m.wifi_unassoc_mw);
printf("wifi_idle_mw=%d\n", m.wifi_idle_mw);
printf("wifi_tx_mw=%d\n", m.wifi_tx_mw);
printf("bt_up_mw=%d\n", m.bt_up_mw);
printf("bt_conn_mw=%d\n", m.bt_conn_mw);
printf("audio_idle_mw=%d\n", m.audio_idle_mw);
printf("audio_play_mw=%d\n", m.audio_play_mw);
printf("cpu_full_mw=%d\n", m.cpu_full_mw);
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 <n>|set crit-pct <n>|"
"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)
{
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 前后台切换——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;
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 const char *cli_usage(void)
{
return "status|estimate|model[set <key> <mw>]|history[N]|top|events[N]|policy show|set ...|calibrate start|stop|list";
}
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(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_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;
}
/* ---------------- main:单一二进制双形态 ---------------- */
int main(int argc, char **argv)
{
const char *e;
if ((e = getenv("MEOW_PS_DIR")) && e[0]) snprintf(g_ps_dir, sizeof g_ps_dir, "%s", e);
if ((e = getenv("MEOW_BL_DIR")) && e[0]) snprintf(g_bl_dir, sizeof g_bl_dir, "%s", e);
if ((e = getenv("MEOW_NET_DIR")) && e[0]) snprintf(g_net_dir, sizeof g_net_dir, "%s", e);
if ((e = getenv("MEOW_RFKILL_DIR")) && e[0]) snprintf(g_rfkill_dir, sizeof g_rfkill_dir, "%s", e);
if ((e = getenv("MEOW_BT_DIR")) && e[0]) snprintf(g_bt_dir, sizeof g_bt_dir, "%s", e);
if ((e = getenv("MEOW_NETDEV")) && e[0]) snprintf(g_netdev, sizeof g_netdev, "%s", e);
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);
if ((e = getenv("MEOW_PM_MIN_SPAN_SEC")) && atoi(e) > 0) g_rate_min_span = atoi(e);
int i = 1;
for (; i < argc; i++) {
if (!strcmp(argv[i], "--sock") && i + 1 < argc) g_sock = argv[++i];
else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) {
printf("meow_powermgr / meow_power (%s)\n", cli_usage());
return 0;
}
else break;
}
/* argv[0] basename=meow_power → 强制 CLI(双形态);meow_powermgr 空参 → 守护常驻。 */
const char *base = strrchr(argv[0], '/');
base = base ? base + 1 : argv[0];
if (!strcmp(base, "meow_power")) {
if (i >= argc) { fprintf(stderr, "usage: meow_power <%s>\n", cli_usage()); return 1; }
return cli_run(argc - i, argv + i);
}
if (i >= argc) return run_daemon(g_sock); /* svcd 空参 → IPC 常驻 */
return cli_run(argc - i, argv + i);
}