diff --git a/devdocs/plan/POWER_HW_FACTS.md b/devdocs/plan/POWER_HW_FACTS.md new file mode 100644 index 0000000..829ecfd --- /dev/null +++ b/devdocs/plan/POWER_HW_FACTS.md @@ -0,0 +1,101 @@ +# POWER_HW_FACTS — meow_powermgr 期 0 真机只读调研结论 + +> 调研时间:2026-09-01 15:43(设备 10.78.193.50,uptime 1:48,`/tmp/meow_boot_mode`=meow,固件 0.7.89+)。 +> 方式:全部只读(ls/cat/sysfs 读),未写任何节点、未碰进程、未重启。 +> 对照基准:`meow_svc_power battery` = `battery=94%(Discharging) 4275mV`。 + +## 0. 两个决定形态的关键结论(必读) + +| 问题 | 结论 | 影响 | +|---|---|---| +| `battery/current_now` 存在吗? | **不存在**。battery/ 全集无 current_now/charge_now/charge_full/charge_full_design/cycle_count/temp | 实时功率**不可能 measured**;走「容量差分 derived」+「经验模型 estimated」双轨(设计 §3.3 第三档)。健康度(charge_full/design)**不可算**,只能透传 `health` 字符串 | +| `cpufreq` 存在吗? | **不存在**(`/sys/devices/system/cpu/cpu0/cpufreq/` No such file;单核 cpu0) | 省电档**无降频控制点**;CPU 功耗项只能按 util×满载系数估算,无可控项(设计 §5.1 CPU 行降级为「仅观测」) | + +## 1. §7 清单逐项结论 + +### 项 1:battery/ 全集 +``` +capacity=94 health=Good present=1 status=Discharging(会撒谎,勿用) +technology=Li-ion voltage_now=4275000(µV) time_to_empty_now=8191 +``` +- `time_to_empty_now=8191`(=0x1FFF)为**恒定哨兵值**,不随负载变化 → 不可用,勿报。 +- 无 temp 节点 → 电池温度报 null(充电器温度见项 2 可替代参考)。 + +### 项 2:mp2731-charger/(权威充电态 + 充电功率可算) +``` +status=Discharging(权威源,设计 §3.1 已实测) online=0 temp=300(=30.0°C) +current_now=0(µA,放电时恒 0;充电时=充电电流) constant_charge_current=0/_max=4520000 +constant_charge_voltage=4200000/_max=4670000 charge_term_current=120000 +input_current_now=100000 input_current_limited=2000000 input_suspend=0 +input_voltage_settled=2280000 charge_control_limit=3000000 charge_full=0(无效) +real_type=USB typec_power_role=sink step_charging_enabled=1 +``` +- **充电功率可估**:`constant_charge_voltage × current_now / 1e6`(充电中 current_now 非 0,待插电实测确认非 0 时刻的值域)。 +- `charge_type` 节点不存在(清单原问的 charge_type 无)。 + +### 项 3:fusb302 / ac / usb / axp_battery +- `fusb302-typec-source/`:online✓(插线检测兜底)、voltage_now、current_max(PD 档位相关,本次=0)。 +- `ac/`:online=0;voltage_now=4600/current_now=1200 为**静态默认值**(online=0 时不变),仅 online 位可用。 +- `usb/`:online=0;current_now=780 静态默认。仅 online/present 位可用。 +- **`axp_battery/`:幻影设备(不可用)**——capacity=0、voltage_now=0、current_now=200000 **三次采样(间隔 5s)恒定不变** → 非实时电量计,是 AXP 遗留驱动的死数据。唯一有用信息:`energy_full_design=1600` + `voltage_max_design=4350000/min=3400000`——可作**标称容量参考**(1600mAh 级,与 R3 Pro II 官方 1600mAh 电池吻合;诚实标注:单位/口径未经证实,仅作续航估算的默认标称容量,可经 power_model.conf `nominal_mah` 覆盖)。 + +### 项 4:cpufreq —— **不存在**(见 §0) + +### 项 5:rfkill —— **存在且可用** +``` +rfkill0: type=bluetooth state=1 soft=0 hard=0 +rfkill1: type=wlan state=1 soft=0 hard=0 +rfkill2: type=wlan state=1 soft=0 hard=0 +rfkill3: type=bluetooth state=1 soft=0 hard=0 +``` +- 4 个 rfkill(2×wlan + 2×bluetooth,两个同类型并存——WiFi/BT combo 芯片双接口特征)。 +- **软关断控制点存在**(写 soft=1,本次未写只读 state)→ 期 3 策略层可用「rfkill soft block」替代 `ip link down`/`hciconfig down`(更省电)。**红线不变:策略层不直接 poke sysfs,需经 svc_power 类动作通道或新增授权写点,期 3 定案。** + +### 项 6:背光节点全集 +``` +max_brightness=101 brightness=0 actual_brightness=0 bl_power=0 type=raw +``` +- **⚠️ 量程是 0-101,不是设计 §3.2 假定的 0-255** → 功耗表背光分档按 0-101 归一(亮度%=brightness/101×100 再分档)。POWER_MANAGER_DESIGN §3.2 表「0-255」需修正为「0-101(R3 Pro II 实测)」。 +- 调查时刻 brightness=0(设备息屏中)→ **息屏态可用 actual_brightness==0 判定**(fb0/blank 读取为空串——驱动未实现 read,不可用)。 + +### 项 7/8:CPU 满载/背光/WiFi 标定实验 +- 本期 0 未执行:容量差分法需分钟级窗口 + 逐项对照(亮屏定档/stress 满载/WiFi 传输),且设备无 current_now 真值可借 → **留待用户在场配合的标定窗口**(每次实验 ≥10min)。 +- 方法已备:capacity 差分(94%→x%)÷ Δt × nominal_mah × 平均电压 = 平均功率;基线=息屏 WiFi 关联空闲。 +- 在标定完成前,一切 mW 数字均为**占位经验值**(设计 §10 诚实标注不变)。 + +### 项 9:WiFi 状态/计数器 +- `wpa_cli status` ✓:`wpa_state=COMPLETED`、bssid/freq/ssid/ip_address 全有(关联态可判)。 +- `/proc/net/dev` wlan0 收发计数器 ✓(bytes/packets 递增)→ 传输态由增量判,可行。 +- wpa_cli 无直接 scan 计数;扫网风暴可用 `wpa_cli scan_results` 时间戳或 wpa 日志——期 2 再定,标记**待验**。 + +### 项 10:蓝牙 +- `hciconfig hci0` ✓(Type: Primary Bus: UART,当前 DOWN,RX/TX 计数器有)。 +- `/sys/class/bluetooth/hci0/` ✓ → up/down + 连接数可判(连接数走 `hciconfig -a` 或 /sys 下 device 枚举,期 2 细化)。 + +### 项 11:audiod MSG_AUDIO_STATE +- `meow_audio status` 实测:`active=0 app=- sid=0 rate=44100 ch=2 fmt=- written=... played=... underrun=1 dropped=0 stall=0 txdrop=0 paused=0 route_mode=auto port=4 vol=44`。 +- **字段完备**:active(流活跃)/route/port/vol 全有 → 音频链功耗项输入够用(无流≈静态 / 播放≈DAC+FPGA 档)。 + +### 项 12:thermal zones —— **不存在** +- `/sys/class/thermal/` 目录本身不存在;`/sys/class/hwmon/` 也不存在。 +- 唯一温度源:`mp2731-charger/temp`=300(30.0°C,充电器芯片温度,非电池/SoC)。 +- → 「异常发热」辅助信号**无热源可用**,期 2 异常检测规则不含温度维度(或仅用充电器温度粗参考)。 + +## 2. 对设计文档的回填修正 + +1. **§3.2 背光量程 0-255 → 0-101**(max_brightness 实测)。 +2. **§3.3 实时功率**:current_now 路径(measured)**整层不可达**——v1 只有 derived(容量差分)+ estimated(经验模型)。UI 必须带 confidence 标注(设计已要求,此处确认强制执行)。 +3. **§3.1 健康度**:charge_full/design 不存在 → health 输出透传 `health` 字符串(Good),不输出百分比。 +4. **§3.1 续航估算**:以容量差分平均速率(%/min)为准;nominal_mah 默认 1600(axp energy_full_design 旁证,待标定)。 +5. **§5.1 CPU 行**:无 cpufreq → 「降频」行为项删除,CPU 仅观测。 +6. **§5.1 WiFi/BT 行**:rfkill soft block 可用作控制点候选(期 3 定案,v1 不动)。 +7. **息屏判定**:fb0/blank 不可读 → 用 `backlight_pwm0/actual_brightness==0` 代理(期 1 采纳)。 +8. **充电功率**:mp2731 `current_now`(µA) + `constant_charge_voltage`(µV) 可估充电功率(插电态待实测值域)。 + +## 3. 遗留 / 待验 + +- [ ] 插电态复测 mp2731 current_now/constant_charge_current 非零值域(充电功率估算校准)。 +- [ ] 标定实验(项 7/8):需用户在场,≥10min/项 × 约 5 项。 +- [ ] axp energy_full_design=1600 的单位口径(mAh? mWh?)——与官方 1600mAh 规格吻合但未经驱动源码证实。 +- [ ] wpa 扫网频率计数法(期 2)。 +- [ ] 时间校准对照:无 current_now,derived 功率的时间分辨率受 capacity 1% 粒度限制(1600mAh×1%=16mAh,~5-15min 才跳 1%)→ 续航估算 v1 精度受限,UI 需标注。 diff --git a/patch_v2/meow_launcherd/meow_ipc.h b/patch_v2/meow_launcherd/meow_ipc.h index 817a0b8..b4963ed 100644 --- a/patch_v2/meow_launcherd/meow_ipc.h +++ b/patch_v2/meow_launcherd/meow_ipc.h @@ -92,6 +92,15 @@ enum { MSG_AUDIO_STATE = 0x00C8, /* ↓ 状态快照文本(NUL 结尾;当前流/参数/帧数/underrun/路由/音量) */ MSG_AUDIO_ROUTE = 0x00C9, /* ↑ 手动路由(u32 port:0=auto 2=3.5 3=4.4;调试/面板用) */ MSG_AUDIO_VOLUME = 0x00CA, /* ↑ 音量(i32 pct:-1=查询 0-100=设置;v1 透传写 DAC numid=1/2) */ + /* ---- meow_powermgr 功耗管理服务(devdocs/plan/POWER_MANAGER_DESIGN.md §6.1):0x00D0-0x00D7 ---- + * 语义:功耗维度的查看/分析/管理(svc_power 管动作、svc_perf 管性能,powermgr 管功耗)。 + * 独立 socket /usr/data/meow_pm.sock(--sock/MEOW_PM_SOCK 覆盖)。鉴权分级同 svc_power: + * QUERY/HISTORY/EVENTS=读级任意调用者;POLICY_SET=动作级(期 3 落地时接 SO_PEERCRED 同源代码)。 */ + MSG_PM_QUERY = 0x00D0, /* ↑ 功耗聚合查询(m_pm_query_t{subset}:PM_SUBSET_* 位;0=全部) */ + MSG_PM_RESULT = 0x00D1, /* ↓ 聚合 JSON(电池深加工/功耗估算/分解/策略,形状对齐 §3/§4.1) */ + /* 0x00D2 MSG_PM_POLICY_GET / 0x00D3 MSG_PM_POLICY_SET —— 期 3(profile/低电阈值读改,SET 动作级鉴权) + * 0x00D4 MSG_PM_HISTORY(u32 hours)/ 0x00D5 MSG_PM_HISTORY_RESULT(统计 JSON)—— 期 2 + * 0x00D6 MSG_PM_EVENTS / 0x00D7 MSG_PM_EVENTS_RESULT(异常耗电事件清单)—— 期 2 */ MSG_SYS = 0x00F0 /* ↓ 系统事件(守护关闭/服务死亡/播放器状态) */ }; @@ -341,6 +350,28 @@ typedef struct m_power_status { char msg[96]; /* 结果/状态文本 */ } m_power_status_t; +/* ---- meow_powermgr 功耗管理(POWER_MANAGER_DESIGN §6.1;服务端 meow_powermgr)---- */ + +/* MSG_PM_QUERY 子集位(0=全部) */ +#define PM_SUBSET_BATTERY (1u << 0) /* 电池深加工:state/pct/mv/health/续航估算 */ +#define PM_SUBSET_ESTIMATE (1u << 1) /* 当前功耗估算:est_mw + confidence(estimated/derived) */ +#define PM_SUBSET_BREAKDOWN (1u << 2) /* 功耗分解:各子系统分量(§3.2 经验模型,系数待标定) */ +#define PM_SUBSET_POLICY (1u << 3) /* 策略档位快照(期 3;期 1 恒 null) */ + +/* MSG_PM_QUERY ↑:功耗聚合查询 */ +typedef struct m_pm_query { + uint32_t subset; /* 0=全部;PM_SUBSET_* 组合 */ +} m_pm_query_t; /* 4 B */ + +/* MSG_PM_RESULT ↓:聚合 JSON(payload 为 NUL 结尾 JSON 文本,同 m_perf_result_t 口径) */ +typedef struct m_pm_result { + uint32_t code; /* MEOW_OK / MEOW_ERR_* */ + uint32_t subset; /* 回显请求 subset */ + uint16_t json_len; /* json 有效字节(不含 NUL) */ + uint16_t pad; + char json[MEOW_IPC_MAX - 12]; /* 聚合 JSON(§3/§4.1 形状) */ +} m_pm_result_t; + /* ---- amsd 控制(AMSD_DESIGN §4.2)---- */ /* MSG_AMS_START →:请求 launcher 拉起某 App。app_id=包名(权威);activity 空=裸启动 diff --git a/patch_v2/meow_powermgr/build_powermgr.sh b/patch_v2/meow_powermgr/build_powermgr.sh new file mode 100755 index 0000000..ef6d46e --- /dev/null +++ b/patch_v2/meow_powermgr/build_powermgr.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# build_powermgr.sh — meow_powermgr 交叉编译(mipsel 静态)+ 宿主编译可选。 +# +# 目标:R3 Pro II / MeowOS(mipsel-linux-muslsf,静态)。powermgr 仅 POSIX + Linux 基接口 +# (复用 meow_ipc.h 帧格式,无 launcher 依赖),单文件满足「固件内、不入卡」(<200KB 阈值)。 +# 单一二进制双形态:/usr/bin/meow_powermgr(svcd 空参=守护)+ /usr/bin/meow_power(CLI,同一份 +# 文件两次安装,argv[0] basename 分形态;fw_content.conf 两行同 src)。 +# +# 用法: +# ./build_powermgr.sh # 交叉编译 → patch_v2/meow_powermgr/meow_powermgr(mipsel 静态) +# ./build_powermgr.sh host # 宿主编译 → patch_v2/meow_powermgr/meow_powermgr_host +set -e +cd "$(dirname "$0")" +ROOT="$HOME/Project/R3ProII" +CC="$ROOT/.tools/zig-mipsel-cc" +I_HDR="$ROOT/patch_v2/meow_launcherd" # meow_ipc.h(帧/MSG_PM_*/PM_SUBSET_*/m_pm_*) + +if [ "$1" = "host" ]; then + HOST_CC="${CC_HOST:-cc}" + "$HOST_CC" -Os -Wall -Wextra -I"$I_HDR" meow_powermgr.c -o meow_powermgr_host + echo "== host build ==" + ls -la meow_powermgr_host + file meow_powermgr_host + exit 0 +fi + +"$CC" -Os -Wall -Wextra -static -I"$I_HDR" meow_powermgr.c -o meow_powermgr +echo "== meow_powermgr (mipsel static) ==" +ls -la meow_powermgr +md5sum meow_powermgr +file meow_powermgr diff --git a/patch_v2/meow_powermgr/meow_powermgr b/patch_v2/meow_powermgr/meow_powermgr new file mode 100755 index 0000000..696459b Binary files /dev/null and b/patch_v2/meow_powermgr/meow_powermgr differ diff --git a/patch_v2/meow_powermgr/meow_powermgr.c b/patch_v2/meow_powermgr/meow_powermgr.c new file mode 100644 index 0000000..663e598 --- /dev/null +++ b/patch_v2/meow_powermgr/meow_powermgr.c @@ -0,0 +1,1071 @@ +#define _GNU_SOURCE /* struct ucred / SO_PEERCRED、asprintf 等 GNU/musl 扩展 */ + +/* meow_powermgr.c — MeowOS 功耗管理专项服务(devdocs/plan/POWER_MANAGER_DESIGN.md,期 1 骨架)。 + * + * 定位(三服务职责划分,§1):svc_power=一次性系统动作;svc_perf=性能只读聚合; + * **powermgr=功耗**——电池深加工(state/cap/mV/续航估算)、经验功耗模型估算与分解。 + * 期 1 范围(§9):svcd 托管骨架 + IPC(MSG_PM_QUERY/RESULT) + CLI status/estimate/model + + * 权威充电链复用 + 经验模型;history/top/events/policy 为期 2/3 桩。 + * + * 单一二进制双形态(同 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 `(argv[0] 双形态)或 `meow_powermgr ` → 一次性 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "meow_ipc.h" /* 帧格式/消息号 + MSG_PM 系列 + PM_SUBSET_* + m_pm_*(号段 0x00D0-0x00D7) */ + +/* ---------------- 路径(全部可经 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/power_model.conf"; /* MEOW_PM_MODEL */ +static char g_audio_sock[128] = "/usr/data/meow_audio.sock"; /* MEOW_AUDIO_SOCK */ +static const char *g_sock = "/usr/data/meow_pm.sock"; /* MEOW_PM_SOCK/--sock */ + +#define SAMPLE_SEC_DEF 30 /* 电池采样/状态刷新节拍(秒;MEOW_PM_SAMPLE_SEC 覆盖,测试用) */ +#define RATE_MIN_SPAN_DEF 120 /* 容量差分速率的最小时间窗(秒;MEOW_PM_MIN_SPAN_SEC 覆盖) */ +#define RING_N 240 /* 采样环容量(30s×240=2h) */ + +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); +} + +/* ---------------- 权威充电链(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) */ +} 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; + 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); +} + +/* ---------------- 经验功耗模型(§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; +} + +/* ---------------- 采样环(守护进程;容量差分速率 → 续航估算,§3.1/§3.3 derived) ---------------- */ +typedef struct { long mono; int pct; int charging; } sample_t; +static 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) +{ + if (b->pct < 0) return; + sample_t s; + s.mono = mono_now(); + s.pct = b->pct; + s.charging = b->charging; + 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 sample_t *nw = &g_ring[g_rcount - 1]; + const 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; +} + +/* WiFi:0=off(接口不存在/operstate 非 up)1=idle 2=tx(netdev 增量>阈值)。 + * 关联/未关联档 v1 无判据(要 wpa_cli,期 2 再细化)——up 即按 idle 起算,诚实标注。 */ +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; + FILE *f = fopen(g_netdev, "r"); + if (!f) return 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 *tk = c + 1; + for (int i = 0; i < 8; i++) { tk = strchr(tk, ' '); if (!tk) break; while (*tk == ' ') tk++; } + /* 第 9 列 = tx bytes */ + char *save = NULL; + char *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; + return 1; +} + +/* 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; + return 1; /* up 空闲档(连接判定期 2) */ +} + +/* 音频链: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 wifi_state; /* 0/1/2 */ + int bt_state; /* 0/1 */ + 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; + 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; + 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 */ + long rx = -1, tx = -1; + e->wifi_state = wifi_read_state(&rx, &tx); + if (e->wifi_state > 0) { + 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 : m->wifi_idle_mw; + } + if (prev_rx) { *prev_rx = rx; *prev_tx = tx; } + /* BT */ + e->bt_state = bt_read_state(); + e->bt_mw = e->bt_state ? 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\","); +} + +/* 聚合 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_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}"); + } else { + jadd(&w, "\"est_mw\":%d,\"confidence\":\"estimated\",\"calibrated\":false}", e->total_mw); + } + } + 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 == 1 ? "idle" : "off")); + jadd(&w, "\"bt_mw\":%d,\"bt_state\":\"%s\",", e->bt_mw, e->bt_state ? "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) jadd(&w, ",\"policy\":null"); /* 期 3 桩 */ + jadd(&w, "}"); +} + +/* ---------------- 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, 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, MSG_PM_RESULT, 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; +static pm_model_t g_model; + +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; +} + +/* 采样拍:模式热切换 + 电池入环 + CPU 差分基准推进。(WiFi 计数基准由 est_compute 回存。) */ +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; + } + batt_t b; + batt_read(&b); + ring_push(&b); + long t = -1, i = 0; + cpu_util_pct(g_cpu_prev_total, g_cpu_prev_idle, &t, &i); + g_cpu_prev_total = t; g_cpu_prev_idle = i; +} + +static void handle_conn(int c) +{ + 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, req_id, q.subset, json); + continue; + } + send_err(c, req_id, MEOW_ERR_INVAL, "unknown type (期1仅 PM_QUERY)"); + 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; /* 立即首拍 */ + while (!g_stop) { + long now = mono_now(); + if (now >= next_tick) { + daemon_tick(); + next_tick = now + g_sample_sec; + } + int wait_ms = (int)((next_tick - mono_now()) * 1000); + if (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); } + } + } + 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; + } +} + +/* ---------------- 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"); + 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(void) +{ + pm_model_t m; + int def = model_load(&m); + printf("# meow_powermgr 经验功耗模型(POWER_MANAGER_DESIGN §3.2)\n"); + printf("# 系数来源: %s\n", def ? "内置默认(conf 缺失)" : g_model_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; +} + +static int cli_stub(const char *what, const char *phase) +{ + printf("meow_power %s: 期 %s 落地,当前未实现(POWER_MANAGER_DESIGN §9 分期)\n", what, phase); + return 1; +} + +static const char *cli_usage(void) +{ + return "status|estimate|model|history[小时]|top|events|policy(后四者期2/3桩)"; +} + +static int cli_run(int n, char **v) +{ + const char *cmd = v[0]; + (void)n; /* 子命令参数期 2/3 才用(history[小时] 等) */ + if (!strcmp(cmd, "status")) return cli_status(); + if (!strcmp(cmd, "estimate")) return cli_estimate(); + if (!strcmp(cmd, "model")) return cli_model(); + if (!strcmp(cmd, "history")) return cli_stub("history", "2"); + if (!strcmp(cmd, "top")) return cli_stub("top", "2"); + if (!strcmp(cmd, "events")) return cli_stub("events", "2"); + if (!strcmp(cmd, "policy")) return cli_stub("policy", "3"); + 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_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); +} diff --git a/patch_v2/meow_powermgr/meow_powermgr_host b/patch_v2/meow_powermgr/meow_powermgr_host new file mode 100755 index 0000000..c7e923a Binary files /dev/null and b/patch_v2/meow_powermgr/meow_powermgr_host differ diff --git a/patch_v2/meow_powermgr/services.d/meow_powermgr.conf b/patch_v2/meow_powermgr/services.d/meow_powermgr.conf new file mode 100644 index 0000000..aefb509 --- /dev/null +++ b/patch_v2/meow_powermgr/services.d/meow_powermgr.conf @@ -0,0 +1,15 @@ +# meow_powermgr 板载服务配置(svcd 托管;由 S89meow_svcd 首启预置到 /usr/data/meow/services.d/) +# +# trigger=boot(用户定案 2026-09-01,POWER_MANAGER_DESIGN §附 1):开机即常驻,历史覆盖 boot 全程。 +# HibyOS 只读降级在进程内门控:/tmp/meow_boot_mode != meow(含开机早期文件缺失)→ 只采样电池, +# 绝不施策略/不碰 wlan0/bt/声卡;文件落地为 meow 后下一采样拍自动转全功能(热切换,无需重启)。 +# 与 audiod 的 trigger=manual+exit(0) 门控不同:功耗服务读侧天然无害,HibyOS 下挂只读采样器零干扰。 +# restart=on-fail:崩溃只丢内存采样环(续航估算缓存),svcd 退避重拉,不级联(FRAMEWORK §10)。 +exec=/usr/bin/meow_powermgr +args= +enabled=1 +restart=on-fail +backoff_ms=2000 +max_restart=5 +trigger=boot +cap= diff --git a/patch_v2/meow_powermgr/test_powermgr.sh b/patch_v2/meow_powermgr/test_powermgr.sh new file mode 100755 index 0000000..9b9da6b --- /dev/null +++ b/patch_v2/meow_powermgr/test_powermgr.sh @@ -0,0 +1,162 @@ +#!/bin/bash +# test_powermgr.sh — meow_powermgr 宿主桩测试(假 sysfs/procfs 节点,MEOW_*_DIR env 覆盖)。 +# 用法:./test_powermgr.sh [host二进制路径](默认 ./meow_powermgr_host;可传 mipsel 二进制 + QEMU 前缀) +# QEMU="qemu-mipsel" ./test_powermgr.sh ./meow_powermgr # qemu 验证 +set -u +cd "$(dirname "$0")" +BIN="${1:-./meow_powermgr_host}" +Q="${QEMU:-}" +T=$(mktemp -d /tmp/pmtest.XXXXXX) +trap 'kill $DPID 2>/dev/null; rm -rf "$T"' EXIT +PASS=0; FAIL=0 +ck() { # ck <名> <期望子串> <实际> + if echo "$3" | grep -qF "$2"; then PASS=$((PASS+1)); echo "PASS $1"; + else FAIL=$((FAIL+1)); echo "FAIL $1: 期望含 [$2] 实际=[$3]"; fi +} + +# ---- 搭假节点 ---- +PS="$T/ps"; BL="$T/bl"; NET="$T/net"; RF="$T/rfkill"; BT="$T/bt" +mkdir -p "$PS/battery" "$PS/mp2731-charger" "$PS/fusb302-typec-source" "$PS/ac" \ + "$BL" "$NET/wlan0" "$RF/rfkill0" "$BT" +echo 94 > "$PS/battery/capacity" +echo Discharging > "$PS/battery/status" +echo 4275000 > "$PS/battery/voltage_now" +echo Good > "$PS/battery/health" +echo Li-ion > "$PS/battery/technology" +echo Discharging > "$PS/mp2731-charger/status" # 权威链 +echo 300 > "$PS/mp2731-charger/temp" +echo 0 > "$PS/fusb302-typec-source/online" +echo 0 > "$PS/ac/online" +echo 101 > "$BL/max_brightness" # R3 Pro II 实测量程 0-101 +echo 50 > "$BL/actual_brightness" # 中档(49%) +echo 50 > "$BL/brightness" +echo up > "$NET/wlan0/operstate" +echo bluetooth > "$RF/rfkill0/type" +echo 1 > "$RF/rfkill0/soft" # BT 被 soft 块 → off +cat > "$T/netdev" <<'EOF' +Inter-| Receive | Transmit + face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed + wlan0: 1000000 1000 0 0 0 0 0 0 2000000 1000 0 0 0 0 0 0 +EOF +echo "cpu 100 0 100 800 0 0 0 0 0 0" > "$T/proc_stat" +echo meow > "$T/boot_mode" + +ENVS="MEOW_PS_DIR=$PS MEOW_BL_DIR=$BL MEOW_NET_DIR=$NET MEOW_RFKILL_DIR=$RF MEOW_BT_DIR=$BT + MEOW_NETDEV=$T/netdev MEOW_PROC_STAT=$T/proc_stat MEOW_BOOT_MODE=$T/boot_mode + MEOW_PM_MODEL=$T/model.conf MEOW_AUDIO_SOCK=$T/no_audio.sock MEOW_PM_SOCK=$T/pm.sock" +ln -sf "$(readlink -f "$BIN")" "$T/meow_power" # argv[0] 双形态 + +# ---- T1: 守护直读(无守护 → 降级直读) ---- +OUT=$(env $ENVS $Q "$T/meow_power" status 2>/dev/null) +ck T1a "state=Discharging cap=94%" "$OUT" +echo "$OUT" | grep -q "mv=4275" && { PASS=$((PASS+1)); echo "PASS T1b mv"; } || { FAIL=$((FAIL+1)); echo "FAIL T1b: $OUT"; } +ck T1c "health=Good" "$OUT" +ck T1d "charger_temp=30.0C" "$OUT" +ck T1e "rate=null" "$OUT" +ck T1f "src=direct" "$OUT" + +# ---- T2: 起守护,IPC 路径 ---- +env $ENVS MEOW_PM_SAMPLE_SEC=1 MEOW_PM_MIN_SPAN_SEC=2 $Q "$BIN" --sock "$T/pm.sock" >"$T/daemon.log" 2>&1 & +DPID=$! +for i in $(seq 1 30); do [ -S "$T/pm.sock" ] && break; sleep 0.2; done +[ -S "$T/pm.sock" ] || { echo "FAIL T0: daemon socket 未出现"; cat "$T/daemon.log"; exit 1; } +PASS=$((PASS+1)); echo "PASS T0 daemon up" + +OUT=$(env $ENVS $Q "$T/meow_power" status 2>/dev/null) +ck T2a "src=daemon" "$OUT" +ck T2b "cap=94%" "$OUT" +ck T2c "mode=meow" "$OUT" + +# ---- T3: 容量差分速率(94→93,span≥2s)→ rate/est_left=derived ---- +echo 93 > "$PS/battery/capacity" +sleep 4 +OUT=$(env $ENVS $Q "$T/meow_power" status 2>/dev/null) +ck T3a "conf=derived" "$OUT" +ck T3b "est_left=" "$OUT" +echo "$OUT" | grep -q "rate=-" && { PASS=$((PASS+1)); echo "PASS T3c 放电速率为负"; } || { FAIL=$((FAIL+1)); echo "FAIL T3c: $OUT"; } + +# ---- T4: estimate(模型合成;中档背光+面板+wifi idle+bt off+audio unknown) ---- +OUT=$(env $ENVS $Q "$T/meow_power" estimate 2>/dev/null) +ck T4a "confidence=estimated" "$OUT" +ck T4b "待真机标定" "$OUT" +ck T4c "backlight=450" "$OUT" # 49% → 中档 450 +ck T4d "panel=80" "$OUT" +ck T4e "bt=0(off)" "$OUT" # rfkill soft=1 → off +ck T4f "audio=0(unknown)" "$OUT" # 无 audiod → unknown 0mW +# est_mw = 300(base)+450+80+200(wifi idle)+0+0+cpu(0,静态proc_stat)=1030 +ck T4g "est_mw=1030" "$OUT" + +# ---- T5: wifi 传输档(netdev 增量 >4096/拍 → tx=600) ---- +sed -i 's/wlan0: 1000000/wlan0: 1999999/; s/ 2000000 / 2999999 /' "$T/netdev" +sleep 3 +OUT=$(env $ENVS $Q "$T/meow_power" estimate 2>/dev/null) +ck T5a "wifi=600(tx)" "$OUT" + +# ---- T6: 息屏(actual_brightness=0)→ 背光/面板归零 ---- +echo 0 > "$BL/actual_brightness" +OUT=$(env $ENVS $Q "$T/meow_power" estimate 2>/dev/null) +ck T6a "backlight=0" "$OUT" +ck T6b "panel=0" "$OUT" +echo 50 > "$BL/actual_brightness" + +# ---- T7: 模型 conf 覆盖 ---- +printf '# test\nbaseline_mw=999\nwifi_idle_mw=111\n' > "$T/model.conf" +OUT=$(env $ENVS $Q "$T/meow_power" model 2>/dev/null) +ck T7a "baseline_mw=999" "$OUT" +ck T7b "wifi_idle_mw=111" "$OUT" +ck T7c "bl_high_mw=900" "$OUT" # 未覆盖键留默认 +rm -f "$T/model.conf" + +# ---- T8: HibyOS 只读降级(不施估算;电池仍可读) ---- +echo hiby > "$T/boot_mode" +sleep 2 # 等守护下一拍热切换 +OUT=$(env $ENVS $Q "$T/meow_power" estimate 2>/dev/null) +ck T8a "HibyOS 只读模式" "$OUT" +OUT=$(env $ENVS $Q "$T/meow_power" status 2>/dev/null) +ck T8b "cap=93%" "$OUT" # 只读模式电池采样不受影响 +ck T8c "mode=readonly" "$OUT" +echo meow > "$T/boot_mode" +sleep 2 +OUT=$(env $ENVS $Q "$T/meow_power" estimate 2>/dev/null) +ck T8d "est_mw=" "$OUT" # 热切换回全功能 + +# ---- T9: CLI 桩(期 2/3) ---- +OUT=$(env $ENVS $Q "$T/meow_power" history 2>&1); RC=$? +ck T9a "期 2" "$OUT" +[ $RC -eq 1 ] && { PASS=$((PASS+1)); echo "PASS T9b rc=1"; } || { FAIL=$((FAIL+1)); echo "FAIL T9b rc=$RC"; } +OUT=$(env $ENVS $Q "$T/meow_power" policy 2>&1) +ck T9c "期 3" "$OUT" + +# ---- T10: meow_powermgr 名直调子命令也走 CLI ---- +OUT=$(env $ENVS $Q "$BIN" model 2>/dev/null) +ck T10a "nominal_mah=1600" "$OUT" + +# ---- T11: 守护停 → CLI 自动降级直读不崩 ---- +kill $DPID 2>/dev/null; wait $DPID 2>/dev/null; DPID="" +sleep 1 +OUT=$(env $ENVS $Q "$T/meow_power" status 2>/dev/null) +ck T11a "src=direct" "$OUT" +ck T11b "cap=93%" "$OUT" + +# ---- T12: audiod 在场时音频链分量(真 audiod 二进制当桩:无 HELLO 直接 QUERY 协议) ---- +AUDIOD_BIN=../meow_audiod/meow_audiod_host +[ -n "$Q" ] && AUDIOD_BIN=../meow_audiod/meow_audiod +if [ -x "$AUDIOD_BIN" ]; then + env MEOW_AUDIO_FORCE=1 MEOW_AUDIO_SOCK="$T/audio.sock" MEOW_AUDIO_OUT="$T/a.wav" \ + MEOW_AUDIO_SINK=null $Q "$AUDIOD_BIN" --sock "$T/audio.sock" >"$T/audiod.log" 2>&1 & + APID=$! + for i in $(seq 1 30); do [ -S "$T/audio.sock" ] && break; sleep 0.2; done + env $ENVS MEOW_AUDIO_SOCK="$T/audio.sock" MEOW_PM_SAMPLE_SEC=1 MEOW_PM_MIN_SPAN_SEC=2 \ + MEOW_PM_SOCK="$T/pm2.sock" $Q "$BIN" --sock "$T/pm2.sock" >"$T/daemon2.log" 2>&1 & + DPID=$! + for i in $(seq 1 30); do [ -S "$T/pm2.sock" ] && break; sleep 0.2; done + OUT=$(env $ENVS MEOW_AUDIO_SOCK="$T/audio.sock" MEOW_PM_SOCK="$T/pm2.sock" $Q "$T/meow_power" estimate 2>/dev/null) + ck T12a "audio=50(idle)" "$OUT" # audiod 在场无流 → 静态档 50 + kill $DPID $APID 2>/dev/null; wait 2>/dev/null; DPID="" +else + echo "SKIP T12 (无 audiod 二进制 $AUDIOD_BIN)" +fi + +echo "==============================" +echo "PASS=$PASS FAIL=$FAIL" +[ $FAIL -eq 0 ] diff --git a/scripts/build/fw_content.conf b/scripts/build/fw_content.conf index 520a483..f0d4716 100644 --- a/scripts/build/fw_content.conf +++ b/scripts/build/fw_content.conf @@ -40,6 +40,8 @@ patch_v2/meow_crash/meow_crash|/usr/bin/meow_crash|62781084445e82351bcadbdadbe4c patch_v2/meow_svc_perf/meow_svc_perf|/usr/bin/meow_svc_perf|7ea196de9d59ab8cbd1e26edb7762772|性能专项服务 M2.75(net_json 补 network 闭合括号,修 /api/system JSON 解析失败) patch_v2/meow_audiod/meow_audiod|/usr/bin/meow_audiod|176c840d80c3e1161df0cb769185520d|独占音频守护 M2.97(+DAC 数字域电平表 rms/peak + 1s 监控日志 audio_monitor_.log) patch_v2/meow_audiod/meow_audio|/usr/bin/meow_audio|891ab26475ef9e9931bf9147d265dce4|audiod 用户侧 CLI M2.97(+monitor 实时电平表/文本电平条;status 补 rms/peak) +patch_v2/meow_powermgr/meow_powermgr|/usr/bin/meow_powermgr|469dddb74de424f7b9bc9c1ef597d69d|功耗管理专项服务期1(POWER_MANAGER_DESIGN:trigger=boot+HibyOS只读降级;权威充电链复用;经验模型待标定;IPC 0x00D0/D1) +patch_v2/meow_powermgr/meow_powermgr|/usr/bin/meow_power|469dddb74de424f7b9bc9c1ef597d69d|meow_powermgr 的 CLI 形态(同二进制双形态,argv[0]=meow_power 即 CLI:status/estimate/model,history/top/events/policy 期2/3桩) # ---- 开机 init 脚本(etc/init.d,数字决定启动顺序,按文件名自然序)---- patch_v2/meow_sysui/S70meow_sysui|/etc/init.d/S70meow_sysui|e435affcb56499e5229728c6ae2524b9|sysui 自启 M2.85(launcher.conf enabled=1+迁移)