From eaa174802af83761c6c69cad86f63c6a6618fbb8 Mon Sep 17 00:00:00 2001 From: cdhigh Date: Tue, 8 Mar 2022 11:51:58 -0300 Subject: [PATCH] V1.1.0FIX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. bug-fix: 修正安卓版的自动重连功能启用后无法手动断开连接的问题 2. 连接如果错误,将具体的错误信息显示出来 3. 双击曲线区域清除曲线数据 4. 检查到新版本后弹出的对话框中增加一个"下载"按钮 --- README.md | 5 +- lib/common/widget_utils.dart | 5 +- lib/connection.dart | 12 +- lib/flutter_libserialport_stub.dart | 8 + lib/help_page.dart | 1 + lib/i18n/common.i18n.dart | 5 + lib/i18n/help.i18n.dart | 4 + lib/i18n/main_page.i18n.dart | 4 + lib/i18n/settings.i18n.dart | 4 + lib/main_page.dart | 28 +++- lib/settings/settings.dart | 20 ++- lib/uni_serial.dart | 141 ++++++++++++++---- pubspec.lock | 21 --- .../flutter/generated_plugin_registrant.cc | 3 - windows/flutter/generated_plugins.cmake | 1 - 15 files changed, 185 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index f334a53..5a09781 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # m328v6数控电子负载上位机APP -这个是一乐论坛[M8V6数控电子负载]的升级版[M328V6数控电子负载]的新版上位机。 -[M328V6数控电子负载制作链接](https://www.yleee.com.cn/thread-90734-1-1.html) +这个是一乐论坛[**M8V6数控电子负载**]的升级版[**M328V6数控电子负载**]的新版上位机。 ## 特性: 1. 支持Android, iOS, Windows, Mac OS, Linux(注1,2) @@ -17,7 +16,7 @@ -## 屏幕截图: +## 屏幕截图(V1.0.0): ![ScreenshotPortrait](https://raw.githubusercontent.com/cdhigh/m328v6host/main/ref/Screenshots/ScrShotPortrait.png) diff --git a/lib/common/widget_utils.dart b/lib/common/widget_utils.dart index 5d29f12..b3f635f 100644 --- a/lib/common/widget_utils.dart +++ b/lib/common/widget_utils.dart @@ -37,8 +37,9 @@ Widget buildEmptyRow(double height, {bool hasTopBorder=false, bool hasBottomBord } ///显示简单的Toast,可以在任何地方调用 -void showToast(String text, {TextStyle textStyle = const TextStyle(fontSize: 17, color: Colors.white)}) { - BotToast.showText(text: text, duration: const Duration(seconds: 3), textStyle: textStyle); +void showToast(String text, {TextStyle textStyle = const TextStyle(fontSize: 17, color: Colors.white), + Duration duration = const Duration(seconds: 3)}) { + BotToast.showText(text: text, duration: duration, textStyle: textStyle); } ///显示简单的顶端通知信息 diff --git a/lib/connection.dart b/lib/connection.dart index 008c29c..72480cd 100644 --- a/lib/connection.dart +++ b/lib/connection.dart @@ -168,15 +168,19 @@ class _ConnectionPageState extends ConsumerState { } final connProvider = ref.watch(Global.connectionProvider); - bool ret = false; + String ret = ""; try { ret = await _uniSerial.open(name, baudRate); } catch (e) { - ret = false; + ret = e.toString(); } - if (!ret) { - showToast("Open device failed".i18n); + if (ret.isNotEmpty) { + if (ret == "Error") { //没有具体错误信息 + showToast("Open device failed".i18n); + } else { + showToast("Open device failed".i18n + "\n$ret"); + } return; } diff --git a/lib/flutter_libserialport_stub.dart b/lib/flutter_libserialport_stub.dart index a39ae9a..0e53949 100644 --- a/lib/flutter_libserialport_stub.dart +++ b/lib/flutter_libserialport_stub.dart @@ -8,11 +8,16 @@ library flutter_libserialport; /// 4. flutter clean /// 5. flutter build apk //export 'package:flutter_libserialport/flutter_libserialport.dart'; +import 'dart:typed_data'; +import 'dart:async'; +import 'dart:io'; class SerialPort { static final availablePorts = []; int get transport => 0; + static OSError? get lastError => const OSError("", 0); SerialPort(String name); + void write(Uint8List data) {} } class SerialPortTransport { @@ -36,4 +41,7 @@ class SerialPortFlowControl { class SerialPortReader { SerialPortReader(dynamic port); + void close() {} + + Stream get stream => Stream.periodic(const Duration(days : 1)); } diff --git a/lib/help_page.dart b/lib/help_page.dart index f81bf80..8feec2f 100644 --- a/lib/help_page.dart +++ b/lib/help_page.dart @@ -20,6 +20,7 @@ class _HelpPageState extends State { "Double-click 'Vset' to set a new value".i18n, "Double-click 'Iset' to set a new value".i18n, "Double-click 'Capacity' to clear Ah of device".i18n, + "Double-click 'Curva area' to clear curva data".i18n, ]; //所有需要带链接的帮助信息都放在这里 diff --git a/lib/i18n/common.i18n.dart b/lib/i18n/common.i18n.dart index bb2672d..d501a1f 100644 --- a/lib/i18n/common.i18n.dart +++ b/lib/i18n/common.i18n.dart @@ -74,8 +74,13 @@ extension Localization on String { { "en_us": "There is a new version (%s), the download link has been copied to the clipboard", "zh_cn": "发现新版本(%s), 下载链接已经拷贝到系统剪贴板", + } + + { + "en_us": "Name of device invalid", + "zh_cn": "设备名字错误", }; + String get i18n => localize(this, t); //"Hello %s, this is %s".i18n.fill(["John", "Mary"]) diff --git a/lib/i18n/help.i18n.dart b/lib/i18n/help.i18n.dart index 3698325..f02db8b 100644 --- a/lib/i18n/help.i18n.dart +++ b/lib/i18n/help.i18n.dart @@ -19,6 +19,10 @@ extension Localization on String { "en_us": "Double-click 'Capacity' to clear Ah of device", "zh_cn": "双击[容量]可以清零设备的容量值", } + + { + "en_us": "Double-click 'Curva area' to clear curva data", + "zh_cn": "双击[曲线区域]可以清除曲线数据", + } + { "en_us": "Swipe on the left side of the screen to popup the menu", "zh_cn": "从屏幕左侧往里滑可以弹出操作菜单", diff --git a/lib/i18n/main_page.i18n.dart b/lib/i18n/main_page.i18n.dart index 859af8c..c162c03 100644 --- a/lib/i18n/main_page.i18n.dart +++ b/lib/i18n/main_page.i18n.dart @@ -83,6 +83,10 @@ extension Localization on String { "en_us": "Clear Ah?", "zh_cn": "清除容量?", } + + { + "en_us": "Clear curva data?", + "zh_cn": "清除曲线数据?", + } + { "en_us": "Turn on the electronic load?", "zh_cn": "打开负载开始放电?", diff --git a/lib/i18n/settings.i18n.dart b/lib/i18n/settings.i18n.dart index 26c09aa..ae7e421 100644 --- a/lib/i18n/settings.i18n.dart +++ b/lib/i18n/settings.i18n.dart @@ -242,6 +242,10 @@ extension Localization on String { { "en_us": "[The download link has been copied to the clipboard]", "zh_cn": "[下载链接已经拷贝到系统剪贴板]", + } + + { + "en_us": "Download", + "zh_cn": "下载", }; String get i18n => localize(this, t); diff --git a/lib/main_page.dart b/lib/main_page.dart index 5a21f4f..e9521ef 100644 --- a/lib/main_page.dart +++ b/lib/main_page.dart @@ -71,8 +71,10 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie Future.delayed(const Duration(seconds: 5)).then(checkNewVersion); //延时确定是否需要检查新版本 _timerForExtraData = PausableTimer(const Duration(seconds: 3), qeuryVersionPeriodic); + _timerForExtraData.pause(); //_timerForExtraData.start(); //需要等连接后再启动定时器 _timerForReconnect = PausableTimer(const Duration(seconds: 1), reconnectPeriodic); + _timerForReconnect.pause(); } //每隔一段时间重发一次请求额外数据的命令,避免下位机中间复位了 @@ -97,17 +99,17 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie return; } - bool ret = false; + String ret = ""; try { ret = await _uniSerial.open(name, baudRate); } catch (e) { - ret = false; + ret = e.toString(); } - if (ret) { //如果重连成功 + if (ret.isEmpty) { //如果重连成功 connProvider.setPort(name, baudRate); Global.bus.sendBroadcast(EventBus.connectionChanged, arg: "1", sendAsync: false); - } else { //2s后继续重试 + } else { //1s后继续重试 _timerForReconnect..reset()..start(); } } @@ -158,6 +160,8 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie void connectionChanged(String isConnect) { if (mounted) { final connProvider = ref.read(Global.connectionProvider); + _timerForReconnect.pause(); + if (isConnect == "1") { //连接 //注册监听函数 connProvider.serial.registerListenFunction(newSrlDataReceived); @@ -166,10 +170,8 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie Future.delayed(const Duration(milliseconds: 500)).then((_) => connProvider.load.queryVersion()); Future.delayed(const Duration(seconds: 1)).then((_) => connProvider.load.requestExtraData()); _timerForExtraData..reset()..start(); - _timerForReconnect.pause(); } else { //断开连接 final rdProvider = ref.read(Global.runningDataProvider); - connProvider.serial.close(); connProvider.closePort(); rdProvider.reset(); _timerForExtraData.pause(); @@ -293,7 +295,7 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie return Container(padding: const EdgeInsets.all(10.0), color: Global.isDarkMode ? Colors.transparent : appInfo.homePageBackgroundColor, child: ListView(children: [ - const CurvaChart(), + GestureDetector(child: const CurvaChart(), onDoubleTap: onDoubleTapCurvaChart), buildVISetDisplay(context), buildVIDisplay(context), buildOtherDisplayData(context), @@ -435,7 +437,8 @@ class _MainPageState extends ConsumerState with AutomaticKeepAliveClie color: Global.isDarkMode ? Colors.transparent : appInfo.homePageBackgroundColor, child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, - children: [CurvaChartLandscape(scrWidth: _scrWidth), + children: [ + GestureDetector(child: CurvaChartLandscape(scrWidth: _scrWidth), onDoubleTap: onDoubleTapCurvaChart), SingleChildScrollView(child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: buildRawDataDisplayLandscape(context), @@ -599,6 +602,15 @@ 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(); + } + } + ///点击标题栏上的Switch按钮,询问是否需要打开关闭放电 void onTapAppBarSwitch(bool isOff) async { final connProvider = ref.read(Global.connectionProvider); diff --git a/lib/settings/settings.dart b/lib/settings/settings.dart index f12cf11..613b240 100644 --- a/lib/settings/settings.dart +++ b/lib/settings/settings.dart @@ -2,9 +2,11 @@ /// 配置页面ui实现 /// Author: cdhigh /// +import 'dart:io' show Platform; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:wakelock/wakelock.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../common/globals.dart'; import '../common/my_widget_chain.dart'; import '../common/widget_utils.dart'; @@ -413,14 +415,15 @@ class _SettingsPageState extends ConsumerState { ///点击了现在检查新版本 void checkUpdateNow() async { - final ret = await checkUpdate(silent: false); - if (ret == null) { + final newVer = await checkUpdate(silent: false); + if (newVer == null) { return; } - final newVer = ret.version; - final whatsnew = ret.whatsNew.replaceAll("
", "\n"); - showOkAlertDialog(context: context, title: "Found new version [%s]".i18n.fill([newVer]), + final newVerNo = newVer.version; + final whatsnew = newVer.whatsNew.replaceAll("
", "\n"); + final ret = await showOkCancelAlertDialog(context: context, title: "Found new version [%s]".i18n.fill([newVerNo]), + okText: "Download".i18n, content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -432,6 +435,13 @@ class _SettingsPageState extends ConsumerState { child: Text("[The download link has been copied to the clipboard]".i18n, textScaleFactor: 0.8)), ],), ); + if (ret == true) { + if (Platform.isAndroid || Platform.isIOS || Platform.isFuchsia) { + launch(newVer.androidFile); + } else { + launch(newVer.windowsFile); + } + } } } diff --git a/lib/uni_serial.dart b/lib/uni_serial.dart index ae145da..7e0d43c 100644 --- a/lib/uni_serial.dart +++ b/lib/uni_serial.dart @@ -14,8 +14,10 @@ import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart' as sb_an //编译apk需要注释掉 flutter_libserialport,取消注释 flutter_libserialport_stub //import 'package:flutter_libserialport/flutter_libserialport.dart' as lib_serial; import 'flutter_libserialport_stub.dart' as lib_serial; +import 'i18n/common.i18n.dart'; import 'common/globals.dart'; import 'common/event_bus.dart'; +import 'common/widget_utils.dart'; ///对外统一接口的串口操作类 class UniSerial { @@ -24,7 +26,8 @@ class UniSerial { int _baudRate = 19200; //当前打开的串口波特率 final _desc = {}; //每个端口的描述 final _portType = {}; //每个端口的类型,"USB"/"BLUETOOTH"/"WIFI"/"NATIVE"/"UNKNOWN" - dynamic _reader; //for desktop + lib_serial.SerialPortReader? _reader; //for desktop + StreamSubscription? _streamrSubscript; //监听端口数据的流订阅句柄 final _bluetooth = sb_android.FlutterBluetoothSerial.instance; //Singleton @@ -99,9 +102,9 @@ class UniSerial { return _portType[name] ?? "UNKNOWN"; } - //创建一个串口,成功返回true + //创建一个串口,成功返回空字符串,否则返回错误描述字符串,如果没有特定错误,则字符串为"Error" //此函数内部可能会抛出异常 - Future open(String name, int baudRate) async { + Future open(String name, int baudRate) async { assert(name.isNotEmpty && (baudRate >= 9600) && (baudRate <= 115200)); _port = null; _name = ""; @@ -114,7 +117,7 @@ class UniSerial { final iStart = name.indexOf("["); final iEnd = name.indexOf("]"); if ((iStart < 0) || (iEnd < 0)) { - return false; + return "Name of device invalid".i18n; } final addr = name.substring(iStart + 1, iEnd); @@ -123,35 +126,49 @@ class UniSerial { _port = connection; _name = name; _baudRate = baudRate; - return true; + return ""; } catch (e) { - return false; + return e.toString(); } } else { final ports = await serial_android.UsbSerial.listDevices(); final idx = ports.indexWhere((elem) => elem.deviceName == name); if (idx < 0) { - return false; + return "Name of device invalid".i18n; + } + + serial_android.UsbPort? port; + bool ret; + try { + port = await ports[idx].create(); + ret = (await port?.open()) ?? false; + } catch (e) { + return e.toString(); } - final port = await ports[idx].create(); - final ret = (await port?.open()) ?? false; if ((port == null) || (!ret)) { - return false; + return "Error"; } else { _port = port; port.setPortParameters(baudRate, serial_android.UsbPort.DATABITS_8, serial_android.UsbPort.STOPBITS_1, serial_android.UsbPort.PARITY_NONE); _name = name; _baudRate = baudRate; - return true; + return ""; } } } else { //final port = serial_win.SerialPort(name, openNow: false, BaudRate: baudRate); - _port = lib_serial.SerialPort(name); - if (!_port.openReadWrite()) { - return false; + try { + _port = lib_serial.SerialPort(name); + if (!_port.openReadWrite()) { + final lastError = lib_serial.SerialPort.lastError; + final errStr = transWindowsErrorNo(lastError?.errorCode ?? 0); + return errStr.isEmpty ? lastError.toString() : errStr; + } + } catch (e) { + return e.toString(); } + //需要先打开端口再配置端口参数 final cfg = lib_serial.SerialPortConfig(); cfg.baudRate = baudRate; @@ -164,7 +181,7 @@ class UniSerial { _name = name; _baudRate = baudRate; - return true; + return ""; } } @@ -177,9 +194,11 @@ class UniSerial { if (Platform.isAndroid || Platform.isIOS || Platform.isFuchsia) { if (_port is sb_android.BluetoothConnection) { - _port.input.listen(func, onDone: sendDisconnectBroadcast, onError: sendDisconnectBroadcast); + sb_android.BluetoothConnection p = _port; + _streamrSubscript = p.input?.listen(func, onError: ([_]) {sendDisconnectBroadcast("-1");}); } else { - _port.inputStream.listen(func, onDone: sendDisconnectBroadcast, onError: sendDisconnectBroadcast); + serial_android.UsbPort p = _port; + _streamrSubscript = p.inputStream?.listen(func, onError: ([_]) {sendDisconnectBroadcast("-1");}); } } else { //Windows需要先注册监听函数,再打开端口 @@ -194,7 +213,7 @@ class UniSerial { }*/ _reader = lib_serial.SerialPortReader(_port); - _reader.stream.listen(func, onDone: sendDisconnectBroadcast, onError: sendDisconnectBroadcast); + _streamrSubscript = _reader!.stream.listen(func, onDone: ([_]) {sendDisconnectBroadcast("-1");}, onError: ([_]) {sendDisconnectBroadcast("-1");}); } return true; @@ -202,41 +221,60 @@ class UniSerial { ///往串口写一个字节串,此函数可能会抛出异常 void write(Uint8List data) async { - //assert(_port != null); if (_port == null) { return; } if (Platform.isAndroid || Platform.isIOS || Platform.isFuchsia) { if (_port is sb_android.BluetoothConnection) { - _port.output.add(data); - await _port.output.allSent; + sb_android.BluetoothConnection p = _port; + try { + p.output.add(data); + await p.output.allSent; + } catch (e) { + sendDisconnectBroadcast("-1"); + } } else { - await _port.write(data); + serial_android.UsbPort p = _port; + try { + await p.write(data); + } catch (e) { + sendDisconnectBroadcast("-1"); + } } } else { //assert(_port?.isOpened); //_port.writeBytesFromUint8List(data); - _port.write(data); + lib_serial.SerialPort p = _port; + try { + p.write(data); + } catch (e) { + sendDisconnectBroadcast("-1"); + } } } ///关闭串口 void close() { + try { + _streamrSubscript?.cancel(); + } catch (e) { + debugPrint(e.toString()); + } try { _reader?.close(); + } catch (e) { + debugPrint(e.toString()); + } + try { _port?.close(); - //if (Platform.isAndroid || Platform.isIOS || Platform.isFuchsia || (_listenFunc == null)) { - // _port?.close(); - //} else { - // _port?.closeOnListen(onListen: () => debugPrint(_port?.isOpened.toString())); - //} } catch (e) { debugPrint(e.toString()); } _port = null; _reader = null; + _streamrSubscript = null; _desc.clear(); _name = ""; _baudRate = 19200; @@ -244,7 +282,50 @@ class UniSerial { ///连接被关闭后发送端口关闭广播 /// 用参数-1表示异常关闭 - void sendDisconnectBroadcast([_]) { - Global.bus.sendBroadcast(EventBus.connectionChanged, arg: "-1", sendAsync: false); + void sendDisconnectBroadcast(String bcFlag) { + if (bcFlag == "0") { + showNotification("sendDisconnectBroadcast: $bcFlag"); + } else { + showToast("sendDisconnectBroadcast: $bcFlag"); + } + Global.bus.sendBroadcast(EventBus.connectionChanged, arg: bcFlag, sendAsync: false); } + + ///将Windows的一些常用系统错误码翻译为字符串, + ///因为Windows返回的部分错误信息为误码(编码关系),暂时找不到很好的方法解码不让乱码,先这样用着 + String transWindowsErrorNo(int errorNo) { + switch (errorNo) { + case 1: + return 'Incorrect function'; + case 2: + return 'The system cannot find the file specified'; + case 3: + return 'The system cannot find the path specified'; + case 4: + return 'The system cannot open the file'; + case 5: + return 'Access is denied'; + case 6: + return 'The handle is invalid'; + case 7: + return 'The storage control blocks were destroyed'; + case 8: + return 'Not enough memory resources are available to process this command'; + case 21: + return 'The device is not ready'; + case 22: + return 'The device does not recognize the command'; + case 29: + return 'The system cannot write to the specified device'; + case 30: + return 'The system cannot read from the specified device'; + case 31: + return 'A device attached to the system is not functioning'; + case 32: + return 'The process cannot access the file because it is being used by another process'; + default: + return ''; + } + } + } diff --git a/pubspec.lock b/pubspec.lock index 43fe11b..1642e78 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -99,13 +99,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.4" - dylib: - dependency: transitive - description: - name: dylib - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.2+1" equatable: dependency: transitive description: @@ -160,13 +153,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.3" - flutter_libserialport: - dependency: "direct main" - description: - name: flutter_libserialport - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.3" flutter_lints: dependency: "direct dev" description: @@ -261,13 +247,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.6.3" - libserialport: - dependency: transitive - description: - name: libserialport - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.0+3" lints: dependency: transitive description: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index f15090e..4f78848 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,12 +6,9 @@ #include "generated_plugin_registrant.h" -#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { - FlutterLibserialportPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FlutterLibserialportPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 8198da6..411af46 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,7 +3,6 @@ # list(APPEND FLUTTER_PLUGIN_LIST - flutter_libserialport url_launcher_windows )