diff --git a/buildApk.py b/buildApk.py index 1379289..a86ec3a 100644 --- a/buildApk.py +++ b/buildApk.py @@ -160,31 +160,32 @@ def process(): shutil.copyfile(UNI_SERIAL_FILE, bakUniSerialDart) shutil.copyfile(GLOBAL_FILE, bakGlobalsDart) - #开始修改文件 - version, foundFlag = modifyPubspecYaml() - if not version: - print('\nVersion string not found in pubspec.yaml\n') - #恢复备份文件 + #闭包函数,恢复备份文件 + def restoreBakFiles(): os.remove(PUB_YAML_FILE) os.remove(UNI_SERIAL_FILE) os.remove(GLOBAL_FILE) shutil.copyfile(bakPubspecYaml, PUB_YAML_FILE) shutil.copyfile(bakUniSerialDart, UNI_SERIAL_FILE) shutil.copyfile(bakGlobalsDart, GLOBAL_FILE) - shutil.rmtree(bakDir) + try: + shutil.rmtree(bakDir) + except: + pass + + #开始修改文件 + version, foundFlag = modifyPubspecYaml() + if not version: + print('\nVersion string not found in pubspec.yaml\n') + #恢复备份文件 + restoreBakFiles() os.system('pause') return ok = input('\nVersion found [{}]\n\nCorrect?[y/n]'.format(version)) if ok.lower() not in ('', 'y', 'yes', 'ok'): #恢复备份文件 - os.remove(PUB_YAML_FILE) - os.remove(UNI_SERIAL_FILE) - os.remove(GLOBAL_FILE) - shutil.copyfile(bakPubspecYaml, PUB_YAML_FILE) - shutil.copyfile(bakUniSerialDart, UNI_SERIAL_FILE) - shutil.copyfile(bakGlobalsDart, GLOBAL_FILE) - shutil.rmtree(bakDir) + restoreBakFiles() return foundFlag |= modifyUniSerialDart() @@ -192,25 +193,13 @@ def process(): if (foundFlag != MODIFIED_ALL_MASK): print('foundFlag : 0x{:x} != 0x{:x}!'.format(foundFlag, MODIFIED_ALL_MASK)) #恢复备份文件 - os.remove(PUB_YAML_FILE) - os.remove(UNI_SERIAL_FILE) - os.remove(GLOBAL_FILE) - shutil.copyfile(bakPubspecYaml, PUB_YAML_FILE) - shutil.copyfile(bakUniSerialDart, UNI_SERIAL_FILE) - shutil.copyfile(bakGlobalsDart, GLOBAL_FILE) - shutil.rmtree(bakDir) + restoreBakFiles() return ok = input('\nPlease confirm modifications in pubspec.yaml/uni_serial.dart/globals.dart\n\nCorrect?[y/n]') if ok.lower() not in ('', 'y', 'yes', 'ok'): #恢复备份文件 - os.remove(PUB_YAML_FILE) - os.remove(UNI_SERIAL_FILE) - os.remove(GLOBAL_FILE) - shutil.copyfile(bakPubspecYaml, PUB_YAML_FILE) - shutil.copyfile(bakUniSerialDart, UNI_SERIAL_FILE) - shutil.copyfile(bakGlobalsDart, GLOBAL_FILE) - shutil.rmtree(bakDir) + restoreBakFiles() return @@ -234,17 +223,7 @@ def process(): print("\n\nCopy apk file failed: {}\n\n".format(ste(e))) #恢复备份文件 - os.remove(PUB_YAML_FILE) - os.remove(UNI_SERIAL_FILE) - os.remove(GLOBAL_FILE) - shutil.copyfile(bakPubspecYaml, PUB_YAML_FILE) - shutil.copyfile(bakUniSerialDart, UNI_SERIAL_FILE) - shutil.copyfile(bakGlobalsDart, GLOBAL_FILE) - try: - shutil.rmtree(bakDir) - except: - #print("Delete backup directory [{}] failed:".format(bakDir)) - pass + restoreBakFiles() elaspsedTime = datetime.datetime.now() - startTime print('\nExecution time : {}\n'.format(elaspsedTime)) diff --git a/buildWindows.py b/buildWindows.py index b9125b8..de53491 100644 --- a/buildWindows.py +++ b/buildWindows.py @@ -158,31 +158,32 @@ def process(): shutil.copyfile(UNI_SERIAL_FILE, bakUniSerialDart) shutil.copyfile(GLOBAL_FILE, bakGlobalsDart) - #开始修改文件 - version, foundFlag = modifyPubspecYaml() - if not version: - print('\nVersion string not found in pubspec.yaml\n') - #恢复备份文件 + #闭包函数,恢复备份文件 + def restoreBakFiles(): os.remove(PUB_YAML_FILE) os.remove(UNI_SERIAL_FILE) os.remove(GLOBAL_FILE) shutil.copyfile(bakPubspecYaml, PUB_YAML_FILE) shutil.copyfile(bakUniSerialDart, UNI_SERIAL_FILE) shutil.copyfile(bakGlobalsDart, GLOBAL_FILE) - shutil.rmtree(bakDir) + try: + shutil.rmtree(bakDir) + except: + pass + + #开始修改文件 + version, foundFlag = modifyPubspecYaml() + if not version: + print('\nVersion string not found in pubspec.yaml\n') + #恢复备份文件 + restoreBakFiles() os.system('pause') return ok = input('\nVersion found [{}]\n\nCorrect?[y/n]'.format(version)) if ok.lower() not in ('', 'y', 'yes', 'ok'): #恢复备份文件 - os.remove(PUB_YAML_FILE) - os.remove(UNI_SERIAL_FILE) - os.remove(GLOBAL_FILE) - shutil.copyfile(bakPubspecYaml, PUB_YAML_FILE) - shutil.copyfile(bakUniSerialDart, UNI_SERIAL_FILE) - shutil.copyfile(bakGlobalsDart, GLOBAL_FILE) - shutil.rmtree(bakDir) + restoreBakFiles() return foundFlag |= modifyUniSerialDart() @@ -190,25 +191,13 @@ def process(): if (foundFlag != MODIFIED_ALL_MASK): print('foundFlag : 0x{:x} != 0x{:x}!'.format(foundFlag, MODIFIED_ALL_MASK)) #恢复备份文件 - os.remove(PUB_YAML_FILE) - os.remove(UNI_SERIAL_FILE) - os.remove(GLOBAL_FILE) - shutil.copyfile(bakPubspecYaml, PUB_YAML_FILE) - shutil.copyfile(bakUniSerialDart, UNI_SERIAL_FILE) - shutil.copyfile(bakGlobalsDart, GLOBAL_FILE) - shutil.rmtree(bakDir) + restoreBakFiles() return ok = input('\nPlease confirm modifications in pubspec.yaml/uni_serial.dart/globals.dart\n\nCorrect?[y/n]') if ok.lower() not in ('', 'y', 'yes', 'ok'): #恢复备份文件 - os.remove(PUB_YAML_FILE) - os.remove(UNI_SERIAL_FILE) - os.remove(GLOBAL_FILE) - shutil.copyfile(bakPubspecYaml, PUB_YAML_FILE) - shutil.copyfile(bakUniSerialDart, UNI_SERIAL_FILE) - shutil.copyfile(bakGlobalsDart, GLOBAL_FILE) - shutil.rmtree(bakDir) + restoreBakFiles() return @@ -232,17 +221,7 @@ def process(): print("\n\nCompress release directory failed: {}\n\n".format(ste(e))) #恢复备份文件 - os.remove(PUB_YAML_FILE) - os.remove(UNI_SERIAL_FILE) - os.remove(GLOBAL_FILE) - shutil.copyfile(bakPubspecYaml, PUB_YAML_FILE) - shutil.copyfile(bakUniSerialDart, UNI_SERIAL_FILE) - shutil.copyfile(bakGlobalsDart, GLOBAL_FILE) - try: - shutil.rmtree(bakDir) - except: - #print("Cannot delete backup directory {}".format(bakDir)) - pass + restoreBakFiles() elaspsedTime = datetime.datetime.now() - startTime print('\nExecution time : {}\n'.format(elaspsedTime)) diff --git a/changelog.md b/changelog.md index 4bfe617..6aaf162 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,8 @@ +# 1.2.1 +1. 双击曲线区域可以查看每次放电的统计信息 +2. 配合下位机V6.31,将功率单位修改为10毫瓦(显示小数点后两位) +3. 配合下位机V6.31,将恒阻模式的电阻单位修改为10毫欧 + # 1.2.0 1. 增加 "测试电源输出能力" 测试项 2. 增加 "测试电源短路保护能力" 测试项 diff --git a/lib/common/globals.dart b/lib/common/globals.dart index 861942c..c01a118 100644 --- a/lib/common/globals.dart +++ b/lib/common/globals.dart @@ -27,7 +27,7 @@ enum KeepScreenOption { ///小部分为程序共用的变量 class Global { //版本号注意需要使用单引号,让buildXXX.py能找的到 - static const version = '1.2.0'; + static const version = '1.2.1'; static const buildNumber = ""; static late final SharedPreferences prefs; diff --git a/lib/common/when.dart b/lib/common/when.dart index 61d79ba..ddbe12d 100644 --- a/lib/common/when.dart +++ b/lib/common/when.dart @@ -8,6 +8,17 @@ import 'bisect.dart'; import '../i18n/common.i18n.dart'; +extension WhenDuration on Duration { + ///将Duration转换为HH:MM:SS格式的字符串 + String toTimeString() { + String twoDigits(int n) => n.toString().padLeft(2, "0"); + String twoDigitMinutes = twoDigits(inMinutes.remainder(60)); + String twoDigitSeconds = twoDigits(inSeconds.remainder(60)); + return "${twoDigits(inHours)}:$twoDigitMinutes:$twoDigitSeconds"; + } +} + + extension When on DateTime { ///快捷函数,返回: yyyy-mm-dd HH:MM:SS格式 String toStdString() => format('yyyy-mm-dd HH:MM:SS'); diff --git a/lib/i18n/load_stats_page.i18n.dart b/lib/i18n/load_stats_page.i18n.dart new file mode 100644 index 0000000..7cf550d --- /dev/null +++ b/lib/i18n/load_stats_page.i18n.dart @@ -0,0 +1,175 @@ +/// 放电统计信息页面的国际化翻译文件 +import 'package:i18n_extension/i18n_extension.dart'; + +extension Localization on String { + static var t = Translations("en_us") + + { + "en_us": "Load Stats", + "zh_cn": "放电统计信息", + } + + { + "en_us": "Export", + "zh_cn": "导出", + } + + { + "en_us": "No data", + "zh_cn": "没有数据", + } + + { + "en_us": "Click to add remark", + "zh_cn": "点击添加备注", + } + + { + "en_us": "Input a remark", + "zh_cn": "输入备注", + } + + { + "en_us": "Initial V", + "zh_cn": "初始电压", + } + + { + "en_us": "End V", + "zh_cn": "结束电压", + } + + { + "en_us": "Average V", + "zh_cn": "平均电压", + } + + { + "en_us": "Initial I", + "zh_cn": "初始电流", + } + + { + "en_us": "End I", + "zh_cn": "结束电流", + } + + { + "en_us": "Average I", + "zh_cn": "平均电流", + } + + { + "en_us": "Initial Ah", + "zh_cn": "初始安时", + } + + { + "en_us": "Ah", + "zh_cn": "本次安时", + } + + { + "en_us": "Total Ah", + "zh_cn": "总安时", + } + + { + "en_us": "Initial Wh", + "zh_cn": "初始瓦时", + } + + { + "en_us": "Wh", + "zh_cn": "本次瓦时", + } + + { + "en_us": "Total Wh", + "zh_cn": "总瓦时", + } + + { + "en_us": "Mode", + "zh_cn": "放电模式", + } + + { + "en_us": "CC", + "zh_cn": "恒流", + } + + { + "en_us": "CR", + "zh_cn": "恒阻", + } + + { + "en_us": "CP", + "zh_cn": "恒功率", + } + + { + "en_us": "CR value", + "zh_cn": "恒阻值", + } + + { + "en_us": "CP value", + "zh_cn": "恒功率值", + } + + { + "en_us": "Rd", + "zh_cn": "直流内阻", + } + + { + "en_us": "Ra", + "zh_cn": "交流内阻", + } + + { + "en_us": "Head sink", + "zh_cn": "散热器温度", + } + + { + "en_us": "Board", + "zh_cn": "主板温度", + } + + { + "en_us": "Start time", + "zh_cn": "开始时间", + } + + { + "en_us": "End time", + "zh_cn": "结束时间", + } + + { + "en_us": "Duration", + "zh_cn": "持续时间", + } + + { + "en_us": "Export load stats data", + "zh_cn": "导出放电统计数据", + } + + { + "en_us": "Export folder", + "zh_cn": "导出目录", + } + + { + "en_us": "Select a folder to save", + "zh_cn": "选择一个保存目录", + } + + { + "en_us": "File name", + "zh_cn": "文件名", + } + + { + "en_us": "Enter a name to save", + "zh_cn": "输入一个保存文件名", + } + + { + "en_us": "Success", + "zh_cn": "成功", + } + + { + "en_us": "Export load stats file success", + "zh_cn": "导出放电统计信息文件成功", + } + + { + "en_us": "Item", + "zh_cn": "条目", + } + + { + "en_us": "Value", + "zh_cn": "数值", + } + + { + "en_us": "Unit", + "zh_cn": "单位", + } + + { + "en_us": "Remark", + "zh_cn": "备注", + }; + + String get i18n => localize(this, t); + + //"Hello %s, this is %s".i18n.fill(["John", "Mary"]) + String fill(List params) => localizeFill(this, params); +} diff --git a/lib/i18n/main_page.i18n.dart b/lib/i18n/main_page.i18n.dart index c162c03..f5b69ca 100644 --- a/lib/i18n/main_page.i18n.dart +++ b/lib/i18n/main_page.i18n.dart @@ -84,12 +84,16 @@ extension Localization on String { "zh_cn": "清除容量?", } + { - "en_us": "Clear curva data?", - "zh_cn": "清除曲线数据?", + "en_us": "Clear curva data", + "zh_cn": "清除曲线数据", } + { - "en_us": "Turn on the electronic load?", - "zh_cn": "打开负载开始放电?", + "en_us": "Show load stats", + "zh_cn": "显示放电统计信息", + } + + { + "en_us": "Turn on the electronic load?\nRemember to clear Ah, if needed", + "zh_cn": "打开负载开始放电?\n如果需要, 记得要先清零安时容量", } + { "en_us": "Turn off the electronic load?", diff --git a/lib/i18n/switch_mode.i18n.dart b/lib/i18n/switch_mode.i18n.dart index e28bb06..bf860b4 100644 --- a/lib/i18n/switch_mode.i18n.dart +++ b/lib/i18n/switch_mode.i18n.dart @@ -36,12 +36,12 @@ extension Localization on String { "zh_cn": "错误", } + { - "en_us": "Resistance must be greater than zero ohm and less than 65 ohms", - "zh_cn": "阻值必须大于零欧姆并小于65欧姆", + "en_us": "Resistance must be greater than zero ohm and less than 655 ohms", + "zh_cn": "阻值必须大于0欧姆并小于655欧姆", } + { - "en_us": "Power must be greater than zero watt and less than 6553 watts", - "zh_cn": "功率值必须大于零瓦并小于6553瓦", + "en_us": "Power must be greater than zero watt and less than 650 watts", + "zh_cn": "功率值必须大于0瓦并小于650瓦", } + { "en_us": "Success", diff --git a/lib/load_stats_page.dart b/lib/load_stats_page.dart new file mode 100644 index 0000000..73b8b6a --- /dev/null +++ b/lib/load_stats_page.dart @@ -0,0 +1,421 @@ +/// m328v6数控电子负载上位机 +/// 查看每次放电的统计信息 +/// Author: cdhigh +/// +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as p; +import 'package:syncfusion_flutter_xlsio/xlsio.dart' as xlsio; +import 'package:file_picker/file_picker.dart'; +import 'i18n/load_stats_page.i18n.dart'; +import 'common/when.dart'; +import 'common/iconfont.dart'; +import 'widgets/modal_dialogs.dart'; +import 'models/load_stats_model.dart'; +export 'models/load_stats_model.dart'; + +///传入参数为每次放电的统计列表 +class LoadStatsPage extends ConsumerStatefulWidget { + final Map> loadStats; + const LoadStatsPage({Key? key, required this.loadStats}) : super(key: key); + @override + _LoadStatsPageState createState() => _LoadStatsPageState(); +} + +class _LoadStatsPageState extends ConsumerState { + //final _delayOnHourCtrl = TextEditingController(text: "00"); + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Load Stats'.i18n)), + body: buildMainList(context), + ); + } + + ///构建页面主体的ListView + Widget buildMainList(BuildContext context) { + final loadStatsWidgets = List.from(widget.loadStats["stats"]?.map((e) { + return ExpansionTile(title: Text(e.remark.isEmpty ? e.startTime.toStdString() + " - " + (e.endTime?.toStdString() ?? "") : e.remark,), + expandedAlignment: Alignment.topLeft, + expandedCrossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector(child: Padding(padding: const EdgeInsets.all(10), child: + Text(e.remark.isNotEmpty ? e.remark : "Click to add remark".i18n, + style: const TextStyle(fontWeight: FontWeight.bold,),),), + onTap: () async { + final ret = await showInputDialog(context: context, title: "Input a remark".i18n, initialText: e.remark); + if (ret != null) { + setState(() {e.remark = ret;}); + } + }, + onDoubleTap: () async { + final ret = await showInputDialog(context: context, title: "Input a remark".i18n, initialText: e.remark); + if (ret != null) { + setState(() {e.remark = ret;}); + } + }, + ), + Padding(padding: const EdgeInsets.only(left: 10, right: 10, bottom: 10), child: SingleLoadStats(stats: e))]); + }) ?? [Text("No data".i18n)]); + + return Container(padding: const EdgeInsets.all(10.0), child: ListView( + shrinkWrap: true, + children: [ + Padding(padding: const EdgeInsets.all(10), child: + Row(children: [ + SizedBox(width: 200, child: ElevatedButton( + onPressed: () {Navigator.pushNamed(context, "/export_stats", arguments: widget.loadStats,);}, + child: Text("Export".i18n),),), + const Expanded(child: Text("")), + ]),), + ...loadStatsWidgets])); + } +} + +///每个单独的放电统计信息widget +class SingleLoadStats extends StatelessWidget { + final LoadStatsModel stats; + + const SingleLoadStats({Key? key, required this.stats}) : super(key: key); + + @override + Widget build(BuildContext context) { + const leftTextWidth = 150.0; + return ListView(shrinkWrap: true, children: [ + const Divider(), + Padding(padding: const EdgeInsets.symmetric(vertical: 5), child: Row(children:[ + SizedBox(width: leftTextWidth, child: SelectableText("Initial V".i18n),), + Expanded(child: SelectableText(stats.initialV.toStringAsFixed(3) + 'V')),]),), + const Divider(), + Padding(padding: const EdgeInsets.symmetric(vertical: 5), child: Row(children:[ + SizedBox(width: leftTextWidth, child: SelectableText("End V".i18n),), + Expanded(child: SelectableText(stats.endV.toStringAsFixed(3) + 'V')),]),), + const Divider(), + Padding(padding: const EdgeInsets.symmetric(vertical: 5), child: Row(children:[ + SizedBox(width: leftTextWidth, child: SelectableText("Average V".i18n),), + Expanded(child: SelectableText(stats.avgV.toStringAsFixed(3) + 'V')),]),), + const Divider(), + Padding(padding: const EdgeInsets.symmetric(vertical: 5), child: Row(children:[ + SizedBox(width: leftTextWidth, child: SelectableText("Initial I".i18n),), + Expanded(child: SelectableText((stats.initialI ?? 0.0).toStringAsFixed(3) + 'A')),]),), + const Divider(), + Padding(padding: const EdgeInsets.symmetric(vertical: 5), child: Row(children:[ + SizedBox(width: leftTextWidth, child: SelectableText("End I".i18n),), + Expanded(child: SelectableText(stats.endI.toStringAsFixed(3) + 'A')),]),), + const Divider(), + Padding(padding: const EdgeInsets.symmetric(vertical: 5), child: Row(children:[ + SizedBox(width: leftTextWidth, child: SelectableText("Average I".i18n),), + Expanded(child: SelectableText(stats.avgI.toStringAsFixed(3) + 'A')),]),), + const Divider(), + Padding(padding: const EdgeInsets.symmetric(vertical: 5), child: Row(children:[ + SizedBox(width: leftTextWidth, child: SelectableText("Initial Ah".i18n),), + Expanded(child: SelectableText(stats.initialAh.toStringAsFixed(3) + 'Ah')),]),), + const Divider(), + Padding(padding: const EdgeInsets.symmetric(vertical: 5), child: Row(children:[ + SizedBox(width: leftTextWidth, child: SelectableText("Ah".i18n),), + Expanded(child: SelectableText(stats.ah.toStringAsFixed(3) + 'Ah')),]),), + const Divider(), + Padding(padding: const EdgeInsets.symmetric(vertical: 5), child: Row(children:[ + SizedBox(width: leftTextWidth, child: SelectableText("Total Ah".i18n),), + Expanded(child: SelectableText(stats.totalAh.toStringAsFixed(3) + 'Ah')),]),), + const Divider(), + Padding(padding: const EdgeInsets.symmetric(vertical: 5), child: Row(children:[ + SizedBox(width: leftTextWidth, child: SelectableText("Initial Wh".i18n),), + Expanded(child: SelectableText((stats.initialWh ?? 0.0).toStringAsFixed(2) + 'Wh')),]),), + const Divider(), + Padding(padding: const EdgeInsets.symmetric(vertical: 5), child: Row(children:[ + SizedBox(width: leftTextWidth, child: SelectableText("Wh".i18n),), + Expanded(child: SelectableText(stats.wh.toStringAsFixed(2) + 'Wh')),]),), + const Divider(), + Padding(padding: const EdgeInsets.symmetric(vertical: 5), child: Row(children:[ + SizedBox(width: leftTextWidth, child: SelectableText("Total Wh".i18n),), + Expanded(child: SelectableText(stats.totalWh.toStringAsFixed(2) + 'Wh')),]),), + const Divider(), + Padding(padding: const EdgeInsets.symmetric(vertical: 5), child: Row(children:[ + SizedBox(width: leftTextWidth, child: SelectableText("Mode".i18n),), + Expanded(child: SelectableText(stats.mode.i18n)),]),), + const Divider(), + if (stats.mode == "CR") Padding(padding: const EdgeInsets.symmetric(vertical: 5), child: Row(children:[ + SizedBox(width: leftTextWidth, child: SelectableText("CR value".i18n),), + Expanded(child: SelectableText(stats.rSet.toStringAsFixed(2) + 'Ohm')),]),), + if (stats.mode == "CR") const Divider(), + if (stats.mode == "CP") Padding(padding: const EdgeInsets.symmetric(vertical: 5), child: Row(children:[ + SizedBox(width: leftTextWidth, child: SelectableText("CP value".i18n),), + Expanded(child: SelectableText(stats.pSet.toStringAsFixed(2) + 'W')),]),), + if (stats.mode == "CP") const Divider(), + Padding(padding: const EdgeInsets.symmetric(vertical: 5), child: Row(children:[ + SizedBox(width: leftTextWidth, child: SelectableText("Rd".i18n),), + Expanded(child: SelectableText(stats.rd.toStringAsFixed(3) + 'Ohm')),]),), + const Divider(), + Padding(padding: const EdgeInsets.symmetric(vertical: 5), child: Row(children:[ + SizedBox(width: leftTextWidth, child: SelectableText("Ra".i18n),), + Expanded(child: SelectableText(stats.ra.toStringAsFixed(3) + 'Ohm')),]),), + const Divider(), + Padding(padding: const EdgeInsets.symmetric(vertical: 5), child: Row(children:[ + SizedBox(width: leftTextWidth, child: SelectableText("Head sink".i18n),), + Expanded(child: SelectableText(stats.temperature1.toString() + 'C')),]),), + const Divider(), + Padding(padding: const EdgeInsets.symmetric(vertical: 5), child: Row(children:[ + SizedBox(width: leftTextWidth, child: SelectableText("Board".i18n),), + Expanded(child: SelectableText(stats.temperature2.toString() + 'C')),]),), + const Divider(), + Padding(padding: const EdgeInsets.symmetric(vertical: 5), child: Row(children:[ + SizedBox(width: leftTextWidth, child: SelectableText("Start time".i18n),), + Expanded(child: SelectableText(stats.startTime.toStdString())),]),), + const Divider(), + Padding(padding: const EdgeInsets.symmetric(vertical: 5), child: Row(children:[ + SizedBox(width: leftTextWidth, child: SelectableText("End time".i18n),), + Expanded(child: SelectableText(stats.endTime?.toStdString() ?? "")),]),), + const Divider(), + Padding(padding: const EdgeInsets.symmetric(vertical: 5), child: Row(children:[ + SizedBox(width: leftTextWidth, child: SelectableText("Duration".i18n),), + Expanded(child: SelectableText( + (stats.endTime != null) ? stats.loadTime.toTimeString() : DateTime.now().difference(stats.startTime).toTimeString(),),),]),), + ]); + } +} + +///////////////////////////////////////////////////////////// +///导出放电统计数据页面,基本上是export_data页面的拷贝,以后再优化吧 +class ExportLoadStatsPage extends ConsumerStatefulWidget { + final Map> loadStats; + const ExportLoadStatsPage({Key? key, required this.loadStats}) : super(key: key); + @override + _ExportLoadStatsPageState createState() => _ExportLoadStatsPageState(); +} + +class _ExportLoadStatsPageState extends ConsumerState { + final _folderCtrller = TextEditingController(); + final _nameCtrller = TextEditingController(); + + @override + void initState() { + super.initState(); + _nameCtrller.text = "load_stats_" + DateTime.now().format('yyyymmdd_HHMMSS') + ".xlsx"; + Future.delayed(const Duration(milliseconds: 500)).then(requestPermission); //延时确认权限并获取路径 + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Export load stats data'.i18n)), + body: buildMainList(context), + ); + } + + ///构建页面主体的ListView + Widget buildMainList(BuildContext context) { + bool btnEnabled = widget.loadStats["stats"]?.isNotEmpty ?? false; + + return Container(padding: const EdgeInsets.all(10), child: + Column(crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding(padding: const EdgeInsets.only(top: 5), child: Text("Export folder".i18n),), + Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Expanded(child: TextField(controller: _folderCtrller, + onChanged: (_) {setState((){});}, + decoration: InputDecoration( + labelText: _folderCtrller.text.isNotEmpty ? null : "Select a folder to save".i18n, + prefixIcon: const Icon(Icons.folder_open)), + ),), + IconButton(icon: const Icon(IconFont.dot3), onPressed: () async { + final ret = await FilePicker.platform.getDirectoryPath( + dialogTitle: "Choose a directory", initialDirectory: _folderCtrller.text).catchError((e){debugPrint(e.toString());}); + if (ret != null) { + setState(() {_folderCtrller.text = ret;}); + } + }),],), + Padding(padding: const EdgeInsets.only(top: 5), child: Text("File name".i18n),), + TextField(controller: _nameCtrller, + onChanged: (_) {setState((){});}, + decoration: InputDecoration( + labelText: _nameCtrller.text.isNotEmpty ? null : "Enter a name to save".i18n, + prefixIcon: const Icon(Icons.file_copy_outlined)), + ), + Padding(padding: const EdgeInsets.all(20), child: + ConstrainedBox(constraints: const BoxConstraints(minWidth: 100, maxWidth: 300), + child: ElevatedButton( + onPressed: btnEnabled ? doExport : null, + child: Text("Export".i18n)), + ),), + ]),); + } + + ///开始导出 + void doExport() { + final loadStats = widget.loadStats["stats"]; + if ((loadStats == null) || (loadStats.isEmpty)) { + return; + } + + final fileName = p.join(_folderCtrller.text, _nameCtrller.text); + + //生成XLSX实例 + final workbook = xlsio.Workbook(loadStats.length); + var wsIdx = 0; + for (final stats in loadStats) { + final sheet = workbook.worksheets[wsIdx]; + //XLSX名字不允许特殊字符 + final remark = stats.remark.replaceAll(":", "-").replaceAll(r"\", "-").replaceAll("/", "-").replaceAll(":", "-"); + sheet.name = remark.isEmpty ? stats.startTime.format('yyyy-mm-dd HH-MM-SS') : remark; + var row = 1; + //填充数据 + sheet.getRangeByName('A$row').setText('Item'.i18n); + sheet.getRangeByName('B$row').setText('Value'.i18n); + sheet.getRangeByName('C$row').setText('Unit'.i18n); + row++; + sheet.getRangeByName('A$row').setText('Initial V'.i18n); + sheet.getRangeByName('B$row').setNumber(stats.initialV); + sheet.getRangeByName('C$row').setText('V'); + row++; + sheet.getRangeByName('A$row').setText('End V'.i18n); + sheet.getRangeByName('B$row').setNumber(stats.endV); + sheet.getRangeByName('C$row').setText('V'); + row++; + sheet.getRangeByName('A$row').setText('Average V'.i18n); + sheet.getRangeByName('B$row').setNumber(stats.avgV); + sheet.getRangeByName('C$row').setText('V'); + row++; + sheet.getRangeByName('A$row').setText('Initial I'.i18n); + sheet.getRangeByName('B$row').setNumber(stats.initialI); + sheet.getRangeByName('C$row').setText('A'); + row++; + sheet.getRangeByName('A$row').setText('End I'.i18n); + sheet.getRangeByName('B$row').setNumber(stats.endI); + sheet.getRangeByName('C$row').setText('A'); + row++; + sheet.getRangeByName('A$row').setText('Average I'.i18n); + sheet.getRangeByName('B$row').setNumber(stats.avgI); + sheet.getRangeByName('C$row').setText('A'); + row++; + sheet.getRangeByName('A$row').setText('Initial Ah'.i18n); + sheet.getRangeByName('B$row').setNumber(stats.initialAh); + sheet.getRangeByName('C$row').setText('Ah'); + row++; + sheet.getRangeByName('A$row').setText('Ah'.i18n); + sheet.getRangeByName('B$row').setNumber(stats.ah); + sheet.getRangeByName('C$row').setText('Ah'); + row++; + sheet.getRangeByName('A$row').setText('Total Ah'.i18n); + sheet.getRangeByName('B$row').setNumber(stats.totalAh); + sheet.getRangeByName('C$row').setText('Ah'); + row++; + sheet.getRangeByName('A$row').setText('Initial Wh'.i18n); + sheet.getRangeByName('B$row').setNumber(stats.initialWh); + sheet.getRangeByName('C$row').setText('Wh'); + row++; + sheet.getRangeByName('A$row').setText('Wh'.i18n); + sheet.getRangeByName('B$row').setNumber(stats.wh); + sheet.getRangeByName('C$row').setText('Wh'); + row++; + sheet.getRangeByName('A$row').setText('Total Wh'.i18n); + sheet.getRangeByName('B$row').setNumber(stats.totalWh); + sheet.getRangeByName('C$row').setText('Wh'); + row++; + sheet.getRangeByName('A$row').setText('Mode'.i18n); + sheet.getRangeByName('B$row').setText(stats.mode.i18n); + sheet.getRangeByName('C$row').setText(''); + row++; + if (stats.mode == "CR") { + sheet.getRangeByName('A$row').setText('CR value'.i18n); + sheet.getRangeByName('B$row').setText(stats.rSet.toStringAsFixed(2)); + sheet.getRangeByName('C$row').setText('Ohm'); + row++; + } else if (stats.mode == "CP") { + sheet.getRangeByName('A$row').setText('CP value'.i18n); + sheet.getRangeByName('B$row').setText(stats.pSet.toStringAsFixed(2)); + sheet.getRangeByName('C$row').setText('W'); + row++; + } + sheet.getRangeByName('A$row').setText('Rd'.i18n); + sheet.getRangeByName('B$row').setNumber(stats.rd); + sheet.getRangeByName('C$row').setText('Ohm'); + row++; + sheet.getRangeByName('A$row').setText('Ra'.i18n); + sheet.getRangeByName('B$row').setNumber(stats.ra); + sheet.getRangeByName('C$row').setText('Ohm'); + row++; + sheet.getRangeByName('A$row').setText('Head sink'.i18n); + sheet.getRangeByName('B$row').setText(stats.temperature1.toString()); + sheet.getRangeByName('C$row').setText('C'); + row++; + sheet.getRangeByName('A$row').setText('Board'.i18n); + sheet.getRangeByName('B$row').setText(stats.temperature2.toString()); + sheet.getRangeByName('C$row').setText('C'); + row++; + sheet.getRangeByName('A$row').setText('Start time'.i18n); + sheet.getRangeByName('B$row').setText(stats.startTime.toStdString()); + sheet.getRangeByName('C$row').setText(''); + row++; + sheet.getRangeByName('A$row').setText('End time'.i18n); + sheet.getRangeByName('B$row').setText(stats.endTime?.toStdString() ?? ""); + sheet.getRangeByName('C$row').setText(''); + row++; + sheet.getRangeByName('A$row').setText('Duration'.i18n); + sheet.getRangeByName('B$row').setText( + (stats.endTime != null) ? stats.loadTime.toTimeString() : DateTime.now().difference(stats.startTime).toTimeString() + ); + sheet.getRangeByName('C$row').setText(''); + row++; + sheet.getRangeByName('A$row').setText('Remark'.i18n); + sheet.getRangeByName('B$row').setText(stats.remark); + sheet.getRangeByName('C$row').setText(''); + row++; + wsIdx++; + } + + //开始保存文件 + final List bytes = workbook.saveAsStream(); + workbook.dispose(); + File(fileName).writeAsBytes(bytes); + + showOkAlertDialog(context: context, title: "Success".i18n, content: Text("Export load stats file success".i18n)); + } + + ///确认权限,如果没有权限,提示需要申请 + void requestPermission([_]) async { + if (Platform.isAndroid || Platform.isIOS || Platform.isFuchsia) { + var status = await Permission.storage.status; + if (!status.isPermanentlyDenied && !status.isDenied) { //之前没有拒绝过 + await Permission.storage.request(); + } + } + + fetchDefaultExportDir(); + } + + ///根据系统不同返回默认的导出目录 + void fetchDefaultExportDir() async { + if (_folderCtrller.text.isNotEmpty) { + return; + } + + var ret = ""; + if (Platform.isAndroid || Platform.isFuchsia) { + //使用path_provider返回的Download目录在Android11上是应用内Download目录,外部无法存取 + //在这里使用一个通用的目录,如果以后Android又修改了,再考虑其他方案 + ret = '/storage/emulated/0/Download'; + } else if (Platform.isIOS) { + ret = (await getApplicationSupportDirectory()).path; + } else if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { + ret = (await getDownloadsDirectory())?.path ?? ""; + } else { + ret = ""; + } + setState((){_folderCtrller.text = ret;}); + } +} diff --git a/lib/m328v6_load.dart b/lib/m328v6_load.dart index 5c5b3e7..4ed7379 100644 --- a/lib/m328v6_load.dart +++ b/lib/m328v6_load.dart @@ -124,7 +124,7 @@ class M328v6Load { void switchToCR(double resistor) { final cmd = BytesBuilder(); cmd.add("^MR".codeUnits); - cmd.add((resistor * 1000).toInt().asUint8List()); //单位切换为下位机使用的毫欧 + cmd.add((resistor * 100).toInt().asUint8List()); //单位切换为下位机使用的10毫欧 cmd.addByte(r"$".codeUnitAt(0)); sendCmd(cmd.toBytes()); } @@ -134,7 +134,7 @@ class M328v6Load { void switchToCP(double power) { final cmd = BytesBuilder(); cmd.add("^MP".codeUnits); - cmd.add((power * 10).toInt().asUint8List()); //单位切换为下位机使用的100毫瓦 + cmd.add((power * 100).toInt().asUint8List()); //单位切换为下位机使用的10毫瓦 cmd.addByte(r"$".codeUnitAt(0)); sendCmd(cmd.toBytes()); } diff --git a/lib/main_page.dart b/lib/main_page.dart index 9ea5e43..2f9bf10 100644 --- a/lib/main_page.dart +++ b/lib/main_page.dart @@ -5,6 +5,7 @@ import 'dart:io' show Platform; import 'dart:async'; import 'dart:typed_data'; +import 'package:flutter/scheduler.dart'; import 'package:flutter/material.dart'; //import 'package:flutter/cupertino.dart'; import 'package:flutter_switch/flutter_switch.dart'; @@ -32,6 +33,7 @@ import 'widgets/colored_safe_area.dart'; import 'widgets/curva_chart.dart'; import 'version_update/version_check.dart'; import 'uni_serial.dart'; +import 'load_stats_page.dart'; class MainPage extends ConsumerStatefulWidget { const MainPage({Key? key}) : super(key: key); @@ -56,6 +58,8 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie //如果异常中断并且启用了“自动重连”选项,则每隔5s自动尝试重连一次 late final PausableTimer _timerForReconnect; DateTime _lostConnectionTime = DateTime(2022); //丢失连接时间,自动重连限时5分钟内 + final _loadStats = []; //每次放电的统计信息 + Offset _tapPosForCurvaDblTap = const Offset(0.0, 0.0); //保存曲线区域的鼠标位置,用于弹出菜单 @override bool get wantKeepAlive =>true; @@ -294,7 +298,9 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie return Container(padding: const EdgeInsets.all(10.0), color: Global.isDarkMode ? Colors.transparent : appInfo.homePageBackgroundColor, child: ListView(children: [ - GestureDetector(child: const CurvaChart(), onDoubleTap: onDoubleTapCurvaChart), + GestureDetector(child: const CurvaChart(), + onDoubleTap: onDoubleTapCurvaChart, + onDoubleTapDown: (TapDownDetails details) {_tapPosForCurvaDblTap = details.globalPosition;},), buildVISetDisplay(context), buildVIDisplay(context), buildOtherDisplayData(context), @@ -356,7 +362,7 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie Row(children: [ SevenSegmentWithSuperText( title: "Power".i18n, - value: rdProvider.powerIn.toStringAsFixed(1).padLeft(6), + value: rdProvider.powerIn.toStringAsFixed(2).padLeft(6), size: _segDisplaySize, color: Colors.amber, ), @@ -364,14 +370,14 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie if (mode == "CR") SevenSegmentWithSuperText( title: "CR".i18n, - value: rdProvider.rSet.toStringAsFixed(3).padLeft(6), + value: rdProvider.rSet.toStringAsFixed(2).padLeft(6), size: _segDisplaySize, color: Colors.amber, ), if (mode == "CP") SevenSegmentWithSuperText( title: "CP".i18n, - value: rdProvider.pSet.toStringAsFixed(3).padLeft(6), + value: rdProvider.pSet.toStringAsFixed(2).padLeft(6), size: _segDisplaySize, color: Colors.amber, ), @@ -388,7 +394,7 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie Expanded(child: Container()), SevenSegmentWithSuperText( title: "Energy".i18n, - value: rdProvider.wh.toStringAsFixed(1).padLeft(6), + value: rdProvider.wh.toStringAsFixed(2).padLeft(6), size: _segDisplaySize, color: Colors.cyan, ), @@ -437,7 +443,9 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ - GestureDetector(child: CurvaChartLandscape(scrWidth: _scrWidth), onDoubleTap: onDoubleTapCurvaChart), + GestureDetector(child: CurvaChartLandscape(scrWidth: _scrWidth), + onDoubleTap: onDoubleTapCurvaChart, + onDoubleTapDown: (TapDownDetails details) {_tapPosForCurvaDblTap = details.globalPosition;},), SingleChildScrollView(child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: buildRawDataDisplayLandscape(context), @@ -490,7 +498,7 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie Padding(padding: const EdgeInsets.only(top: 5, bottom: 5), child: Row(children: [ Padding(padding: const EdgeInsets.only(right: 20), child: Text("Power".i18n, style: const TextStyle(color: Colors.white))), SevenSegmentDisplay(size: 3, backgroundColor: Colors.transparent, - value: rdProvider.powerIn.toStringAsFixed(1).padLeft(6), + value: rdProvider.powerIn.toStringAsFixed(2).padLeft(6), segmentStyle: DefaultSegmentStyle(enabledColor: Colors.amber, disabledColor: Colors.amber.withOpacity(0.15),), )])), @@ -507,7 +515,7 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie Padding(padding: const EdgeInsets.symmetric(vertical: 5), child: Row(children: [ Padding(padding: const EdgeInsets.only(right: 20), child: Text("Energy".i18n, style: const TextStyle(color: Colors.white))), SevenSegmentDisplay(size: 3, backgroundColor: Colors.transparent, - value: rdProvider.wh.toStringAsFixed(1).padLeft(6), + value: rdProvider.wh.toStringAsFixed(2).padLeft(6), segmentStyle: DefaultSegmentStyle(enabledColor: Colors.cyan, disabledColor: Colors.cyan.withOpacity(0.15),), )])), @@ -532,7 +540,7 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie Padding(padding: const EdgeInsets.symmetric(vertical: 5), child: Row(children: [ Padding(padding: const EdgeInsets.only(right: 20), child: Text(mode.i18n, style: const TextStyle(color: Colors.white))), SevenSegmentDisplay(size: 3, backgroundColor: Colors.transparent, - value: (mode == "CR") ? rdProvider.rSet.toStringAsFixed(3).padLeft(6) : rdProvider.pSet.toStringAsFixed(1).padLeft(6), + value: (mode == "CR") ? rdProvider.rSet.toStringAsFixed(2).padLeft(6) : rdProvider.pSet.toStringAsFixed(2).padLeft(6), segmentStyle: DefaultSegmentStyle(enabledColor: Colors.amber, disabledColor: Colors.cyan.withOpacity(0.15),), )])), @@ -601,19 +609,39 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie } } - ///双击曲线区域弹出提问,是否需要清除曲线数据 + ///双击曲线区域弹出菜单 void onDoubleTapCurvaChart() async { - final bool? ok = await showOkCancelAlertDialog(context: context, title: "Confirm".i18n, content: Text("Clear curva data?".i18n)); - if (ok == true) { - final vhProvider = ref.read(Global.vHistoryProvider); - vhProvider.clear(); + final overlay = Overlay.of(context)?.context.findRenderObject(); + final size = Overlay.of(context)?.context.size; + if ((overlay == null) || (size == null)) { + return; } + + await showMenu(context: context, + elevation: 8.0, + position: RelativeRect.fromRect( + _tapPosForCurvaDblTap & const Size(40, 40), // smaller rect, the touch area + Offset.zero & size // Bigger rect, the entire screen + ), + items: [ + PopupMenuItem(child: Text("Clear curva data".i18n), + onTap: () {ref.watch(Global.vHistoryProvider).clear();}), + PopupMenuItem(child: Text("Show load stats".i18n), + enabled: _loadStats.isNotEmpty, + onTap: () { //菜单本身就是一个页面,所以需要等此页面关闭后才能打开另一个页面,使用 addPostFrameCallback + SchedulerBinding.instance?.addPostFrameCallback((_) { + Navigator.pushNamed(context, "/load_stats", + arguments: Map>.from({"stats": _loadStats}),); + }); + }), + ], + ); } ///点击标题栏上的Switch按钮,询问是否需要打开关闭放电 void onTapAppBarSwitch(bool isOff) async { final connProvider = ref.read(Global.connectionProvider); - final txt = isOff ? "Turn on the electronic load?".i18n : "Turn off the electronic load?".i18n; + final txt = isOff ? "Turn on the electronic load?\nRemember to clear Ah, if needed".i18n : "Turn off the electronic load?".i18n; final bool? ret = await showOkCancelAlertDialog(context: context, title: "Confirm".i18n, content: Text(txt)); if (ret == true) { @@ -670,14 +698,19 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie rdProvider.mode = mode; } if (wh != null) { - rdProvider.wh = wh / 10; //下位机的单位为100mwh + rdProvider.wh = wh / 100; //下位机的单位为10毫瓦时 } if (rSet != null) { - rdProvider.rSet = rSet / 1000; //下位机的单位为毫欧 + rdProvider.rSet = rSet / 100; //下位机的单位为10毫欧 } if (pSet != null) { - rdProvider.pSet = pSet / 10; //下位机的单位为100mw + rdProvider.pSet = pSet / 100; //下位机的单位为10毫瓦 + } + + if (_loadStats.isNotEmpty) { + _loadStats.last.initialWh ??= rdProvider.wh; } + rdProvider.notifyDataChanged(); //每次收到EXTRA数据就复位请求额外数据的定时器 @@ -686,6 +719,7 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie } else if (size >= 29) { //老版本M8V6定义的数据 //debugPrint(String.fromCharCodes(bag)); final vhProvider = ref.watch(Global.vHistoryProvider); + int loadStatus = 0; //1-开始,2-结束 int? iNow; int? vNow = int.tryParse(String.fromCharCodes(bag, 6, 11)); @@ -702,6 +736,7 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie //从未放电到放电状态,则自动清除原先的曲线数据,如果需要,启用屏幕常亮 if (_prevReceivedOff && !isOff) { vhProvider.clear(); + loadStatus = 1; //标识放电开始,下面的代码会创建一个放电状态容器 if (Global.keepScreenOn == KeepScreenOption.onWhenDischarge) { Wakelock.enable(); } @@ -710,6 +745,7 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie //从放电状态到未放电状态,如果需要,关闭屏幕常亮 if (rdProvider.running && isOff) { + loadStatus = 2; //标识放电结束,下面的代码会保存更新本次放电信息 if (Global.keepScreenOn != KeepScreenOption.always) { Wakelock.disable(); } @@ -737,13 +773,61 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie rdProvider.iNow = iNow / 1000; rdProvider.vNow = vNow / 1000; - //功率需要自己计算,下位机没有上报, - //同时为了和下位机显示一致,所以计算公式稍复杂了点,因为下位机仅使用整数运算 - rdProvider.powerIn = (((vNow ~/ 10) * iNow).toInt() ~/ 10000) / 10; + //功率需要自己计算,下位机没有上报 + rdProvider.powerIn = calP(vNow, iNow); rdProvider.ah = (ah != null) ? (ah / 1000) : 0.0; rdProvider.rd = (rd != null) ? (rd / 1000) : 0.0; rdProvider.ra = (ra != null) ? (ra / 1000) : 0.0; + + //放电状态统计信息 + if (loadStatus == 1) { //放电开始 + final lStats = LoadStatsModel(initialV: rdProvider.vNow, initialAh: rdProvider.ah); + lStats.rd = rdProvider.rd; + lStats.ra = rdProvider.ra; + if (_loadStats.length > 16) { //仅保留最近16个记录 + _loadStats.removeAt(0); + } + _loadStats.add(lStats); //新增一个统计数据 + } else if (_loadStats.isNotEmpty) { + final lStats = _loadStats.last; + if (loadStatus == 2) { //放电结束 + lStats.endV = rdProvider.vNow; + lStats.endI = rdProvider.iNow; + lStats.avgV = (lStats.initialV + lStats.endV) / 2; + lStats.avgI = ((lStats.initialI ?? 0.0) + lStats.endI) / 2; + lStats.ah = lStats.totalAh - lStats.initialAh; + lStats.wh = lStats.totalWh - (lStats.initialWh ?? 0.0); + lStats.temperature1 = rdProvider.temperature1; + lStats.temperature2 = rdProvider.temperature2; + lStats.endTime = DateTime.now(); + lStats.loadTime = lStats.endTime!.difference(lStats.startTime); + } else { + if (rdProvider.iNow > 0.0) { + lStats.initialI ??= rdProvider.iNow; + } + if (rdProvider.rd > lStats.rd) { + lStats.rd = rdProvider.rd; + } + if (rdProvider.ra > lStats.ra) { + lStats.ra = rdProvider.ra; + } + lStats.totalAh = rdProvider.ah; + lStats.totalWh = rdProvider.wh; + lStats.ah = lStats.totalAh - lStats.initialAh; + lStats.wh = lStats.totalWh - (lStats.initialWh ?? 0.0); + lStats.temperature1 = rdProvider.temperature1; + lStats.temperature2 = rdProvider.temperature2; + if (lStats.mode.isEmpty && rdProvider.mode.isNotEmpty) { + lStats.mode = rdProvider.mode; + if (lStats.mode == "CR") { + lStats.rSet = rdProvider.rSet; + } else if (lStats.mode == "CP") { + lStats.pSet = rdProvider.pSet; + } + } + } + } rdProvider.notifyDataChanged(); } else if ((size >= 13) && (bag[6] == ",".codeUnitAt(0))) { //VIL数据(实时电压电流LOG) @@ -785,7 +869,7 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie final value = int.tryParse(String.fromCharCodes(bag, 2, 7)); if ((value != null) && (value >= 0) && (value <= 65000)) { rdProvider.vNow = value / 1000; - rdProvider.powerIn = (((value ~/ 10) * (rdProvider.iNow * 1000).toInt()).toInt() ~/ 10000) / 10; + rdProvider.powerIn = calP(value, (rdProvider.iNow * 1000).toInt()); rdProvider.notifyDataChanged(); //debugPrint("Response of V: ${rdProvider.vNow}, power: ${rdProvider.powerIn}, i: ${rdProvider.iNow}"); } @@ -795,7 +879,7 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie final value = int.tryParse(String.fromCharCodes(bag, 2, 7)); if ((value != null) && (value >= 0) && (value <= 15000)) { rdProvider.iNow = value / 1000; - rdProvider.powerIn = ((((rdProvider.vNow * 1000).toInt() ~/ 10) * value).toInt() ~/ 10000) / 10; + rdProvider.powerIn = calP((rdProvider.vNow * 1000).toInt(), value); rdProvider.notifyDataChanged(); //debugPrint("Response of I: ${rdProvider.iNow}, power: ${rdProvider.powerIn}, i: ${rdProvider.iNow}"); } @@ -808,7 +892,7 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie final value = int.tryParse(String.fromCharCodes(bag, 2, 7)); if ((value != null) && (value >= 0) && (value <= 65000)) { rdProvider.vNow = value / 1000; - rdProvider.powerIn = (((value ~/ 10) * (rdProvider.iNow * 1000).toInt()).toInt() ~/ 10000) / 10; + rdProvider.powerIn = calP(value, (rdProvider.iNow * 1000).toInt()); rdProvider.notifyDataChanged(); } } @@ -817,11 +901,17 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie final value = int.tryParse(String.fromCharCodes(bag, 2, 7)); if ((value != null) && (value >= 0) && (value <= 15000)) { rdProvider.iNow = value / 1000; - rdProvider.powerIn = ((((rdProvider.vNow * 1000).toInt() ~/ 10) * value).toInt() ~/ 10000) / 10; + rdProvider.powerIn = calP((rdProvider.vNow * 1000).toInt(), value); rdProvider.notifyDataChanged(); } } } } } + + ///使用和下位机一样的计算功率算法,保证和下位机显示一致(因为下位机仅使用整数运算) + ///最后的除以100是因为下位机使用10毫瓦为单位 + double calP(int v, int i) { + return (((v ~/ 10) * i).toInt() ~/ 1000) / 100; + } } diff --git a/lib/models/load_stats_model.dart b/lib/models/load_stats_model.dart new file mode 100644 index 0000000..0caf675 --- /dev/null +++ b/lib/models/load_stats_model.dart @@ -0,0 +1,33 @@ +/// m328v6数控电子负载上位机 +/// 单次放电的统计数据 +/// Author: cdhigh +/// + +class LoadStatsModel { + final double initialV; //初始电压 + double endV = 0.0; //放电截止电压 + double avgV = 0.0; //平均电压 + double? initialI; //初始电流 + double endI = 0.0; //放电截止时电流 + double avgI = 0.0; //平均电流 + final double initialAh; //初始安时 + double ah = 0.0; //本次放电的安时 + double totalAh = 0.0; //总安时:初始安时+本次安时 + double? initialWh; //初始瓦时 + double wh = 0.0; //本次放电的瓦时 + double totalWh = 0.0; //总瓦时:初始瓦时+本次瓦时 + String mode = ""; //放电模式 + double rSet = 0.0; //CR模式的参数 + double pSet = 0.0; //CP模式的参数 + double ra = 0.0; //本次放电的交流内阻 + double rd = 0.0; //本次放电的直流内阻 + int temperature1 = 0; //本次放电结束时的散热器温度 + int temperature2 = 0; //本次放电结束时的主板温度 + final DateTime startTime; //放电开始时间 + DateTime? endTime; //放电结束时间 + Duration loadTime = const Duration(); //放电持续时间 + String remark = ""; //备注 + + LoadStatsModel({required this.initialV, required this.initialAh}) + : startTime = DateTime.now(); +} diff --git a/lib/models/volt_history_provider.dart b/lib/models/volt_history_provider.dart index d409aa7..a53bce7 100644 --- a/lib/models/volt_history_provider.dart +++ b/lib/models/volt_history_provider.dart @@ -110,8 +110,8 @@ class _VoltHistoryFilter { double add(double volt) { assert(Global.curvaFilterDotNum <= maxFilterNum); - //避免因为传输误码等导致解析失败,产生偶尔的零,来两个零才输出零 - /*if (volt == 0.0) { + //避免因为传输误码或同时收发等原因导致解析失败,产生偶尔的零,来两个零才输出零 + if (volt == 0.0) { if (_prevValue != 0.0) { final prevTemp = _prevValue; _prevValue = 0.0; @@ -120,7 +120,7 @@ class _VoltHistoryFilter { reset(); return 0.0; } - }*/ + } //对阀值进行处理 if ((volt - _prevValue).abs() > Global.curvaFilterThreshold) { diff --git a/lib/routes.dart b/lib/routes.dart index 8eadfc5..cd54e43 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -11,6 +11,7 @@ import 'delay_period_on_off.dart'; import 'switch_mode.dart'; import 'help_page.dart'; import 'export_data.dart'; +import 'load_stats_page.dart'; import 'load_testers/max_i_tester.dart'; import 'load_testers/short_circuit_tester.dart'; @@ -29,6 +30,8 @@ MaterialPageRoute routesPath(RouteSettings settings) { '/mode': (context) => I18n(child: const SwitchModePage(),), '/help': (context) => I18n(child: const HelpPage(),), '/export': (context) => I18n(child: const ExportPage(),), + '/load_stats': (context) => I18n(child: LoadStatsPage(loadStats: settings.arguments as Map>,),), + '/export_stats': (context) => I18n(child: ExportLoadStatsPage(loadStats: settings.arguments as Map>,),), '/max_i_tester': (context) => I18n(child: const MaxITesterPage(),), '/sc_tester': (context) => I18n(child: const ScTesterPage(),), }; diff --git a/lib/switch_mode.dart b/lib/switch_mode.dart index 4f5319a..22a9944 100644 --- a/lib/switch_mode.dart +++ b/lib/switch_mode.dart @@ -31,9 +31,9 @@ class _SwitchModePageState extends ConsumerState { isCR = (_modeStr == "CR"); isCP = (_modeStr == "CP"); if (isCR) { - _modeParamctrller.text = rdProvider.rSet.toString(); + _modeParamctrller.text = rdProvider.rSet.toStringAsFixed(2); } else if (isCP) { - _modeParamctrller.text = rdProvider.pSet.toString(); + _modeParamctrller.text = rdProvider.pSet.toStringAsFixed(2); } }); } @@ -66,7 +66,7 @@ class _SwitchModePageState extends ConsumerState { });},),), SizedBox(width: 300, child: TextField(controller: _modeParamctrller, enabled: isCR || isCP, - //keyboardType: TextInputType.number, + keyboardType: TextInputType.number, onTap: () {}, decoration: InputDecoration( labelText: isCR ? "set the resistor value (Ohm)".i18n @@ -92,19 +92,19 @@ class _SwitchModePageState extends ConsumerState { await showOkAlertDialog(context: context, title: "Success".i18n, content: Text("set CC mode successfully".i18n)); } else if (_modeStr == "CR") { double? resistor = double.tryParse(_modeParamctrller.text); - if ((resistor != null) && (resistor > 0.0) && (resistor < 65.535)) { + if ((resistor != null) && (resistor > 0.0) && (resistor < 655.0)) { load.switchToCR(resistor); await showOkAlertDialog(context: context, title: "Success".i18n, content: Text("set CR mode successfully".i18n)); } else { - await showOkAlertDialog(context: context, title: "Error".i18n, content: Text("Resistance must be greater than zero ohm and less than 65 ohms".i18n)); + await showOkAlertDialog(context: context, title: "Error".i18n, content: Text("Resistance must be greater than zero ohm and less than 655 ohms".i18n)); } } else if (_modeStr == "CP") { double? power = double.tryParse(_modeParamctrller.text); - if ((power != null) && (power > 0.0) && (power < 6553)) { + if ((power != null) && (power > 0.0) && (power < 650.0)) { load.switchToCP(power); await showOkAlertDialog(context: context, title: "Success".i18n, content: Text("set CP mode successfully".i18n)); } else { - await showOkAlertDialog(context: context, title: "Error".i18n, content: Text("Power must be greater than zero watt and less than 6553 watts".i18n)); + await showOkAlertDialog(context: context, title: "Error".i18n, content: Text("Power must be greater than zero watt and less than 650 watts".i18n)); } } } diff --git a/pubspec.yaml b/pubspec.yaml index 633a62d..e4d7eb0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.2.0 +version: 1.2.1 environment: sdk: ">=2.16.1 <3.0.0" @@ -47,7 +47,7 @@ dependencies: collection: ^1.15.0 url_launcher: ^6.0.20 segment_display: ^0.5.0 - #flutter_libserialport: ^0.2.3 #for desktop + flutter_libserialport: ^0.2.3 #for desktop usb_serial: ^0.4.0 #for android flutter_bluetooth_serial: ^0.4.0 #for android #serial_port_win32: ^0.4.9 #for windows diff --git a/versions/version.json b/versions/version.json index 594475d..42f1381 100644 --- a/versions/version.json +++ b/versions/version.json @@ -1,6 +1,13 @@ { - "lastest": "1.2.0", + "lastest": "1.2.1", "history": [ + { + "version": "1.2.1", + "build": "2022-03-21", + "android": "https://github.com/cdhigh/m328v6host/releases/download/v1.2.1/m328v6_V1.2.1.apk", + "windows": "https://github.com/cdhigh/m328v6host/releases/download/v1.2.1/m328v6_win_V1.2.1.zip", + "whatsnew": "1.双击曲线区域可以查看每次放电的统计信息
2.功率单位修改为10毫瓦
3.恒阻单位修改为10毫欧" + }, { "version": "1.2.0", "build": "2022-03-16",