- 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。
2344 lines
105 KiB
C
2344 lines
105 KiB
C
#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);
|
||
}
|