6 Commits
10 changed files with 617 additions and 130 deletions
+242 -57
View File
@@ -1,88 +1,273 @@
# MEIZU Battery Healthy
> 专机专配 · 仅针对魅族独家开发
> **专机专配 · 仅针对魅族设备开发**
>
> 一款面向魅族手机的电池健康度与充电状态监测工具。
一个简洁的手机电池健康度监测工具<br>
通过读取 BMS 日志(`/storage/emulated/0/mbattery_charger/`)直接获取电池真实数据<br>
不依赖系统 API 估算。
MEIZU Battery Healthy 是一款基于 Flutter 开发的 Android 电池监测工具,主要针对魅族设备及其 Flyme 系统进行适配。
---
应用以设备生成的 BMS 充电日志为核心数据来源,结合 Android `BatteryManager` API,读取电池容量、电压、温度、循环次数及实时充电参数,并通过本地数据库积累历史数据。
## 功能
与仅展示系统电量百分比的工具不同,本项目更关注**底层数据的读取、不同数据源的对照,以及电池状态的长期观测**。
- 读取 BMS 日志,解析 `learned_cap`、`cycle`、`tbat`、`vbat` 等关键字段
- 显示电池健康度(当前容量 / 设计容量)
- 显示循环次数、电压、温度(含当日极值)
- 近 15 天容量百分比趋势柱状图
- 实时充电状态识别(有线 / 无线、PPS / SDP / EPP)
- 充电功率、电压、电流实时显示
- 充电详情页(协议 / 功率 / 电压 / 电流 / 档位)
## 功能特性
---
### 🔋 日常健康观察
## 当前状态
- **电池容量提取**:优先读取 `learned_cap`,部分机型回退至 `remaining_cap` 与 `status=Full` 的组合。
- **健康度计算**:根据当前可用容量与机型设计容量计算健康度百分比。
- **循环次数**:解析 BMS 日志中的 `cycle` 字段。
- **电压监测**:显示当前电压及当日最高、最低电压。
- **温度监测**:显示当前温度、当日最高温度、最低温度与平均温度。
- **异常数据提示**:对容量低于 60%、高于 115% 等异常读数进行标记,并提示电压异常。
| 版本 | 主题 | 状态 |
| :--- | :--- | :--- |
| 1.5.0 | 功率计算优化 | ✅ 已发布 |
| 1.5.2 | 部分场景字体替换 | ✅ 已发布 |
| 1.6.0 | 更新日志独立页面 | ✅ 已发布 |
| 1.7.0 | 关于页与紧急发布log文件数据反作弊能力 | ✅ 已发布 |
| 1.8.0 | 适配夜间模式 | 📋近期开发 |
| 2.0.0 | 数据看板月份显示扩展等系列更新 | 📋 远景 |
| 3.0.0 | 搭配PC上位机记录充电信息等 | 📋 远景 |
---
### ⚡ 实时充电监控
## 环境要求
- **充电状态识别**:识别有线充电、无线充电及 USB 相关状态。
- **充电协议识别**:解析 PPS、SDP、EPP 等协议标识。
- **充电档位解析**:将 BMS 日志中的 `L-n` 标定档位转换为对应的功率信息。
- **双数据源监测**:
- BMS 侧:读取充电器端电压、电流、功率及无线充电参数。
- BatteryManager 侧:读取电池端电压、电流,并计算电池端功率。
- **独立刷新策略**:BatteryManager 数据约每秒刷新一次;BMS 数据采用约 15 秒的轮询策略,实际更新还受到日志生成与落盘延迟影响。
- **充电详情页**:分别展示 BMS 日志与 BatteryManager 数据,方便对照观察。
- Flutter SDK: ^3.9.2
- Android: 8.0+ (API 26+)
#### 主界面充电信息示例:
---
`充电中 · PPS · 5.98W / 7.5W`
## 机型匹配情况
- **PPS**:当前识别到的充电协议。
- **5.98W**:由 Android `BatteryManager` API 获取电池端电流,并结合电压计算得到的当前功率,反映电池端的实时充电状态。
- **7.5W**:由 BMS 日志解析得到的当前充电档位对应的标定功率,用于表示该档位的功率信息。
| 机型 | 系统版本 | 容量来源 | 设计容量 | 状态 |
|---|---|---|---|---|
| 魅族 22 | Flyme 12.x | 系统未生成 `mbattery_charger` 目录 | 5510 mAh | ❌ 不支持 |
| 魅族 21 | Flyme 12.x | `learned_cap` | 4800 mAh | ✅ 与 21 Pro 同步获得支持 |
| 魅族 21 Pro | Flyme 12.x | `learned_cap` | 5050 mAh | ✅ 初始支持 |
| 魅族 21 Note | Flyme 12.x | `learned_cap` | 5500 mAh | ✅ 1.6.0 新增支持 |
> 其中,电池端功率为实时计算值,充电档位功率为 BMS 提供的档位信息。两者的数据来源、更新频率与测量口径不同,不应直接视为同一测量点的实际功率与额定功率。
### 📈 历史数据与趋势
- 使用 SQLite 在本地持久化每日容量、电压极值及平均温度。
- 支持全量日志扫描与历史数据入库。
- 提供近 15 天电池容量百分比趋势柱状图。
- 支持数据库版本管理与自动迁移,目前 v2 版本新增 `temp_avg` 字段。
### 🎨 主题与界面
- 支持浅色、深色及跟随系统三种主题模式。
- 使用 Flutter `ThemeExtension` 统一管理深浅主题色板。
- 首页、详情页、更新日志页及关于页适配深色模式。
- 使用 SharedPreferences 持久化主题选择。
- 针对中文与数字采用不同的字体显示策略。
## 机型兼容性
本项目采用**机型与 Flyme 版本联合适配**的策略。同一机型在不同系统版本下,可能存在日志格式变化、字段缺失或日志停止生成等情况。
以下为当前已知的适配情况。
| 机型 | 系统版本 | 容量数据来源 | 设计容量 | 状态 |
|---|---|---|---:|---|
| 魅族 22 | Flyme 12.x | 系统未生成日志目录 | 5510 mAh | ❌ 不支持 |
| 魅族 21 | Flyme 12.x | `learned_cap` | 4800 mAh | ✅ 支持 |
| 魅族 21 Pro | Flyme 12.x | `learned_cap` | 5050 mAh | ✅ 支持 |
| 魅族 21 Note | Flyme 12.x | `learned_cap` | 5500 mAh | ✅ 支持 |
| 魅族 21 Note | Flyme 10 / 11 | 日志格式异常 | — | ❌ 不支持 |
| 魅族 20 | Flyme 12.x | `remaining_cap` + `status=Full` | 4700 mAh | ✅ 1.4.0 首次支持 |
| 魅族 20 Pro | Flyme 12.x | `remaining_cap` + `status=Full` | 5000 mAh | ✅ 1.4.0 首次支持 |
| 魅族 20 INFINITY | Flyme 10.x | `remaining_cap` + `status=Full` | 4800 mAh | ✅ 1.5.3 字段匹配 |
| 魅族 18 / 18s / 18 Pro / 18s Pro / 18X | Flyme 9 | 日志停止生成 | 4000 / 4500 mAh | ❌ 无适配条件 |
| 魅族 18 / 18s / 18 Pro / 18s Pro / 18X | Flyme 10 | 不再主动适配 | 4000 / 4500 mAh | ⚠️ 实验性支持 |
| 魅族 17 / 17Pro | Flyme 8 / 9 / 10 | 等待群友提供字段信息 | 4500 / 4500mAh | ⚠️ 待验证 |
| 魅族 16th / 16th Plus / 16X / 16s / 16sPro / 16Xs / 16T | Flyme 7 / 8 / 9 | 需回退原厂固件验证 | - | ⚠️ 若新版本无日志,则变为实验性支持,不主动适配 |
| 魅族 20 | Flyme 12.x | `remaining_cap` + `status=Full` | 4700 mAh | ✅ 支持 |
| 魅族 20 Pro | Flyme 12.x | `remaining_cap` + `status=Full` | 5000 mAh | ✅ 支持 |
| 魅族 20 INFINITY | Flyme 10.x | `remaining_cap` + `status=Full` | 4800 mAh | ✅ 支持 |
| 魅族 18 / 18s 系列 | Flyme 9 | 日志停止生成 | 4000 / 4500 mAh | ❌ 不支持 |
| 魅族 18 / 18s 系列 | Flyme 10 | 待实机确认 | 4000 / 4500 mAh | ⚠️ 实验性支持 |
| 魅族 17 / 17 Pro | Flyme 8 / 9 / 10 | 待收集日志样本 | 4500 mAh | ⚠️ 待验证 |
| 魅族 16 系列 | Flyme 7 / 8 / 9 | 待验证 | — | ⚠️ 实验性支持 |
### 状态说明
| 状态 | 含义 |
- ✅ **支持**:已通过实机验证,相关字段能够正常解析。
- ⚠️ **实验性支持**:具备一定适配基础,但尚未完成充分的实机验证。
- ⚠️ **待验证**:缺少足够的日志样本或实机测试结果。
- ❌ **不支持**:当前已知环境下无法获取所需数据,或日志格式不满足解析要求。
### 兼容性说明
**Flyme 版本差异**
同一机型在不同 Flyme 版本下可能使用不同的日志格式或字段。请以具体机型和系统版本的实际测试结果为准。
**日志生成状态**
部分设备升级系统后可能不再生成新的 BMS 日志。即使日志目录仍然存在,也不代表其中的数据持续更新。
**设计容量**
设计容量用于计算健康度,并非由应用实时测量。计算结果会受到机型参数、底层容量字段含义及电池管理系统校准状态的影响。
**适配策略**
当前优先维护日志格式相对稳定、已完成实机验证的机型。旧机型及其他系统版本的适配将根据实际日志样本逐步推进。
## 数据来源与测量说明
本项目使用两套数据来源,分别承担底层日志分析与实时电池监测的职责。
### 1. BMS 日志:充电器端及设备充电状态
- **数据位置**:`/storage/emulated/0/mbattery_charger/`
- **主要字段**:`learned_cap`、`remaining_cap`、`cycle`、`tbat`、`vbat`、`ibus`、`vbus`、`wls_volt`、`wls_curr` 等。
- **主要用途**:容量提取、循环次数解析、充电协议识别、充电档位解析及充电器端参数展示。
- **更新特征**:日志写入与落盘存在延迟,应用的轮询间隔不等于底层数据的实际更新频率。
BMS 日志的可用字段取决于设备及系统版本。不同机型不一定提供完全相同的数据。
### 2. BatteryManager:电池端实时监测
- **接口**:`BatteryManager.BATTERY_PROPERTY_CURRENT_NOW`
- **主要用途**:获取电池端电流,并结合电压计算电池端功率。
- **刷新策略**:应用侧约每秒轮询一次。
- **数据特征**:电流读数的单位、符号及实际更新行为需要结合 Android 实现与设备厂商行为处理。
本项目会对电流极性进行归一化,使充电方向以正值显示。
需要注意,BatteryManager 提供的电流读数并不等同于充电器的输出电流。它反映的是电池端的电流状态,可能受到系统功耗、充电管理策略及电量计实现的影响。
### 3. 两套数据如何对照?
BMS 数据主要用于观察充电器端及充电协议相关信息,BatteryManager 数据主要用于观察电池端电流、电压与功率。
在采样时间接近、测量口径明确的情况下,两侧功率的差异可以帮助分析系统运行功耗与转换损耗。但由于日志延迟、采样时差及两侧测量边界不同,**功率差值只能作为参考,不能直接视为精确的系统功耗或充电转换损耗**。
### 4. 数据可信度与适用边界
- 健康度是根据可读取的容量字段与预设设计容量计算得到的结果,不等同于专业电池检测设备给出的容量测试结果。
- 容量、电压与温度异常标记用于辅助识别可疑读数,不单独构成电池故障诊断。
- 日志数据可能存在缺失、延迟、格式差异或历史记录不完整的情况。
- 应用不会将尚未验证的机型适配能力视为已经得到实机确认。
## 技术架构
项目采用 Flutter 与 Kotlin 原生通道协作的结构。
```text
MEIZU Battery Healthy
│
├── Flutter UI
│ ├── 首页
│ ├── 充电详情页
│ ├── 历史趋势
│ ├── 更新日志
│ └── 关于页面
│
├── 业务逻辑层
│ ├── ChargeParser
│ ├── CapacityExtractor
│ ├── BatteryDataService
│ └── ChargeUtils
│
├── 服务层
│ ├── SafStorageService
│ ├── BatteryManagerService
│ ├── DatabaseService
│ └── ThemeService
│
└── Kotlin 原生层
├── /saf
├── /charge
├── /charge_events
├── /frame_rate
└── BatteryManagerHelper
```
### 主要技术组件
- **Flutter**:跨层 UI 与业务逻辑实现。
- **Kotlin / Android 原生接口**:文件访问、BatteryManager 数据读取、充电插拔事件与刷新率控制。
- **SQLite**:本地历史数据持久化及数据库迁移。
- **SharedPreferences**:主题配置与授权相关状态的持久化。
日志访问通过 Android Storage Access Framework(SAF)授权机制管理。实际可读取的数据取决于用户授权状态及设备系统对相关文件的访问权限。
## 开发环境
| 组件 | 版本要求 |
|---|---|
| ✅ 完美支持 | 已通过实机验证,所有字段完整解析 |
| ⚠️ 实验性支持 | 理论上可支持,但需要用户协助验证 |
| ⚠️ 待验证 | 尚未收到实机日志反馈,待确认 |
| ❌ 不支持 | 系统未生成 BMS 日志,或日志格式无法解析 |
| Flutter SDK | `^3.9.2` |
| Android | API 26+(Android 8.0+) |
| Android Gradle Plugin | 9.0.0 |
| Kotlin Gradle Plugin | 2.2.20 |
| Gradle | 9.3.1 |
### 特别说明
当前版本使用经过验证的构建工具组合。升级 AGP、KGP 或 Gradle 前,建议先完成构建兼容性验证。
- **Flyme 版本差异**:同一机型在不同 Flyme 版本下日志格式可能不同。例如 21 Note 在 Flyme 12.x 下可正常读取 `learned_cap`,但在 Flyme 10/11 下日志格式异常。
- **日志停止生成**:部分机型在升级到新系统后(如 17/18 系升级到 Flyme 10),系统不再向 `mbattery_charger` 目录写入新日志,文件夹虽存在但停止更新。
- **适配策略**:优先支持日志格式稳定的 Flyme 12.x 机型,旧版本系统暂不主动适配,若有用户长期停留在旧版本并愿意协助测试,可酌情增加支持。
## 从源码构建
---
### 1. 获取源码
## 如果需要基于此源代码构建
```bash
git clone https://git.whitetop.xyz/lxh2875931338/MEIZU-Battery-Healthy.git
cd MEIZU-Battery-Healthy
```
### 2. 安装依赖
```bash
# 拉取依赖
flutter pub get
```
# Debug 版本
### 3. 运行 Debug 版本
```bash
flutter run --debug
```
# Release APK
### 4. 构建 Release APK
当前发布构建面向 ARM64 Android 设备:
```bash
flutter build apk --release --target-platform android-arm64
```
### 构建注意事项
- 修改 Kotlin 原生代码后,需要重新执行构建或运行流程;热重载不会重新编译原生代码。
- Release 构建需要 `android/app/proguard-rules.pro`,项目已包含对应规则。
- 不建议在未完成验证前随意升级 AGP、KGP 或 Gradle。
- 实际构建结果还取决于本地 Android SDK、JDK 及依赖环境。
## 版本历史
| 版本 | 主要更新 | 状态 |
|---|---|---|
| 1.5.0 | 功率计算优化 | ✅ 已发布 |
| 1.5.2 | 部分场景字体替换 | ✅ 已发布 |
| 1.6.0 | 更新日志独立页面 | ✅ 已发布 |
| 1.7.0 | 关于页、数据反作弊校验 | ✅ 已发布 |
| 1.7.1 | 平均温度显示、数据库 v2 迁移 | ✅ 已发布 |
| 1.8.0 | 夜间模式、主题系统重构 | ✅ 已发布 |
| 1.9.0 | BatteryManager 实时数据接入 | ✅ 已发布 |
### 后续规划
| 版本 | 规划内容 | 状态 |
|---|---|---|
| 2.0.0 | 数据看板月份扩展、充电曲线 | 📋 开发中 |
| 3.0.0 | PC 上位机充电信息记录 | 📋 远景规划 |
以上规划不代表相关功能已经包含在当前发布版本中,实际进度以仓库更新为准。
## 反馈与机型适配
项目优先关注真实设备上的数据可用性与日志格式差异。
如果你希望协助适配新机型,欢迎提交 Issue,并尽可能提供以下信息:
- 手机型号。
- Flyme 系统版本。
- BMS 日志是否持续生成。
- 容量字段及充电相关字段的可用情况。
- 对应的异常现象与复现步骤。
日志样本可能包含设备运行信息。提交前请检查并移除个人信息、设备标识及其他不希望公开的内容。
**源码仓库:**
[MEIZU Battery Healthy](https://git.whitetop.xyz/lxh2875931338/MEIZU-Battery-Healthy)
> 本项目专注于魅族设备的电池状态观测与数据分析,不保证所有魅族机型或所有 Flyme 版本均可使用。监测结果仅供参考,不构成电池故障诊断或维修依据。
@@ -0,0 +1,51 @@
package com.lxh.battery_health
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
class BatteryManagerHelper(private val context: Context) {
fun getBatteryData(): Map<String, Any?> {
val bm = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
// 电流(µA)。部分机型充电为负、放电为正,部分相反
val rawCurrentUa = bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW)
// 电压 / 状态从 ACTION_BATTERY_CHANGED 广播里拿(比 BATTERY_PROPERTY_VOLTAGE 兼容性好)
val intent = context.registerReceiver(
null,
IntentFilter(Intent.ACTION_BATTERY_CHANGED)
)
val voltageMv = intent?.getIntExtra(BatteryManager.EXTRA_VOLTAGE, -1) ?: -1
val status = intent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1
val plugged = intent?.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) ?: -1
val temperatureTenth = intent?.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, -1) ?: -1
val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
status == BatteryManager.BATTERY_STATUS_FULL
// 符号规约:充电为正
val normalizedUa = when {
rawCurrentUa == Int.MIN_VALUE -> 0
isCharging && rawCurrentUa < 0 -> -rawCurrentUa
!isCharging && rawCurrentUa > 0 -> -rawCurrentUa
else -> rawCurrentUa
}
// 功率 W = V × A = (mV / 1000) × (µA / 1_000_000)
val powerW = if (voltageMv > 0 && normalizedUa != 0) {
(voltageMv / 1000.0) * (normalizedUa / 1_000_000.0)
} else 0.0
return mapOf(
"voltageMv" to voltageMv,
"currentUa" to normalizedUa,
"powerW" to powerW,
"temperatureC" to if (temperatureTenth > 0) temperatureTenth / 10.0 else 0.0,
"plugged" to plugged, // 0=未插, 1=AC, 2=USB, 4=WIRELESS
"isCharging" to isCharging,
)
}
}
@@ -255,26 +255,35 @@ private fun clearRefreshRate() {
// 充电状态通道
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHARGE_CHANNEL)
.setMethodCallHandler { call, result ->
if (call.method == "getChargeState") {
val isCharging = getBatteryStatus()
val chargeType = if (isCharging) {
val batteryIntent = registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
val plug = batteryIntent?.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) ?: -1
when (plug) {
BatteryManager.BATTERY_PLUGGED_AC -> "AC"
BatteryManager.BATTERY_PLUGGED_USB -> "USB"
BatteryManager.BATTERY_PLUGGED_WIRELESS -> "WIRELESS"
else -> "UNKNOWN"
}
} else {
"NONE"
.setMethodCallHandler { call, result ->
when (call.method) {
"getChargeState" -> {
val isCharging = getBatteryStatus()
val chargeType = if (isCharging) {
val batteryIntent = registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
val plug = batteryIntent?.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) ?: -1
when (plug) {
BatteryManager.BATTERY_PLUGGED_AC -> "AC"
BatteryManager.BATTERY_PLUGGED_USB -> "USB"
BatteryManager.BATTERY_PLUGGED_WIRELESS -> "WIRELESS"
else -> "UNKNOWN"
}
result.success(mapOf("isCharging" to isCharging, "chargeType" to chargeType))
} else {
result.notImplemented()
"NONE"
}
result.success(mapOf("isCharging" to isCharging, "chargeType" to chargeType))
}
// 🆕 新增分支
"getBatteryManagerData" -> {
try {
result.success(BatteryManagerHelper(applicationContext).getBatteryData())
} catch (e: Exception) {
result.error("BM_ERROR", e.message, null)
}
}
else -> result.notImplemented()
}
}
// SAF 通道
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, SAF_CHANNEL)
+41 -1
View File
@@ -1,4 +1,44 @@
## 1.8.0
## [1.9.0] - 2026-10-11
### ✨ 新增功能
- **BatteryManager 实时数据接入**
- 新增原生 `BatteryManagerHelper`,通过 Android 官方 API 读取电池端真实功率
- 主界面充电行改为显示电池端实时功率(1 秒刷新),反映电池实际吸收的能量
- 充电详情页拆分为两个数据源分区:
- **BMS 日志**:协议、标定档位、实际功率、电压、电流(原有)
- **BatteryManager(电池端)**:电池端电压、电流、功率(新增)
- 与专业功率计(PowerZ)实测数据一致,误差在 0.02W 以内
### 🎨 体验优化
- **充电行显示格式调整**
- 有线充电:`充电中 · PPS · 5.98W / 7.5W`(实际功率 / 标定档位)
- 无线充电:`充电中 · 无线 · 12.3W`
- USB 慢充:`充电中 · USB · 2.1W`
- 瓦特单位统一为大写 `W`
- 7.5W 等带小数的档位保留一位小数,其余整数值去掉 `.0`
- **字体细节优化**
- 充电行数字与拉丁字母改用 Flyme 字体,中文字符保持 HarmonyOS Sans SC
- 解决混合文本下的字体割裂感
- 充电详情页所有功率数值同步应用 Flyme 字体
### 🐛 问题修复
- 修复主界面充电行在充电器接入初期显示"读取中..."后数值迟迟不刷新的问题
- 修复详情页功率数值字体与其它行不统一的问题
- 修复 `wired_level` 解析后标定档位显示取整误差(如 `≈8W/L-1` 修正为 `≈7.5W/L-1`)
### 📝 技术说明
- `BATTERY_PROPERTY_CURRENT_NOW` 读数为**电池端净电流**(充电器输入减去系统功耗),而非充电器输出端数据
- 电池接近满电时数值会明显偏低(涓流阶段),属正常现象
- 与系统状态栏显示的充电功率为不同概念,前者反映电池实际吸收,后者反映充电器输出
---
## [1.8.0] - 2026-10-11
### 新增
- **夜间模式**:支持浅色 / 深色 / 跟随系统三种模式,可在右上角菜单切换
+38 -10
View File
@@ -5,6 +5,7 @@ import 'package:flutter/services.dart';
import 'package:device_info_plus/device_info_plus.dart';
import '../theme/theme_service.dart';
import '../theme/app_colors.dart';
import '../services/battery_manager_service.dart';
// --- 本地导入 ---
import '../models/device_matching.dart';
@@ -64,6 +65,9 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
int? _wiredLevel;
bool _isWireless = false;
DateTime? _chargingStartTime;
double _bmPower = 0.0;
double _bmVolt = 0.0;
double _bmCurr = 0.0;
// ---------- SAF 状态 ----------
final SafStorageService _safService = SafStorageService();
@@ -80,6 +84,7 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
// 轮询定时器(15秒)
Timer? _pollTimer;
Timer? _bmTimer;
// ---------- 生命周期 ----------
@override
@@ -124,7 +129,7 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
readBatteryCapacity();
}
});
_startBmPolling(); // ← 调用,不是定义
dev.log('🔵 ========== initState 结束 ==========', name: 'BatteryHealth');
}
@@ -132,6 +137,7 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
void dispose() {
_chargeEventSubscription?.cancel();
_pollTimer?.cancel();
_bmTimer?.cancel();
_dbService.database.then((db) => db.close()).ignore();
super.dispose();
}
@@ -252,6 +258,9 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
_chargeCurr = 0.0;
_wiredLevel = null;
_chargingStartTime = null;
_bmPower = 0.0; // 🆕
_bmVolt = 0.0; // 🆕
_bmCurr = 0.0; // 🆕
return;
}
@@ -610,18 +619,34 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
}
}
// ---------- BM 轮询(1秒) ----------
void _startBmPolling() {
_bmTimer?.cancel();
_bmTimer = Timer.periodic(const Duration(seconds: 1), (_) async {
if (!_isCharging) return;
try {
final d = await BatteryManagerService.fetch();
if (!mounted) return;
setState(() {
_bmPower = d.powerW;
_bmVolt = d.voltageV;
_bmCurr = d.currentA;
});
} catch (e) {
// 静默降级,保留上一次的值
}
});
}
// ---------- 显示辅助 ----------
String _getDisplayPower() {
if (!_isCharging) return '';
if (_isWireless) {
return ChargeUtils.getDisplayPowerWireless(_chargePower);
} else {
return ChargeUtils.getMainDisplayPowerWired(
_chargePower,
_wiredLevel,
_chargeType,
);
}
return ChargeUtils.getMainDisplayPowerBm(
_bmPower,
_wiredLevel,
_chargeType,
_isWireless,
);
}
String _getChargeTypeLabel() {
@@ -820,6 +845,9 @@ class _BatteryHealthPageState extends State<BatteryHealthPage> {
volt: _chargeVolt,
curr: _chargeCurr,
wiredLevel: _wiredLevel,
bmPower: _bmPower, // 🆕
bmVolt: _bmVolt, // 🆕
bmCurr: _bmCurr, // 🆕
),
),
);
+57
View File
@@ -0,0 +1,57 @@
import 'package:flutter/services.dart';
/// BatteryManager 实时数据(电池端:电压 / 电流 / 功率)
class BatteryManagerData {
final int voltageMv;
final int currentUa;
final double powerW;
final double temperatureC;
final int plugged; // 0=未插, 1=AC, 2=USB, 4=WIRELESS
final bool isCharging;
const BatteryManagerData({
required this.voltageMv,
required this.currentUa,
required this.powerW,
required this.temperatureC,
required this.plugged,
required this.isCharging,
});
factory BatteryManagerData.empty() => const BatteryManagerData(
voltageMv: 0,
currentUa: 0,
powerW: 0,
temperatureC: 0,
plugged: 0,
isCharging: false,
);
factory BatteryManagerData.fromMap(dynamic raw) {
if (raw is! Map) return BatteryManagerData.empty();
return BatteryManagerData(
voltageMv: (raw['voltageMv'] as num?)?.toInt() ?? 0,
currentUa: (raw['currentUa'] as num?)?.toInt() ?? 0,
powerW: (raw['powerW'] as num?)?.toDouble() ?? 0.0,
temperatureC: (raw['temperatureC'] as num?)?.toDouble() ?? 0.0,
plugged: (raw['plugged'] as num?)?.toInt() ?? 0,
isCharging: raw['isCharging'] as bool? ?? false,
);
}
/// 电压 V
double get voltageV => voltageMv / 1000.0;
/// 电流 A
double get currentA => currentUa / 1_000_000.0;
}
class BatteryManagerService {
static const _channel = MethodChannel('com.lxh.battery_health/charge');
/// 主动拉取一次
static Future<BatteryManagerData> fetch() async {
final result = await _channel.invokeMethod('getBatteryManagerData');
return BatteryManagerData.fromMap(result);
}
}
+44 -20
View File
@@ -1,7 +1,7 @@
import '../constants/charge_maps.dart';
class ChargeUtils {
// ============ 无线充电(保持不变) ============
// ============ 无线充电 ============
static String getDisplayPowerWireless(double power) {
if (power == 0) return '读取中...';
const List<int> levels = [5, 7, 15, 27, 40, 50, 66];
@@ -13,7 +13,7 @@ class ChargeUtils {
return '≈${matchedLevel}W';
}
// ============ 原有有线逻辑(保留兼容,但主界面不再直接调用) ============
// ============ 旧的有线逻辑(保留兼容) ============
static String getDisplayPowerWired(
double power,
int? wiredLevel,
@@ -34,57 +34,81 @@ class ChargeUtils {
return best > 0 ? '≈${best.toInt()}W' : '≈${power.round()}W';
}
// ============ 🟢 1.5.0 新增:主界面显示(实际/标定) ============
/// 示例:≈30w/65w
// ============ 主界面:实际 / 标定(旧版) ============
static String getMainDisplayPowerWired(
double power,
int? wiredLevel,
String chargeType,
) {
// USB 慢充锁定 7.5W
if (chargeType == "SDP") return "≈7.5w";
if (chargeType == "SDP") return "≈7.5W";
if (power == 0) return "读取中...";
final actual = power.round();
// 有档位且能查到标定值
if (wiredLevel != null && ChargeMaps.wiredPower.containsKey(wiredLevel)) {
final requested = ChargeMaps.wiredPower[wiredLevel]!.round();
return "≈${actual}w/${requested}w";
return "≈${actual}W/${requested}W";
}
// 无档位信息,仅显示实际功率
return "≈${actual}w";
return "≈${actual}W";
}
// ============ 🟢 1.5.0 新增:二级页面标定档位显示 ============
/// 示例:≈80w/L-10
// ============ 主界面:BM 电池端功率 + 标定档位 ============
/// 示例:5.98W / 7.5W、12.3W、读取中...
static String getMainDisplayPowerBm(
double bmPower,
int? wiredLevel,
String chargeType,
bool isWireless,
) {
if (bmPower <= 0 || bmPower > 100) return '读取中...';
final powerStr = '${bmPower.toStringAsFixed(1)}W';
if (isWireless || chargeType == 'SDP') return powerStr;
if (wiredLevel != null && ChargeMaps.wiredPower.containsKey(wiredLevel)) {
final requested = ChargeMaps.wiredPower[wiredLevel]!;
final requestedStr = requested == requested.roundToDouble()
? requested.toInt().toString()
: requested.toStringAsFixed(1);
return '$powerStr / ${requestedStr}W';
}
return powerStr;
}
// ============ 详情页:标定档位 ============
/// 示例:≈80W/L-10、≈7.5W/L-1
static String getDetailRequestedPowerWired(
double power,
int? wiredLevel,
String chargeType,
) {
if (chargeType == "SDP") return "≈7.5w";
if (chargeType == "SDP") return "≈7.5W";
if (power == 0) return "读取中...";
if (wiredLevel != null && ChargeMaps.wiredPower.containsKey(wiredLevel)) {
final requested = ChargeMaps.wiredPower[wiredLevel]!.round();
return "≈${requested}w/L-$wiredLevel";
final requested = ChargeMaps.wiredPower[wiredLevel]!;
final str = requested == requested.roundToDouble()
? requested.toInt().toString()
: requested.toStringAsFixed(1);
return "≈${str}W/L-$wiredLevel";
}
return "≈${power.round()}w";
return "≈${power.round()}W";
}
// ============ 🟢 1.5.0 新增:二级页面实际功率显示 ============
// ============ 详情页:实际功率 ============
static String getDetailActualPowerWired(double power, String chargeType) {
if (chargeType == "SDP") return "≈7.5w";
if (chargeType == "SDP") return "≈7.5W";
if (power == 0) return "读取中...";
return "≈${power.round()}w";
return "≈${power.round()}W";
}
// ============ 充电类型标签(保持不变) ============
// ============ 充电类型标签 ============
static String getChargeTypeLabel(bool isWireless, String chargeType) {
if (isWireless) {
return chargeType == "无线" ? "无线" : chargeType;
+56 -17
View File
@@ -10,6 +10,9 @@ class ChargeDetailPage extends StatelessWidget {
final double volt;
final double curr;
final int? wiredLevel;
final double bmPower;
final double bmVolt;
final double bmCurr;
const ChargeDetailPage({
super.key,
@@ -19,6 +22,9 @@ class ChargeDetailPage extends StatelessWidget {
required this.volt,
required this.curr,
this.wiredLevel,
this.bmPower = 0,
this.bmVolt = 0,
this.bmCurr = 0,
});
String _getProtocolDisplay() {
@@ -62,18 +68,31 @@ class ChargeDetailPage extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ========== BMS 来源 ==========
_buildSectionTitle(context, 'BMS 日志'),
_buildTextRow(context, '协议', _getProtocolDisplay()),
const Divider(),
if (!isWireless) ...[
_buildTextRow(context, '标定档位', _getRequestedDisplay()),
_buildTextRow(
context,
'标定档位',
_getRequestedDisplay(),
valueFont: 'Flyme',
),
const Divider(),
_buildTextRow(context, '实际功率', _getActualDisplay()),
_buildTextRow(
context,
'实际功率',
_getActualDisplay(),
valueFont: 'Flyme',
),
const Divider(),
] else ...[
_buildTextRow(
context,
'功率',
ChargeUtils.getDisplayPowerWireless(power),
valueFont: 'Flyme',
),
const Divider(),
],
@@ -81,20 +100,19 @@ class ChargeDetailPage extends StatelessWidget {
const Divider(),
_buildDetailRow(context, '电流', curr, 'A'),
const Divider(),
if (isWireless) _buildTextRow(context, '类型', '无线'),
if (!isWireless && wiredLevel == null)
_buildTextRow(context, '档位', '自动'),
const SizedBox(height: 24),
// ========== BM 来源 ==========
_buildSectionTitle(context, 'BatteryManager(电池端)'),
_buildDetailRow(context, '电压', bmVolt, 'V'),
const Divider(),
_buildDetailRow(context, '电流', bmCurr, 'A'),
const Divider(),
bmPower > 0
? _buildDetailRow(context, '功率', bmPower, 'W')
: _buildTextRow(context, '功率', '读取中...'),
const Spacer(),
Center(
child: Text(
'数据来自 BMS 实时日志',
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 12,
color: c.textTertiary,
),
),
),
],
),
),
@@ -102,7 +120,28 @@ class ChargeDetailPage extends StatelessWidget {
);
}
Widget _buildTextRow(BuildContext context, String label, String value) {
Widget _buildSectionTitle(BuildContext context, String title) {
final c = context.colors;
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
title,
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 13,
fontWeight: FontWeight.w600,
color: c.textTertiary,
),
),
);
}
Widget _buildTextRow(
BuildContext context,
String label,
String value, {
String valueFont = 'HarmonyOS_Sans_SC',
}) {
final c = context.colors;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6.0),
@@ -121,7 +160,7 @@ class ChargeDetailPage extends StatelessWidget {
Text(
value,
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontFamily: valueFont,
fontSize: 16,
fontWeight: FontWeight.w500,
color: c.textPrimary,
+61 -7
View File
@@ -21,13 +21,17 @@ class ChargeEntry extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
summary,
style: TextStyle(
fontFamily: 'HarmonyOS_Sans_SC',
fontSize: 16,
fontWeight: FontWeight.w500,
color: c.textPrimary,
Flexible(
child: RichText(
overflow: TextOverflow.ellipsis,
text: TextSpan(
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: c.textPrimary,
),
children: _buildSpans(summary),
),
),
),
Row(
@@ -57,4 +61,54 @@ class ChargeEntry extends StatelessWidget {
),
);
}
/// 按 CJK / 非 CJK 拆分字符串,分别应用 HarmonyOS / Flyme
List<TextSpan> _buildSpans(String text) {
final spans = <TextSpan>[];
if (text.isEmpty) return spans;
final buffer = StringBuffer();
bool? isCJK;
void flush() {
if (buffer.isEmpty) return;
spans.add(
TextSpan(
text: buffer.toString(),
style: TextStyle(
fontFamily: (isCJK ?? false) ? 'HarmonyOS_Sans_SC' : 'Flyme',
),
),
);
buffer.clear();
}
for (final rune in text.runes) {
final cjk = _isCJKRune(rune);
if (isCJK == null || cjk == isCJK) {
buffer.writeCharCode(rune);
isCJK = cjk;
} else {
flush();
buffer.writeCharCode(rune);
isCJK = cjk;
}
}
flush();
return spans;
}
bool _isCJKRune(int rune) {
// CJK 统一表意文字
if (rune >= 0x4E00 && rune <= 0x9FFF) return true;
// CJK 扩展 A
if (rune >= 0x3400 && rune <= 0x4DBF) return true;
// 中文标点(。、,:;等)
if (rune >= 0x3000 && rune <= 0x303F) return true;
// CJK 兼容符号
if (rune >= 0xFE30 && rune <= 0xFE4F) return true;
// 全角字符
if (rune >= 0xFF00 && rune <= 0xFFEF) return true;
return false;
}
}
+1 -1
View File
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.8.0
version: 1.9.0
environment:
sdk: ^3.9.2