Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 17c728565c | |||
| 64e1a9f910 | |||
| 9d5a53ea1f | |||
| 49d4127ad9 | |||
| b97508dd10 | |||
| ff1070a350 | |||
| 22db1bc89b | |||
| c445ced7c1 | |||
| a745a02b2a | |||
| 4652034373 | |||
| 0834ba8406 | |||
| b8ce1e4462 | |||
| 15870b4ac4 |
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"cmake.ignoreCMakeListsMissing": true
|
||||
}
|
||||
@@ -1,16 +1,53 @@
|
||||
# battery_health
|
||||
# MEIZU Battery Healthy
|
||||
|
||||
A new Flutter project.
|
||||
> 专机专配 · 仅针对魅族独家开发
|
||||
|
||||
## Getting Started
|
||||
一个简洁的手机电池健康度监测工具<br>
|
||||
通过读取 BMS 日志(`/storage/emulated/0/mbattery_charger/`)直接获取电池真实数据<br>
|
||||
不依赖系统 API 估算。
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
---
|
||||
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
## 功能
|
||||
|
||||
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||
- 读取 BMS 日志,解析 `learned_cap`、`cycle`、`tbat`、`vbat` 等关键字段
|
||||
- 显示电池健康度(当前容量 / 设计容量)
|
||||
- 显示循环次数、电压、温度(含当日极值)
|
||||
- 近 15 天容量百分比趋势柱状图
|
||||
- 实时充电状态识别(有线 / 无线、PPS / SDP / EPP)
|
||||
- 充电功率、电压、电流实时显示
|
||||
- 充电详情页(协议 / 功率 / 电压 / 电流 / 档位)
|
||||
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
---
|
||||
|
||||
## 当前状态
|
||||
|
||||
| 版本 | 主题 | 状态 |
|
||||
| :--- | :--- | :--- |
|
||||
| 1.2.0 | UTC 时区基础修正 | ✅ 已发布 |
|
||||
| 1.3.0 | 时间窗口 + real_type 优先级 | 🚀 测试中 |
|
||||
| 1.4.0 | SAF 权限改造 | 📋 规划中 |
|
||||
| 1.5.0 | 功率计算优化 | 📋 规划中 |
|
||||
| 2.0.0 | 数据看板扩展(月/季/年趋势) | 📋 远景 |
|
||||
|
||||
---
|
||||
|
||||
## 环境要求
|
||||
|
||||
- Flutter SDK: ^3.9.2
|
||||
- Android: 8.0+ (API 26+)
|
||||
- 仅支持魅族手机(15 系列 ~ 25 系列)
|
||||
|
||||
---
|
||||
|
||||
## 如果需要基于此源代码构建
|
||||
|
||||
```bash
|
||||
# 拉取依赖
|
||||
flutter pub get
|
||||
|
||||
# Debug 版本
|
||||
flutter run
|
||||
|
||||
# Release APK
|
||||
flutter build apk --release
|
||||
@@ -22,6 +22,6 @@ class ChargeMaps {
|
||||
6: 30,
|
||||
7: 45,
|
||||
8: 65,
|
||||
9: 80,
|
||||
10: 80,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -47,13 +47,16 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
|
||||
|
||||
// ---------- 充电状态 ----------
|
||||
bool _isCharging = false;
|
||||
String _chargeType = ''; // "EPP", "PPS", "SDP", "无线" 等
|
||||
String _chargeType = ''; // "EPP", "PPS", "SDP", "无线", "MZ_EPP" 等
|
||||
double _chargePower = 0.0;
|
||||
double _chargeVolt = 0.0;
|
||||
double _chargeCurr = 0.0;
|
||||
int? _wiredLevel;
|
||||
bool _isWireless = false;
|
||||
|
||||
// 🟢 1.3.0-dev.3: 充电开始时间(UTC),用于时间窗口判断
|
||||
DateTime? _chargingStartTime;
|
||||
|
||||
// 动画控制
|
||||
late AnimationController _chargeAnimationController;
|
||||
late Animation<double> _bottomSlide;
|
||||
@@ -195,15 +198,17 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 1.3.0-dev.1: 使用 UTC 生成文件名 ----------
|
||||
String getTodayFileName() {
|
||||
DateTime now = DateTime.now();
|
||||
String year = now.year.toString();
|
||||
String month = now.month.toString().padLeft(2, '0');
|
||||
String day = now.day.toString().padLeft(2, '0');
|
||||
DateTime nowUtc = DateTime.now().toUtc();
|
||||
String year = nowUtc.year.toString();
|
||||
String month = nowUtc.month.toString().padLeft(2, '0');
|
||||
String day = nowUtc.day.toString().padLeft(2, '0');
|
||||
return 'mbattery_charger_log_${year}_${month}_$day';
|
||||
}
|
||||
|
||||
// ---------- 时间戳工具方法 ----------
|
||||
/// 解析日志行中的时间戳,强制解析为 UTC
|
||||
DateTime? _parseLogTimestamp(String line) {
|
||||
RegExp reg = RegExp(r'\[(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})\]');
|
||||
Match? match = reg.firstMatch(line);
|
||||
@@ -226,18 +231,24 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
|
||||
}
|
||||
}
|
||||
|
||||
/// 🟢 1.3.0-dev.3: 时间窗口以插入时刻为基准,只向前看
|
||||
bool _isLogTimestampValid(DateTime? logTime) {
|
||||
if (logTime == null) return false;
|
||||
if (_chargingStartTime == null) {
|
||||
// 边缘情况:回退到当前时间 ±3 分钟
|
||||
DateTime nowUtc = DateTime.now().toUtc();
|
||||
Duration diff = nowUtc.difference(logTime);
|
||||
bool isValid = diff.abs().inMinutes <= 3;
|
||||
return isValid;
|
||||
return diff.abs().inMinutes <= 3;
|
||||
}
|
||||
// 日志时间必须在 [充电开始时间, 充电开始时间 + 3 分钟] 范围内
|
||||
DateTime endTime = _chargingStartTime!.add(const Duration(minutes: 3));
|
||||
return logTime.isAfter(_chargingStartTime!) && logTime.isBefore(endTime);
|
||||
}
|
||||
|
||||
// ---------- 🟢 核心:系统API只触发充电状态,不设类型 ----------
|
||||
// ---------- 核心:系统API定调 ----------
|
||||
void _applySystemChargeState(bool isCharging, String systemChargeType) {
|
||||
dev.log(
|
||||
'[_applySystemChargeState] isCharging: $isCharging, systemChargeType: $systemChargeType (忽略,只用来触发状态)',
|
||||
'[_applySystemChargeState] isCharging: $isCharging, systemChargeType: $systemChargeType',
|
||||
name: 'BatteryHealth',
|
||||
);
|
||||
|
||||
@@ -252,16 +263,48 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
|
||||
_chargeVolt = 0.0;
|
||||
_chargeCurr = 0.0;
|
||||
_wiredLevel = null;
|
||||
_chargingStartTime = null; // 🟢 清空开始时间
|
||||
_updateChargeState();
|
||||
return;
|
||||
}
|
||||
|
||||
// 🟢 不再根据 systemChargeType 设定类型,统一显示"识别中..."
|
||||
// 类型完全由日志解析决定
|
||||
if (_chargeType.isEmpty) {
|
||||
_chargeType = ''; // 保持空,让日志解析来填充
|
||||
// 🟢 1.3.0-dev.3: 记录充电开始时刻(仅当从断开状态变为充电状态时)
|
||||
if (_chargingStartTime == null) {
|
||||
_chargingStartTime = DateTime.now().toUtc();
|
||||
dev.log(
|
||||
'[_applySystemChargeState] 充电开始时间: $_chargingStartTime',
|
||||
name: 'BatteryHealth',
|
||||
);
|
||||
}
|
||||
|
||||
if (systemChargeType == "WIRELESS") {
|
||||
dev.log('[_applySystemChargeState] 无线充电模式', name: 'BatteryHealth');
|
||||
_isWireless = true;
|
||||
_chargeType = "无线";
|
||||
_wiredLevel = null;
|
||||
} else if (systemChargeType == "AC") {
|
||||
dev.log(
|
||||
'[_applySystemChargeState] 有线快充模式 (AC/PPS)',
|
||||
name: 'BatteryHealth',
|
||||
);
|
||||
_isWireless = false;
|
||||
_chargeType = "PPS";
|
||||
} else if (systemChargeType == "USB") {
|
||||
dev.log(
|
||||
'[_applySystemChargeState] 有线慢充模式 (USB/SDP)',
|
||||
name: 'BatteryHealth',
|
||||
);
|
||||
_isWireless = false;
|
||||
_chargeType = "SDP";
|
||||
} else {
|
||||
// UNKNOWN 时清空旧类型,等待日志修正
|
||||
dev.log(
|
||||
'[_applySystemChargeState] ⚠️ 未知类型: $systemChargeType,清空类型等待日志',
|
||||
name: 'BatteryHealth',
|
||||
);
|
||||
_isWireless = false;
|
||||
_chargeType = '';
|
||||
}
|
||||
_isWireless = false; // 重置无线标志,等待日志确认
|
||||
|
||||
_chargePower = 0.0;
|
||||
_chargeVolt = 0.0;
|
||||
@@ -269,13 +312,12 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
|
||||
_updateChargeState();
|
||||
});
|
||||
|
||||
// 🟢 插入充电器后立即读取日志
|
||||
if (_isCharging) {
|
||||
_fetchChargeDataFromLog();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 🟢 日志解析:唯一的数据源 ----------
|
||||
// ---------- 日志解析 ----------
|
||||
void _parseChargeDataFromLog(List<String> lines) {
|
||||
dev.log(
|
||||
'[_parseChargeDataFromLog] 开始解析,共 ${lines.length} 行',
|
||||
@@ -367,15 +409,16 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
|
||||
_chargeVolt = vbus / 1000000.0;
|
||||
_chargeCurr = ibus / 1000000.0;
|
||||
|
||||
// 有线模式
|
||||
_isWireless = false;
|
||||
|
||||
// 🟢 1.3.0-dev.2: real_type 优先级最高
|
||||
if (realType != null && realType == "PPS") {
|
||||
_chargeType = "PPS";
|
||||
} else if (level >= 3) {
|
||||
_chargeType = "PPS";
|
||||
} else if (realType != null && realType == "SDP") {
|
||||
} else if (realType != null &&
|
||||
(realType == "SDP" || realType == "FLOAT")) {
|
||||
_chargeType = "SDP";
|
||||
} else if (level >= 3 && _chargeType != "PPS") {
|
||||
_chargeType = "PPS";
|
||||
} else if (_chargeType.isEmpty) {
|
||||
_chargeType = "SDP";
|
||||
}
|
||||
@@ -385,8 +428,9 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
|
||||
} else {
|
||||
_wiredLevel = null;
|
||||
}
|
||||
|
||||
dev.log(
|
||||
'[_parseChargeDataFromLog] 有线更新: $_chargeType, ${_chargePower.toStringAsFixed(2)}W',
|
||||
'[_parseChargeDataFromLog] 有线更新: $_chargeType, ${_chargePower.toStringAsFixed(2)}W, level: $level',
|
||||
name: 'BatteryHealth',
|
||||
);
|
||||
});
|
||||
@@ -394,11 +438,25 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
|
||||
}
|
||||
}
|
||||
|
||||
// 🟢 1.3.0-dev.3: 增加超时保护
|
||||
Future<void> _fetchChargeDataFromLog() async {
|
||||
if (!_isCharging) {
|
||||
dev.log('[_fetchChargeDataFromLog] 未充电,跳过读取', name: 'BatteryHealth');
|
||||
return;
|
||||
}
|
||||
|
||||
// 超时检查:如果插入超过 5 分钟还没读到数据,不再频繁报错
|
||||
if (_chargingStartTime != null) {
|
||||
Duration elapsed = DateTime.now().toUtc().difference(_chargingStartTime!);
|
||||
if (elapsed.inMinutes > 5) {
|
||||
dev.log(
|
||||
'[_fetchChargeDataFromLog] 充电开始已超过 5 分钟,跳过本次轮询',
|
||||
name: 'BatteryHealth',
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
String fullPath = _filePath + getTodayFileName();
|
||||
dev.log('[_fetchChargeDataFromLog] 尝试读取: $fullPath', name: 'BatteryHealth');
|
||||
|
||||
@@ -636,7 +694,6 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
|
||||
_applySystemChargeState(isCharging, systemType);
|
||||
});
|
||||
|
||||
// 🟢 轮询间隔缩短为 15 秒
|
||||
_pollTimer = Timer.periodic(const Duration(seconds: 15), (timer) {
|
||||
if (_isCharging) {
|
||||
_fetchChargeDataFromLog();
|
||||
@@ -738,10 +795,13 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
|
||||
children: [
|
||||
Text(
|
||||
healthPercent.toStringAsFixed(1),
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 40,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.0,
|
||||
color: healthPercent < 70
|
||||
? Colors.red
|
||||
: Colors.black, // 🟢 动态颜色
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
@@ -751,7 +811,9 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w300,
|
||||
height: 1.0,
|
||||
color: Colors.grey.shade600,
|
||||
color: healthPercent < 70
|
||||
? Colors.red
|
||||
: Colors.grey.shade600, // 🟢 动态颜色
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -982,10 +1044,11 @@ class _BatteryHealthPageState extends State<BatteryHealthPage>
|
||||
if (value > 0) {
|
||||
if (value >= 80) {
|
||||
barColor = kHealthGreen;
|
||||
} else if (value >= 60)
|
||||
} else if (value >= 70) {
|
||||
barColor = kHealthYellow;
|
||||
else
|
||||
barColor = kHealthOrange;
|
||||
} else {
|
||||
barColor = Colors.red; // 🟢 小于 70 显示红色
|
||||
}
|
||||
} else {
|
||||
barColor = Colors.grey.shade300;
|
||||
}
|
||||
|
||||
@@ -11,14 +11,14 @@ class ChargeDetailPage extends StatelessWidget {
|
||||
final int? wiredLevel;
|
||||
|
||||
const ChargeDetailPage({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.chargeType,
|
||||
required this.isWireless,
|
||||
required this.power,
|
||||
required this.volt,
|
||||
required this.curr,
|
||||
this.wiredLevel,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
String _getDisplayPower() {
|
||||
if (isWireless) {
|
||||
|
||||
+1
-1
@@ -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.2.0
|
||||
version: 1.3.0
|
||||
|
||||
environment:
|
||||
sdk: ^3.9.2
|
||||
|
||||
Reference in New Issue
Block a user