绑定设备+覆盖模式+控制显示bug+在线更新
This commit is contained in:
@@ -288,7 +288,7 @@
|
||||
"location": "Location",
|
||||
"refresh": "Refresh",
|
||||
"select_work_mode": "Select Work Mode",
|
||||
"bow_mode": "Bow Mode",
|
||||
"bow_mode": "Coverage Mode",
|
||||
"bow_mode_subtitle": "Standard full coverage path planning",
|
||||
"custom_mode": "Custom Mode",
|
||||
"custom_mode_subtitle": "Manually set work area",
|
||||
|
||||
@@ -302,7 +302,7 @@
|
||||
"location": "定位",
|
||||
"refresh": "刷新",
|
||||
"select_work_mode": "选择作业模式",
|
||||
"bow_mode": "弓字模式",
|
||||
"bow_mode": "覆盖模式",
|
||||
"bow_mode_subtitle": "标准全覆盖路径规划",
|
||||
"custom_mode": "自定义模式",
|
||||
"custom_mode_subtitle": "手动设定作业区域",
|
||||
|
||||
4
dist/manifest.json
vendored
4
dist/manifest.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": "1.0.13",
|
||||
"md5": "181176ea51ec26ac97f34c2a550661f0",
|
||||
"version": "1.0.7",
|
||||
"md5": "3e9ad72a89880b96b024eeacec9518c6",
|
||||
"targetVersionCode": 1,
|
||||
"abi": "arm64-v8a"
|
||||
}
|
||||
|
||||
12
fix_json.ps1
Normal file
12
fix_json.ps1
Normal file
@@ -0,0 +1,12 @@
|
||||
$zhPath = 'C:\Users\jsmbz\Documents\flutter-app-02\assets\languages\zh-CN.json'
|
||||
$enPath = 'C:\Users\jsmbz\Documents\flutter-app-02\assets\languages\en-US.json'
|
||||
|
||||
$zhContent = [System.IO.File]::ReadAllText($zhPath)
|
||||
$zhContent = $zhContent.Replace('"bow_mode": "弓字模式"', '"bow_mode": "覆盖模式"')
|
||||
[System.IO.File]::WriteAllText($zhPath, $zhContent)
|
||||
|
||||
$enContent = [System.IO.File]::ReadAllText($enPath)
|
||||
$enContent = $enContent.Replace('"bow_mode": "Bow Mode"', '"bow_mode": "Coverage Mode"')
|
||||
[System.IO.File]::WriteAllText($enPath, $enContent)
|
||||
|
||||
Write-Host "Done!"
|
||||
106
flutter_01.log
Normal file
106
flutter_01.log
Normal file
@@ -0,0 +1,106 @@
|
||||
Flutter crash report.
|
||||
Please report a bug at https://github.com/flutter/flutter/issues.
|
||||
|
||||
## command
|
||||
|
||||
flutter --no-color run --machine --track-widget-creation --device-id=192.168.2.48:37689 --start-paused --dart-define=flutter.inspector.structuredErrors=true --devtools-server-address=http://127.0.0.1:9100 lib\main.dart
|
||||
|
||||
## exception
|
||||
|
||||
FormatException: FormatException: Unexpected character (at character 1)
|
||||
Failed to init NativeSymbolResolver (SymInitialize 3221225476)
|
||||
^
|
||||
|
||||
|
||||
```
|
||||
#0 _ChunkedJsonParser.fail (dart:convert-patch/convert_patch.dart:1467:5)
|
||||
#1 _ChunkedJsonParser.parseNumber (dart:convert-patch/convert_patch.dart:1333:9)
|
||||
#2 _ChunkedJsonParser.parse (dart:convert-patch/convert_patch.dart:935:22)
|
||||
#3 _JsonStringDecoderSink.addSlice (dart:convert-patch/convert_patch.dart:1555:13)
|
||||
#4 _JsonStringDecoderSink.add (dart:convert-patch/convert_patch.dart:1560:5)
|
||||
#5 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18)
|
||||
#6 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24)
|
||||
#7 _rootRunUnary (dart:async/zone.dart:1538:47)
|
||||
#8 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||
#9 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||
#10 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||
#11 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||
#12 _SinkTransformerStreamSubscription._add (dart:async/stream_transformers.dart:67:11)
|
||||
#13 _EventSinkWrapper.add (dart:async/stream_transformers.dart:13:11)
|
||||
#14 _StringAdapterSink.add (dart:convert/string_conversion.dart:228:11)
|
||||
#15 _StringAdapterSink.addSlice (dart:convert/string_conversion.dart:233:7)
|
||||
#16 _Utf8ConversionSink.addSlice (dart:convert/string_conversion.dart:313:20)
|
||||
#17 _Utf8ConversionSink.add (dart:convert/string_conversion.dart:306:5)
|
||||
#18 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18)
|
||||
#19 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24)
|
||||
#20 _rootRunUnary (dart:async/zone.dart:1538:47)
|
||||
#21 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||
#22 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||
#23 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||
#24 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||
#25 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19)
|
||||
#26 _StreamController._add (dart:async/stream_controller.dart:663:7)
|
||||
#27 _StreamController.add (dart:async/stream_controller.dart:618:5)
|
||||
#28 _Socket._onData (dart:io-patch/socket_patch.dart:2880:41)
|
||||
#29 _rootRunUnary (dart:async/zone.dart:1546:13)
|
||||
#30 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||
#31 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||
#32 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||
#33 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||
#34 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19)
|
||||
#35 _StreamController._add (dart:async/stream_controller.dart:663:7)
|
||||
#36 _StreamController.add (dart:async/stream_controller.dart:618:5)
|
||||
#37 new _RawSocket.<anonymous closure> (dart:io-patch/socket_patch.dart:2315:31)
|
||||
#38 _NativeSocket.issueReadEvent.issue (dart:io-patch/socket_patch.dart:1655:14)
|
||||
#39 _microtaskLoop (dart:async/schedule_microtask.dart:40:35)
|
||||
#40 _startMicrotaskLoop (dart:async/schedule_microtask.dart:49:5)
|
||||
#41 _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:127:13)
|
||||
#42 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:194:5)
|
||||
```
|
||||
|
||||
## flutter doctor
|
||||
|
||||
```
|
||||
[✓] Flutter (Channel stable, 3.38.7, on Microsoft Windows [版本 10.0.26200.9168], locale zh-CN) [1,623ms]
|
||||
• Flutter version 3.38.7 on channel stable at D:\fluttersdk\flutter_windows_3.38.7-stable\flutter
|
||||
• Upstream repository https://github.com/flutter/flutter.git
|
||||
• Framework revision 3b62efc2a3 (8 months ago), 2026-01-13 13:47:42 -0800
|
||||
• Engine revision 78fc3012e4
|
||||
• Dart version 3.10.7
|
||||
• DevTools version 2.51.1
|
||||
• Pub download mirror https://pub.flutter-io.cn
|
||||
• Flutter download mirror https://storage.flutter-io.cn
|
||||
• Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, omit-legacy-version-file, enable-lldb-debugging
|
||||
|
||||
[✓] Windows Version (11 家庭版 中文版 64-bit, 25H2, 2009) [9.3s]
|
||||
|
||||
[✓] Android toolchain - develop for Android devices (Android SDK version 36.1.0) [14.1s]
|
||||
• Android SDK at C:\Users\jsmbz\AppData\Local\Android\Sdk
|
||||
• Emulator version 36.3.10.0 (build_id 14472402) (CL:N/A)
|
||||
• Platform android-36, build-tools 36.1.0
|
||||
• ANDROID_HOME = C:\Users\jsmbz\AppData\Local\Android\Sdk
|
||||
• Java binary at: D:\devtools\anrdstod\jbr\bin\java
|
||||
This is the JDK bundled with the latest Android Studio installation on this machine.
|
||||
To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
|
||||
• Java version OpenJDK Runtime Environment (build 21.0.8+-14196175-b1038.72)
|
||||
• All Android licenses accepted.
|
||||
|
||||
[✓] Chrome - develop for the web [20ms]
|
||||
• Chrome at C:\Program Files\Google\Chrome\Application\chrome.exe
|
||||
|
||||
[✗] Visual Studio - develop Windows apps [17ms]
|
||||
✗ Visual Studio not installed; this is necessary to develop Windows apps.
|
||||
Download at https://visualstudio.microsoft.com/downloads/.
|
||||
Please install the "Desktop development with C++" workload, including all of its default components
|
||||
|
||||
[✓] Connected device (4 available) [8.2s]
|
||||
• PKB110 (mobile) • 192.168.2.48:37689 • android-arm64 • Android 16 (API 36)
|
||||
• Windows (desktop) • windows • windows-x64 • Microsoft Windows [版本 10.0.26200.9168]
|
||||
• Chrome (web) • chrome • web-javascript • Google Chrome 151.0.7922.138
|
||||
• Edge (web) • edge • web-javascript • Microsoft Edge 151.0.4129.93
|
||||
|
||||
[✓] Network resources [1,268ms]
|
||||
• All expected network resources are available.
|
||||
|
||||
! Doctor found issues in 1 category.
|
||||
```
|
||||
109
flutter_02.log
Normal file
109
flutter_02.log
Normal file
@@ -0,0 +1,109 @@
|
||||
Flutter crash report.
|
||||
Please report a bug at https://github.com/flutter/flutter/issues.
|
||||
|
||||
## command
|
||||
|
||||
flutter --no-color run --machine --track-widget-creation --device-id=192.168.2.39:42831 --start-paused --dart-define=flutter.inspector.structuredErrors=true --devtools-server-address=http://127.0.0.1:9100 lib\main.dart
|
||||
|
||||
## exception
|
||||
|
||||
FormatException: FormatException: Unexpected character (at character 1)
|
||||
Failed to init NativeSymbolResolver (SymInitialize 3221225476)
|
||||
^
|
||||
|
||||
|
||||
```
|
||||
#0 _ChunkedJsonParser.fail (dart:convert-patch/convert_patch.dart:1467:5)
|
||||
#1 _ChunkedJsonParser.parseNumber (dart:convert-patch/convert_patch.dart:1333:9)
|
||||
#2 _ChunkedJsonParser.parse (dart:convert-patch/convert_patch.dart:935:22)
|
||||
#3 _JsonStringDecoderSink.addSlice (dart:convert-patch/convert_patch.dart:1555:13)
|
||||
#4 _JsonStringDecoderSink.add (dart:convert-patch/convert_patch.dart:1560:5)
|
||||
#5 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18)
|
||||
#6 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24)
|
||||
#7 _rootRunUnary (dart:async/zone.dart:1538:47)
|
||||
#8 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||
#9 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||
#10 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||
#11 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||
#12 _SinkTransformerStreamSubscription._add (dart:async/stream_transformers.dart:67:11)
|
||||
#13 _EventSinkWrapper.add (dart:async/stream_transformers.dart:13:11)
|
||||
#14 _StringAdapterSink.add (dart:convert/string_conversion.dart:228:11)
|
||||
#15 _StringAdapterSink.addSlice (dart:convert/string_conversion.dart:233:7)
|
||||
#16 _Utf8ConversionSink.addSlice (dart:convert/string_conversion.dart:313:20)
|
||||
#17 _Utf8ConversionSink.add (dart:convert/string_conversion.dart:306:5)
|
||||
#18 _ConverterStreamEventSink.add (dart:convert/chunked_conversion.dart:70:18)
|
||||
#19 _SinkTransformerStreamSubscription._handleData (dart:async/stream_transformers.dart:115:24)
|
||||
#20 _rootRunUnary (dart:async/zone.dart:1538:47)
|
||||
#21 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||
#22 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||
#23 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||
#24 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||
#25 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19)
|
||||
#26 _StreamController._add (dart:async/stream_controller.dart:663:7)
|
||||
#27 _StreamController.add (dart:async/stream_controller.dart:618:5)
|
||||
#28 _Socket._onData (dart:io-patch/socket_patch.dart:2880:41)
|
||||
#29 _rootRunUnary (dart:async/zone.dart:1546:13)
|
||||
#30 _CustomZone.runUnary (dart:async/zone.dart:1429:19)
|
||||
#31 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1329:7)
|
||||
#32 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:381:11)
|
||||
#33 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:312:7)
|
||||
#34 _SyncStreamControllerDispatch._sendData (dart:async/stream_controller.dart:798:19)
|
||||
#35 _StreamController._add (dart:async/stream_controller.dart:663:7)
|
||||
#36 _StreamController.add (dart:async/stream_controller.dart:618:5)
|
||||
#37 new _RawSocket.<anonymous closure> (dart:io-patch/socket_patch.dart:2315:31)
|
||||
#38 _NativeSocket.issueReadEvent.issue (dart:io-patch/socket_patch.dart:1655:14)
|
||||
#39 _microtaskLoop (dart:async/schedule_microtask.dart:40:35)
|
||||
#40 _startMicrotaskLoop (dart:async/schedule_microtask.dart:49:5)
|
||||
#41 _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:127:13)
|
||||
#42 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:194:5)
|
||||
```
|
||||
|
||||
## flutter doctor
|
||||
|
||||
```
|
||||
[✓] Flutter (Channel stable, 3.38.7, on Microsoft Windows [版本 10.0.26200.9168], locale zh-CN) [841ms]
|
||||
• Flutter version 3.38.7 on channel stable at D:\fluttersdk\flutter_windows_3.38.7-stable\flutter
|
||||
• Upstream repository https://github.com/flutter/flutter.git
|
||||
• Framework revision 3b62efc2a3 (8 months ago), 2026-01-13 13:47:42 -0800
|
||||
• Engine revision 78fc3012e4
|
||||
• Dart version 3.10.7
|
||||
• DevTools version 2.51.1
|
||||
• Pub download mirror https://pub.flutter-io.cn
|
||||
• Flutter download mirror https://storage.flutter-io.cn
|
||||
• Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, omit-legacy-version-file, enable-lldb-debugging
|
||||
|
||||
[✓] Windows Version (11 家庭版 中文版 64-bit, 25H2, 2009) [4.4s]
|
||||
|
||||
[✓] Android toolchain - develop for Android devices (Android SDK version 36.1.0) [9.0s]
|
||||
• Android SDK at C:\Users\jsmbz\AppData\Local\Android\Sdk
|
||||
• Emulator version 36.3.10.0 (build_id 14472402) (CL:N/A)
|
||||
• Platform android-36, build-tools 36.1.0
|
||||
• ANDROID_HOME = C:\Users\jsmbz\AppData\Local\Android\Sdk
|
||||
• Java binary at: D:\devtools\anrdstod\jbr\bin\java
|
||||
This is the JDK bundled with the latest Android Studio installation on this machine.
|
||||
To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
|
||||
• Java version OpenJDK Runtime Environment (build 21.0.8+-14196175-b1038.72)
|
||||
• All Android licenses accepted.
|
||||
|
||||
[✓] Chrome - develop for the web [11ms]
|
||||
• Chrome at C:\Program Files\Google\Chrome\Application\chrome.exe
|
||||
|
||||
[✗] Visual Studio - develop Windows apps [8ms]
|
||||
✗ Visual Studio not installed; this is necessary to develop Windows apps.
|
||||
Download at https://visualstudio.microsoft.com/downloads/.
|
||||
Please install the "Desktop development with C++" workload, including all of its default components
|
||||
|
||||
[✓] Connected device (7 available) [11.6s]
|
||||
• 21091116AC (mobile) • 192.168.2.16:35367 • android-arm64 • Android 13 (API 33)
|
||||
• PKB110 (mobile) • 192.168.2.39:42831 • android-arm64 • Android 16 (API 36)
|
||||
• PKB110 (wireless) (mobile) • adb-4TRKWOEE4HR8AMEA-ti69Ed._adb-tls-connect._tcp • android-arm64 • Android 16 (API 36)
|
||||
• 21091116AC (wireless) (mobile) • adb-QK4LCEO78TWOVKOB-v9ALzk._adb-tls-connect._tcp • android-arm64 • Android 13 (API 33)
|
||||
• Windows (desktop) • windows • windows-x64 • Microsoft Windows [版本 10.0.26200.9168]
|
||||
• Chrome (web) • chrome • web-javascript • Google Chrome 151.0.7922.138
|
||||
• Edge (web) • edge • web-javascript • Microsoft Edge 151.0.4129.93
|
||||
|
||||
[✓] Network resources [939ms]
|
||||
• All expected network resources are available.
|
||||
|
||||
! Doctor found issues in 1 category.
|
||||
```
|
||||
@@ -14,6 +14,7 @@ import '../features/devices/presentation/bloc/device_status_event.dart';
|
||||
import '../features/devices/presentation/bloc/devices_cubit.dart';
|
||||
import '../features/remote_control/presentation/bloc/remote_control_cubit.dart';
|
||||
import '../features/remote_control/presentation/bloc/remote_control_state.dart';
|
||||
import '../core/logging/log_time.dart';
|
||||
|
||||
const int DATA_TIMEOUT_SECONDS = 15;
|
||||
|
||||
@@ -49,6 +50,7 @@ class _DeviceStatusModalState extends State<DeviceStatusModal> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
debugPrint('${LogTime.now()} 🖥️ [设备数据监控] 页面打开,开始展示当前订阅设备的数据');
|
||||
_startDataTimeoutTimer();
|
||||
// 🔥 页面初始化时自动调用刷新
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
@@ -64,6 +66,7 @@ class _DeviceStatusModalState extends State<DeviceStatusModal> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
debugPrint('${LogTime.now()} 🚪 [设备数据监控] 页面关闭(MQTT订阅保持,后台继续收数据)');
|
||||
_dataTimeoutTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -1,123 +1,125 @@
|
||||
import '../env/env_config.dart';
|
||||
|
||||
class HttpApiConsts {
|
||||
// static const String baseUrl = "http://8.159.134.0:8012"; // 旧地址
|
||||
// static const String baseUrl = "http://1.95.137.212:59015"; // 测试地址
|
||||
// static const String baseUrl = "http://1.95.137.212:8081"; // 生产地址(暂时切到测试服 59003,回退时恢复此行)
|
||||
static const String baseUrl = "http://1.95.137.212:59003"; // 测试服地址
|
||||
static String get baseUrl => EnvConfig.baseUrl;
|
||||
|
||||
/// 账号相关
|
||||
// 登录
|
||||
static const String loginUrl = "$baseUrl/login";
|
||||
static String get loginUrl => "$baseUrl/login";
|
||||
|
||||
///设备相关
|
||||
// 获取设备列表
|
||||
static const String getUserDevicesList = "$baseUrl/iot/device/list";
|
||||
static String get getUserDevicesList => "$baseUrl/iot/device/list";
|
||||
// 绑定设备
|
||||
static const String bindDevice = "$baseUrl/iot/device/bind";
|
||||
static String get bindDevice => "$baseUrl/iot/device/bind";
|
||||
// 解绑设备
|
||||
static const String unbindDevice = "$baseUrl/forward/device/unbind";
|
||||
static String get unbindDevice => "$baseUrl/forward/device/unbind";
|
||||
// 切换设备
|
||||
static const String switchDevice = "$baseUrl/forward/device/switchDevice";
|
||||
static String get switchDevice => "$baseUrl/forward/device/switchDevice";
|
||||
|
||||
// 获取光伏电站列表
|
||||
static const String getSiteList = "$baseUrl/system/site/selectByUserId";
|
||||
static String get getSiteList => "$baseUrl/system/site/selectByUserId";
|
||||
|
||||
// 获取场站下的设备列表
|
||||
static const String getSiteDeviceList = "$baseUrl/iot/device/getSiteList";
|
||||
static String get getSiteDeviceList => "$baseUrl/iot/device/getSiteList";
|
||||
|
||||
// 获取场站下的无人机机场列表
|
||||
static const String getSiteUAVList = "$baseUrl/iot/UAV/getSiteUAVList";
|
||||
static String get getSiteUAVList => "$baseUrl/iot/UAV/getSiteUAVList";
|
||||
|
||||
// 获取UAV状态详情
|
||||
static const String getUAVState = "$baseUrl/iot/UAV/getUAVState";
|
||||
static String get getUAVState => "$baseUrl/iot/UAV/getUAVState";
|
||||
|
||||
// 切换摄像头获取视频流
|
||||
static const String changeCamera = "$baseUrl/iot/UAV/changeCamera";
|
||||
static String get changeCamera => "$baseUrl/iot/UAV/changeCamera";
|
||||
|
||||
// 获取设备位置
|
||||
static const String getDeviceLocation = "$baseUrl/iot/device/userDevice";
|
||||
static String get getDeviceLocation => "$baseUrl/iot/device/userDevice";
|
||||
|
||||
// 获取全部设备列表(用于蓝牙绑定匹配)
|
||||
static String get getDeviceList => "$baseUrl/iot/device/getDeviceList";
|
||||
|
||||
// 获取机器人列表
|
||||
static const String getRobotList = "$baseUrl/iot/device/getSiteList";
|
||||
static String get getRobotList => "$baseUrl/iot/device/getSiteList";
|
||||
|
||||
// 获取飞行任务列表
|
||||
static const String getFlightTask = "$baseUrl/iot/UAV/getFlightTask";
|
||||
static String get getFlightTask => "$baseUrl/iot/UAV/getFlightTask";
|
||||
|
||||
// 获取飞行任务详情
|
||||
static const String getFlightTaskDetail =
|
||||
static String get getFlightTaskDetail =>
|
||||
"$baseUrl/iot/UAV/getFlightTaskDetail";
|
||||
|
||||
// 获取航线列表
|
||||
static const String getWayline = "$baseUrl/iot/UAV/getWayline";
|
||||
static String get getWayline => "$baseUrl/iot/UAV/getWayline";
|
||||
|
||||
// 创建飞行任务
|
||||
static const String createFlightTask = "$baseUrl/iot/UAV/createFlightTask";
|
||||
static String get createFlightTask => "$baseUrl/iot/UAV/createFlightTask";
|
||||
|
||||
// 更新飞行任务状态
|
||||
static const String updateFlightTaskStatus =
|
||||
static String get updateFlightTaskStatus =>
|
||||
"$baseUrl/iot/UAV/updateFlightTaskStatus";
|
||||
|
||||
// 切换无人机镜头获取视频流
|
||||
static const String changeUAVLens = "$baseUrl/iot/UAV/changeLens";
|
||||
static String get changeUAVLens => "$baseUrl/iot/UAV/changeLens";
|
||||
|
||||
// 获取无人机详情
|
||||
static const String getUAVDetail = "$baseUrl/iot/UAV/getUAVDetail";
|
||||
static String get getUAVDetail => "$baseUrl/iot/UAV/getUAVDetail";
|
||||
|
||||
// 飞行任务命令控制(暂停、返航等)
|
||||
static const String flightTaskCommand = "$baseUrl/iot/UAV/flightTaskCommand";
|
||||
static String get flightTaskCommand => "$baseUrl/iot/UAV/flightTaskCommand";
|
||||
|
||||
/// 告警相关
|
||||
static const String alarmBaseUrl = baseUrl;
|
||||
static String get alarmBaseUrl => baseUrl;
|
||||
// 获取告警工单配置列表
|
||||
static const String alarmOrderConfigList =
|
||||
static String get alarmOrderConfigList =>
|
||||
"$alarmBaseUrl/iot/alarmOrderConfig/list";
|
||||
// 获取告警列表
|
||||
static const String alarmList = "$baseUrl/iot/alarm/list";
|
||||
static String get alarmList => "$baseUrl/iot/alarm/list";
|
||||
// 获取告警详情
|
||||
static const String alarmDetail = "$baseUrl/iot/alarm";
|
||||
static String get alarmDetail => "$baseUrl/iot/alarm";
|
||||
// 处理告警(确认/关闭等)
|
||||
static const String alarmHandle = "$baseUrl/iot/alarm/handle";
|
||||
static String get alarmHandle => "$baseUrl/iot/alarm/handle";
|
||||
|
||||
/// 工单相关
|
||||
// 获取工单模型配置列表
|
||||
static const String orderModelList = "$baseUrl/iot/orderModel/list";
|
||||
static String get orderModelList => "$baseUrl/iot/orderModel/list";
|
||||
// 添加工单(上报)
|
||||
static const String workOrderAdd = "$baseUrl/iot/ioTworkOrder/add";
|
||||
static String get workOrderAdd => "$baseUrl/iot/ioTworkOrder/add";
|
||||
// 获取工单列表
|
||||
static const String workOrderList = "$baseUrl/iot/ioTworkOrder/list";
|
||||
static String get workOrderList => "$baseUrl/iot/ioTworkOrder/list";
|
||||
// 获取工单详情
|
||||
static const String workOrderDetail = "$baseUrl/iot/ioTworkOrder";
|
||||
static String get workOrderDetail => "$baseUrl/iot/ioTworkOrder";
|
||||
// 派发工单
|
||||
static const String workOrderDispat = "$baseUrl/iot/ioTworkOrder/dispatch";
|
||||
static String get workOrderDispat => "$baseUrl/iot/ioTworkOrder/dispatch";
|
||||
// 挂起工单
|
||||
static const String workOrderSuspend = "$baseUrl/iot/ioTworkOrder/suspend";
|
||||
static String get workOrderSuspend => "$baseUrl/iot/ioTworkOrder/suspend";
|
||||
// 完成工单
|
||||
static const String workOrderComplete = "$baseUrl/iot/ioTworkOrder/complete";
|
||||
static String get workOrderComplete => "$baseUrl/iot/ioTworkOrder/complete";
|
||||
// 开始执行工单
|
||||
static const String workOrderStart = "$baseUrl/iot/ioTworkOrder/start";
|
||||
static String get workOrderStart => "$baseUrl/iot/ioTworkOrder/start";
|
||||
// 获取工单统计
|
||||
static const String workOrderCount = "$baseUrl/iot/ioTworkOrder/count";
|
||||
static String get workOrderCount => "$baseUrl/iot/ioTworkOrder/count";
|
||||
// 设备运行参数查询
|
||||
static const String deviceRunParamSelect =
|
||||
static String get deviceRunParamSelect =>
|
||||
"$baseUrl/iot/deviceRunParam/selectByDeviceId";
|
||||
// 设备运行参数保存
|
||||
static const String deviceRunParamSave = "$baseUrl/iot/deviceRunParam/save";
|
||||
static String get deviceRunParamSave => "$baseUrl/iot/deviceRunParam/save";
|
||||
|
||||
// 设备操作权限校验
|
||||
static const String hasPermission = "$baseUrl/iot/device/hasPermission";
|
||||
static String get hasPermission => "$baseUrl/iot/device/hasPermission";
|
||||
|
||||
// 释放远程控制权限(请求体:platform/type/deviceId/token)
|
||||
static const String releaseControlUrl =
|
||||
static String get releaseControlUrl =>
|
||||
"$baseUrl/forward/device/releaseControl";
|
||||
|
||||
/// 用户相关
|
||||
// 获取用户列表
|
||||
static const String systemUserList = "$baseUrl/system/user/list";
|
||||
static String get systemUserList => "$baseUrl/system/user/list";
|
||||
|
||||
/// 组织与场站相关
|
||||
// 获取组织列表
|
||||
static const String orgList = "$baseUrl/system/org/list";
|
||||
static String get orgList => "$baseUrl/system/org/list";
|
||||
// 根据组织ID获取场站列表
|
||||
static const String siteListByOrgId = "$baseUrl/system/site/selectByOrgId";
|
||||
static String get siteListByOrgId => "$baseUrl/system/site/selectByOrgId";
|
||||
// 根据场站ID获取用户列表
|
||||
static const String userListBySiteId = "$baseUrl/system/user/list";
|
||||
static String get userListBySiteId => "$baseUrl/system/user/list";
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ import '../../features/v2/device_list/domain/repositories/device_repository.dart
|
||||
as device_v2_domain;
|
||||
import '../../features/v2/device_list/domain/usecases/get_device_status_data_usecase.dart'
|
||||
as device_v2_usecase;
|
||||
import '../../features/v2/device_list/domain/usecases/get_all_devices_usecase.dart';
|
||||
import '../../features/v2/device_list/presentation/bloc/device_status_bloc.dart'
|
||||
as device_v2_bloc;
|
||||
import '../../features/v2/device_list/data/datasources/drone_station_datasource.dart';
|
||||
@@ -151,10 +152,13 @@ import '../network/mqtt/domain/interfaces/mqtt_client.dart';
|
||||
import '../network/mqtt/data/infrastructure/mqtt_client_impl.dart';
|
||||
import '../network/mqtt/data/datasources/drone_osd_datasource.dart';
|
||||
import '../network/mqtt/data/datasources/task_message_datasource.dart';
|
||||
import '../network/mqtt/data/datasources/mower_realtime_datasource.dart';
|
||||
import '../network/mqtt/domain/repositories/drone_osd_repository.dart';
|
||||
import '../network/mqtt/domain/repositories/task_message_repository.dart';
|
||||
import '../network/mqtt/domain/repositories/mower_realtime_repository.dart';
|
||||
import '../network/mqtt/data/repositories/drone_osd_repository_impl.dart';
|
||||
import '../network/mqtt/data/repositories/task_message_repository_impl.dart';
|
||||
import '../network/mqtt/data/repositories/mower_realtime_repository_impl.dart';
|
||||
import '../router/app_router.dart';
|
||||
import '../storage/impl/user_storage_impl.dart';
|
||||
import '../storage/user_storage.dart';
|
||||
@@ -177,15 +181,20 @@ Future<void> init() async {
|
||||
);
|
||||
sl.registerLazySingleton(() => PathPlanningService());
|
||||
|
||||
/// 1.1.3 MQTT Clients - 两个独立的 MQTT 客户端实例
|
||||
/// 1.1.3 MQTT Clients - 三个独立的 MQTT 客户端实例
|
||||
sl.registerLazySingleton<MqttClient>(
|
||||
() => MqttClientImpl(),
|
||||
() => MqttClientImpl(clientName: 'droneOsdClient'),
|
||||
instanceName: 'droneOsdClient',
|
||||
);
|
||||
sl.registerLazySingleton<MqttClient>(
|
||||
() => MqttClientImpl(),
|
||||
() => MqttClientImpl(clientName: 'taskMessageClient'),
|
||||
instanceName: 'taskMessageClient',
|
||||
);
|
||||
// 🔥 割草机机器状态专用 MQTT 客户端(1.95.137.212:59007)
|
||||
sl.registerLazySingleton<MqttClient>(
|
||||
() => MqttClientImpl(clientName: 'mowerRealtimeClient'),
|
||||
instanceName: 'mowerRealtimeClient',
|
||||
);
|
||||
|
||||
/// 1.1.4 NetMessageDispatcher:消息调度器,并将 TcpClient 注入给它
|
||||
sl.registerLazySingleton(
|
||||
@@ -228,6 +237,14 @@ Future<void> init() async {
|
||||
),
|
||||
);
|
||||
|
||||
/// 割草机机器状态(MQTT):车辆实时+定位,LazySingleton 跨页面共享同一订阅
|
||||
/// 🔥 使用独立 MQTT 连接 mowerRealtimeClient(1.95.137.212:59007)
|
||||
sl.registerLazySingleton<MowerRealtimeDataSource>(
|
||||
() => MowerRealtimeDataSourceImpl(
|
||||
sl<MqttClient>(instanceName: 'mowerRealtimeClient'),
|
||||
),
|
||||
);
|
||||
|
||||
/// 1.5 --- MQTT Repositories ---
|
||||
sl.registerLazySingleton<DroneOsdRepository>(
|
||||
() => DroneOsdRepositoryImpl(sl<DroneOsdDataSource>()),
|
||||
@@ -235,6 +252,9 @@ Future<void> init() async {
|
||||
sl.registerLazySingleton<TaskMessageRepository>(
|
||||
() => TaskMessageRepositoryImpl(sl<TaskMessageDataSource>()),
|
||||
);
|
||||
sl.registerLazySingleton<MowerRealtimeRepository>(
|
||||
() => MowerRealtimeRepositoryImpl(sl<MowerRealtimeDataSource>()),
|
||||
);
|
||||
|
||||
/// 2. 数据源 (DataSource)
|
||||
sl.registerLazySingleton<AuthHttpDataSource>(
|
||||
@@ -357,6 +377,9 @@ Future<void> init() async {
|
||||
sl.registerLazySingleton<device_v2_usecase.GetDeviceStatusDataUseCase>(
|
||||
() => device_v2_usecase.GetDeviceStatusDataUseCase(repository: sl()),
|
||||
);
|
||||
sl.registerLazySingleton<GetAllDevicesUseCase>(
|
||||
() => GetAllDevicesUseCase(repository: sl()),
|
||||
);
|
||||
sl.registerFactory<device_v2_bloc.DeviceStatusBloc>(
|
||||
() => device_v2_bloc.DeviceStatusBloc(sl()),
|
||||
);
|
||||
|
||||
18
lib/core/env/env_config.dart
vendored
18
lib/core/env/env_config.dart
vendored
@@ -14,14 +14,14 @@ class EnvConfig {
|
||||
|
||||
static bool get isProduction => environment == 'prod';
|
||||
|
||||
/// TCP 服务器配置
|
||||
static String get tcpIp => '1.95.137.212';
|
||||
// ─── 服务器 IP(测试/生产共用同一台机器) ───
|
||||
static const String serverHost = '1.95.137.212';
|
||||
|
||||
static int get tcpPort {
|
||||
// 测试服: 59016, 生产服: 9001(目前暂时都走测试服 59004,回退时改回 9001)
|
||||
if (environment == 'prod') {
|
||||
return 59004;
|
||||
}
|
||||
return 59004; // TCP 端口
|
||||
}
|
||||
// ─── TCP 控制长连接 ───
|
||||
static String get tcpIp => serverHost;
|
||||
static int get tcpPort => isProduction ? 9001 : 59004;
|
||||
|
||||
// ─── HTTP 接口 ───
|
||||
static int get httpPort => isProduction ? 8081 : 59003;
|
||||
static String get baseUrl => 'http://$serverHost:$httpPort';
|
||||
}
|
||||
|
||||
14
lib/core/logging/log_time.dart
Normal file
14
lib/core/logging/log_time.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
/// 链路日志统一时间戳工具
|
||||
///
|
||||
/// 用于打印"什么时间做了什么"的全链路日志,格式:[HH:mm:ss.SSS]
|
||||
class LogTime {
|
||||
LogTime._();
|
||||
|
||||
static String now() {
|
||||
final now = DateTime.now();
|
||||
return '[${now.hour.toString().padLeft(2, '0')}:'
|
||||
'${now.minute.toString().padLeft(2, '0')}:'
|
||||
'${now.second.toString().padLeft(2, '0')}.'
|
||||
'${now.millisecond.toString().padLeft(3, '0')}]';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../../../logging/log_time.dart';
|
||||
import '../../domain/interfaces/mqtt_client.dart';
|
||||
import '../../domain/models/mqtt_message.dart';
|
||||
import '../../domain/entities/mower_location_entity.dart';
|
||||
import '../../domain/entities/real_time_message_entity.dart';
|
||||
import '../../domain/entities/route_status_entity.dart';
|
||||
|
||||
/// 割草机实时状态 MQTT 数据源(替代原 TCP 0x02 机器状态推送)
|
||||
///
|
||||
/// 订阅主题:
|
||||
/// - 车辆实时状态 mower/{sn}/property/realtime/post(1Hz,不含定位)
|
||||
/// - 高频定位 mower/{sn}/property/location/post(5-10Hz)
|
||||
/// - 路径规划 device/{deviceId}/route/post(任务执行后推送覆盖清扫路线)
|
||||
abstract class MowerRealtimeDataSource {
|
||||
Stream<RealTimeMessageEntity> get vehicleStream;
|
||||
Stream<MowerLocationEntity> get locationStream;
|
||||
Stream<RouteStatusEntity> get routeStatusStream;
|
||||
|
||||
/// 按设备 SN 启动订阅:自动退订旧设备主题、同 SN 去重
|
||||
Future<void> startListening({required String deviceSn});
|
||||
|
||||
Future<void> stopListening();
|
||||
}
|
||||
|
||||
class MowerRealtimeDataSourceImpl implements MowerRealtimeDataSource {
|
||||
final MqttClient mqttClient;
|
||||
|
||||
static const String _realtimeSuffix = 'property/realtime/post';
|
||||
static const String _locationSuffix = 'property/location/post';
|
||||
static const String _routeSuffix = 'route/post';
|
||||
|
||||
final _vehicleController =
|
||||
StreamController<RealTimeMessageEntity>.broadcast();
|
||||
final _locationController = StreamController<MowerLocationEntity>.broadcast();
|
||||
final _routeController = StreamController<RouteStatusEntity>.broadcast();
|
||||
|
||||
StreamSubscription<MqttMessage>? _subscription;
|
||||
|
||||
/// 当前监听的设备 SN
|
||||
String? _deviceSn;
|
||||
|
||||
/// 收帧计数(用于标记首帧)
|
||||
int _vehicleMsgCount = 0;
|
||||
int _locationMsgCount = 0;
|
||||
int _routeMsgCount = 0;
|
||||
|
||||
MowerRealtimeDataSourceImpl(this.mqttClient);
|
||||
|
||||
@override
|
||||
Stream<RealTimeMessageEntity> get vehicleStream => _vehicleController.stream;
|
||||
|
||||
@override
|
||||
Stream<MowerLocationEntity> get locationStream => _locationController.stream;
|
||||
|
||||
@override
|
||||
Stream<RouteStatusEntity> get routeStatusStream => _routeController.stream;
|
||||
|
||||
String _realtimeTopic(String sn) => 'mower/$sn/$_realtimeSuffix';
|
||||
String _locationTopic(String sn) => 'mower/$sn/$_locationSuffix';
|
||||
String _routeTopic(String sn) => 'device/$sn/$_routeSuffix';
|
||||
|
||||
@override
|
||||
Future<void> startListening({required String deviceSn}) async {
|
||||
if (deviceSn.isEmpty) {
|
||||
debugPrint('${LogTime.now()} ⚠️ [MowerRealtimeDataSource] 订阅跳过:设备SN为空');
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔥 同一设备重复调用:去重,不重复订阅(参照 DroneOsdDataSource 已验证的模式)
|
||||
if (_deviceSn == deviceSn && _subscription != null) {
|
||||
debugPrint('${LogTime.now()} [MowerRealtimeDataSource] 已在监听该设备,跳过: $deviceSn');
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔥 切换设备:先退订旧设备的主题,防止数据串台
|
||||
if (_deviceSn != null && _deviceSn!.isNotEmpty) {
|
||||
await mqttClient.unsubscribe(_realtimeTopic(_deviceSn!));
|
||||
await mqttClient.unsubscribe(_locationTopic(_deviceSn!));
|
||||
await mqttClient.unsubscribe(_routeTopic(_deviceSn!));
|
||||
debugPrint('${LogTime.now()} 🔕 [MowerRealtimeDataSource] 已退订旧设备: $_deviceSn');
|
||||
}
|
||||
|
||||
_deviceSn = deviceSn;
|
||||
_vehicleMsgCount = 0;
|
||||
_locationMsgCount = 0;
|
||||
_routeMsgCount = 0;
|
||||
|
||||
await mqttClient.subscribe(_realtimeTopic(deviceSn));
|
||||
await mqttClient.subscribe(_locationTopic(deviceSn));
|
||||
await mqttClient.subscribe(_routeTopic(deviceSn));
|
||||
|
||||
// 消息流监听只建立一次,按 _deviceSn 过滤
|
||||
_subscription ??= mqttClient.messageStream?.listen(_handleMessage);
|
||||
|
||||
debugPrint('${LogTime.now()} ✅ [MowerRealtimeDataSource] 开始监听设备: $deviceSn');
|
||||
debugPrint(' 车辆实时: ${_realtimeTopic(deviceSn)}');
|
||||
debugPrint(' 定位: ${_locationTopic(deviceSn)}');
|
||||
debugPrint(' 路径规划: ${_routeTopic(deviceSn)}');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopListening() async {
|
||||
if (_deviceSn == null || _deviceSn!.isEmpty) return;
|
||||
|
||||
await mqttClient.unsubscribe(_realtimeTopic(_deviceSn!));
|
||||
await mqttClient.unsubscribe(_locationTopic(_deviceSn!));
|
||||
await mqttClient.unsubscribe(_routeTopic(_deviceSn!));
|
||||
debugPrint('${LogTime.now()} 🔕 [MowerRealtimeDataSource] 停止监听设备: $_deviceSn');
|
||||
_deviceSn = null;
|
||||
}
|
||||
|
||||
void _handleMessage(MqttMessage message) {
|
||||
final sn = _deviceSn;
|
||||
if (sn == null || sn.isEmpty) return;
|
||||
|
||||
// 🔥 防串台:只处理当前监听设备的主题(mower/ 和 device/ 前缀都要匹配)
|
||||
if (!message.topic.contains('mower/$sn/') &&
|
||||
!message.topic.contains('device/$sn/')) return;
|
||||
|
||||
// 🔥 第一阶段:打印原始报文,确认格式后再完善解析
|
||||
debugPrint('${LogTime.now()} 📥 [MQTT-机器状态] ──────── 收到消息 ────────');
|
||||
debugPrint('${LogTime.now()} 📥 [MQTT-机器状态] topic: ${message.topic}');
|
||||
debugPrint('${LogTime.now()} 📥 [MQTT-机器状态] payload: ${message.payload}');
|
||||
|
||||
try {
|
||||
final jsonData = jsonDecode(message.payload) as Map<String, dynamic>;
|
||||
|
||||
if (message.topic.endsWith(_realtimeSuffix)) {
|
||||
// {name, value, unit} 数据点格式与 RealTimeMessageEntity 吻合,防御性解析
|
||||
final vehicle = RealTimeMessageEntity.fromJson(jsonData);
|
||||
if (vehicle.data.isNotEmpty) {
|
||||
_vehicleMsgCount++;
|
||||
if (_vehicleMsgCount == 1) {
|
||||
debugPrint('${LogTime.now()} 🎉 [MQTT-机器状态] 车辆实时首帧到达: $sn');
|
||||
}
|
||||
debugPrint('${LogTime.now()} 📊 [MQTT-机器状态] 车辆实时第$_vehicleMsgCount帧 解析后: type=${vehicle.type}, 数据点=${vehicle.data.length}个');
|
||||
for (final dp in vehicle.data) {
|
||||
debugPrint('${LogTime.now()} ├ ${dp.name} = ${dp.value}${dp.unit}');
|
||||
}
|
||||
_vehicleController.add(vehicle);
|
||||
}
|
||||
} else if (message.topic.endsWith(_locationSuffix)) {
|
||||
_locationMsgCount++;
|
||||
if (_locationMsgCount == 1) {
|
||||
debugPrint('${LogTime.now()} 🎉 [MQTT-机器状态] 定位首帧到达: $sn');
|
||||
}
|
||||
// 🔥 关键修复:坐标在 data 子对象中,不是顶层
|
||||
final dataObj = jsonData['data'] as Map<String, dynamic>? ?? jsonData;
|
||||
final location = MowerLocationEntity.fromJson(dataObj);
|
||||
debugPrint('${LogTime.now()} 📍 [MQTT-机器状态] 定位第$_locationMsgCount帧 解析后: lat=${location.latitude}, lng=${location.longitude}, alt=${location.altitude}, heading=${location.heading}, speed=${location.speed}');
|
||||
_locationController.add(location);
|
||||
} else if (message.topic.endsWith(_routeSuffix)) {
|
||||
_routeMsgCount++;
|
||||
if (_routeMsgCount == 1) {
|
||||
debugPrint('${LogTime.now()} 🎉 [MQTT-机器状态] 路径规划首帧到达: $sn');
|
||||
}
|
||||
final routeMsg = RouteStatusEntity.fromJson(jsonData);
|
||||
debugPrint('${LogTime.now()} 🛤️ [MQTT-机器状态] 路径规划第$_routeMsgCount帧 - 设备: ${routeMsg.deviceId}, 任务: ${routeMsg.taskId}, 点数: ${routeMsg.data.length}');
|
||||
_routeController.add(routeMsg);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('${LogTime.now()} ❌ [MowerRealtimeDataSource] 解析消息失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
stopListening();
|
||||
_subscription?.cancel();
|
||||
_subscription = null;
|
||||
_vehicleController.close();
|
||||
_locationController.close();
|
||||
_routeController.close();
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:mqtt_client/mqtt_client.dart' as mqtt;
|
||||
import 'package:mqtt_client/mqtt_server_client.dart' as mqtt_server;
|
||||
|
||||
import '../../../../logging/log_time.dart';
|
||||
import '../../domain/interfaces/mqtt_client.dart';
|
||||
import '../../domain/models/mqtt_config.dart';
|
||||
import '../../domain/models/mqtt_message.dart';
|
||||
@@ -13,6 +14,9 @@ class MqttClientImpl implements MqttClient {
|
||||
final _messageController = StreamController<MqttMessage>.broadcast();
|
||||
final Map<String, int> _subscriptionRefs = <String, int>{};
|
||||
|
||||
/// 客户端名称,用于日志区分(如 droneOsdClient / taskMessageClient / mowerRealtimeClient)
|
||||
final String clientName;
|
||||
|
||||
MqttConfig? _currentConfig;
|
||||
StreamSubscription? _updatesSubscription;
|
||||
Timer? _reconnectTimer;
|
||||
@@ -20,6 +24,8 @@ class MqttClientImpl implements MqttClient {
|
||||
bool _isConnecting = false;
|
||||
bool _manualDisconnect = false;
|
||||
|
||||
MqttClientImpl({this.clientName = 'MqttClient'});
|
||||
|
||||
@override
|
||||
Stream<MqttMessage>? get messageStream => _messageController.stream;
|
||||
|
||||
@@ -50,13 +56,13 @@ class MqttClientImpl implements MqttClient {
|
||||
_client = _createClient(config);
|
||||
_configureClient(_client!, config);
|
||||
|
||||
debugPrint('[MqttClient] connecting to ${config.connectionAddress}');
|
||||
debugPrint('${LogTime.now()} 🔌 [$clientName] 正在连接 ${config.connectionAddress}:${config.port} (clientId=${config.clientId})');
|
||||
await _client!.connect(config.username, config.password);
|
||||
|
||||
if (_client!.connectionStatus?.state ==
|
||||
mqtt.MqttConnectionState.connected) {
|
||||
_isConnected = true;
|
||||
debugPrint('[MqttClient] connected');
|
||||
debugPrint('${LogTime.now()} ✅ [$clientName] 连接成功 ${config.connectionAddress}:${config.port}');
|
||||
_listenToMessages();
|
||||
_resubscribeAll();
|
||||
} else {
|
||||
@@ -65,7 +71,7 @@ class MqttClientImpl implements MqttClient {
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[MqttClient] connect error: $e');
|
||||
debugPrint('${LogTime.now()} ❌ [$clientName] 连接失败: $e');
|
||||
_isConnected = false;
|
||||
_isConnecting = false;
|
||||
_scheduleReconnect();
|
||||
@@ -153,7 +159,7 @@ class MqttClientImpl implements MqttClient {
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('[MqttClient] subscribe: $topic');
|
||||
debugPrint('${LogTime.now()} 📮 [$clientName] 发起订阅: $topic');
|
||||
_client!.subscribe(topic, mqtt.MqttQos.atLeastOnce);
|
||||
}
|
||||
|
||||
@@ -173,7 +179,7 @@ class MqttClientImpl implements MqttClient {
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('[MqttClient] unsubscribe: $topic');
|
||||
debugPrint('${LogTime.now()} 🔕 [$clientName] 退订: $topic');
|
||||
_client!.unsubscribe(topic);
|
||||
}
|
||||
|
||||
@@ -198,7 +204,7 @@ class MqttClientImpl implements MqttClient {
|
||||
(msg.payload as mqtt.MqttPublishMessage).payload.message,
|
||||
);
|
||||
|
||||
debugPrint('[MqttClient] received [$topic]: $payload');
|
||||
debugPrint('${LogTime.now()} 📥 [$clientName] 收到消息 [$topic]: $payload');
|
||||
_messageController.add(MqttMessage(topic: topic, payload: payload));
|
||||
}
|
||||
},
|
||||
@@ -215,7 +221,7 @@ class MqttClientImpl implements MqttClient {
|
||||
if (_subscriptionRefs.isEmpty || _client == null) return;
|
||||
|
||||
for (final topic in _subscriptionRefs.keys) {
|
||||
debugPrint('[MqttClient] resubscribe: $topic');
|
||||
debugPrint('${LogTime.now()} 📮 [$clientName] 重连后重新订阅: $topic');
|
||||
_client!.subscribe(topic, mqtt.MqttQos.atLeastOnce);
|
||||
}
|
||||
}
|
||||
@@ -226,7 +232,7 @@ class MqttClientImpl implements MqttClient {
|
||||
}
|
||||
|
||||
void _onDisconnected() {
|
||||
debugPrint('[MqttClient] disconnected callback');
|
||||
debugPrint('${LogTime.now()} ⚠️ [$clientName] 连接断开');
|
||||
_isConnected = false;
|
||||
if (!_manualDisconnect) {
|
||||
_scheduleReconnect();
|
||||
@@ -234,7 +240,7 @@ class MqttClientImpl implements MqttClient {
|
||||
}
|
||||
|
||||
void _onSubscribed(String topic) {
|
||||
debugPrint('[MqttClient] subscribed: $topic');
|
||||
debugPrint('${LogTime.now()} ✅ [$clientName] 订阅确认成功: $topic');
|
||||
}
|
||||
|
||||
void _scheduleReconnect() {
|
||||
@@ -246,12 +252,12 @@ class MqttClientImpl implements MqttClient {
|
||||
if (_reconnectTimer?.isActive ?? false) return;
|
||||
|
||||
final delay = Duration(milliseconds: config.reconnectDelayMs);
|
||||
debugPrint('[MqttClient] reconnect scheduled in ${delay.inMilliseconds}ms');
|
||||
debugPrint('${LogTime.now()} ⏳ [$clientName] ${delay.inMilliseconds}ms 后尝试重连');
|
||||
_reconnectTimer = Timer(delay, () async {
|
||||
if (_manualDisconnect || _isConnected || _isConnecting) return;
|
||||
|
||||
try {
|
||||
debugPrint('[MqttClient] reconnecting...');
|
||||
debugPrint('${LogTime.now()} 🔄 [$clientName] 开始重连...');
|
||||
await connect(config);
|
||||
} catch (e) {
|
||||
debugPrint('[MqttClient] reconnect failed: $e');
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'dart:async';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
|
||||
import '../../../../../core/error/failure.dart';
|
||||
import '../datasources/mower_realtime_datasource.dart';
|
||||
import '../../domain/repositories/mower_realtime_repository.dart';
|
||||
import '../../domain/entities/mower_location_entity.dart';
|
||||
import '../../domain/entities/real_time_message_entity.dart';
|
||||
import '../../domain/entities/route_status_entity.dart';
|
||||
|
||||
class MowerRealtimeRepositoryImpl implements MowerRealtimeRepository {
|
||||
final MowerRealtimeDataSource dataSource;
|
||||
|
||||
MowerRealtimeRepositoryImpl(this.dataSource);
|
||||
|
||||
@override
|
||||
Stream<RealTimeMessageEntity> get vehicleStream => dataSource.vehicleStream;
|
||||
|
||||
@override
|
||||
Stream<MowerLocationEntity> get locationStream => dataSource.locationStream;
|
||||
|
||||
@override
|
||||
Stream<RouteStatusEntity> get routeStatusStream =>
|
||||
dataSource.routeStatusStream;
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> startListening({
|
||||
required String deviceSn,
|
||||
}) async {
|
||||
try {
|
||||
await dataSource.startListening(deviceSn: deviceSn);
|
||||
return right(null);
|
||||
} catch (e) {
|
||||
return left(Failure(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopListening() async {
|
||||
await dataSource.stopListening();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// 割草机定位消息实体(MQTT 主题 mower/{sn}/property/location/post,5-10Hz)
|
||||
///
|
||||
/// 报文格式尚未最终确认(第一阶段仅打印),fromLen 对常见字段名做宽容解析,
|
||||
/// 确认后如与实际不符在此调整即可,不影响上层。
|
||||
class MowerLocationEntity extends Equatable {
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
final double? altitude;
|
||||
|
||||
/// 航向角
|
||||
final double? heading;
|
||||
|
||||
/// 速度
|
||||
final double? speed;
|
||||
final String? timestamp;
|
||||
|
||||
/// 原始报文,便于解析格式确认后补充字段
|
||||
final Map<String, dynamic> rawData;
|
||||
|
||||
const MowerLocationEntity({
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
this.altitude,
|
||||
this.heading,
|
||||
this.speed,
|
||||
this.timestamp,
|
||||
this.rawData = const {},
|
||||
});
|
||||
|
||||
bool get isValid => latitude != null && longitude != null;
|
||||
|
||||
factory MowerLocationEntity.fromJson(Map<String, dynamic> json) {
|
||||
return MowerLocationEntity(
|
||||
latitude: _toDouble(json['latitude'] ?? json['lat']),
|
||||
longitude: _toDouble(json['longitude'] ?? json['lng'] ?? json['lon']),
|
||||
altitude: _toDouble(json['altitude'] ?? json['height']),
|
||||
heading: _toDouble(json['heading'] ?? json['yaw']),
|
||||
speed: _toDouble(json['speed']),
|
||||
timestamp: json['timestamp']?.toString(),
|
||||
rawData: json,
|
||||
);
|
||||
}
|
||||
|
||||
static double? _toDouble(dynamic value) {
|
||||
if (value == null) return null;
|
||||
if (value is num) return value.toDouble();
|
||||
return double.tryParse(value.toString());
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [latitude, longitude, altitude, heading, speed, timestamp];
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// MQTT 路径规划消息实体
|
||||
/// 对应后端 Java DTO: WebRouteStatusMessageDTO
|
||||
/// Topic: device/{deviceId}/route/post
|
||||
class RouteStatusEntity extends Equatable {
|
||||
final List<LatAndLngEntity> data;
|
||||
final String deviceId;
|
||||
final int taskId;
|
||||
final String type;
|
||||
|
||||
const RouteStatusEntity({
|
||||
required this.data,
|
||||
required this.deviceId,
|
||||
required this.taskId,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
factory RouteStatusEntity.fromJson(Map<String, dynamic> json) {
|
||||
final dataList = json['data'] as List<dynamic>? ?? [];
|
||||
return RouteStatusEntity(
|
||||
data: dataList.map((item) => LatAndLngEntity.fromJson(item)).toList(),
|
||||
deviceId: json['deviceId'] as String? ?? '',
|
||||
taskId: (json['taskId'] as num?)?.toInt() ?? 0,
|
||||
type: json['type'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [data, deviceId, taskId, type];
|
||||
}
|
||||
|
||||
/// 经纬度坐标实体
|
||||
/// 对应后端 Java DTO: LatAndLngEntity
|
||||
class LatAndLngEntity extends Equatable {
|
||||
final double lat;
|
||||
final double lng;
|
||||
|
||||
const LatAndLngEntity({required this.lat, required this.lng});
|
||||
|
||||
factory LatAndLngEntity.fromJson(Map<String, dynamic> json) {
|
||||
return LatAndLngEntity(
|
||||
lat: (json['lat'] as num?)?.toDouble() ?? 0.0,
|
||||
lng: (json['lng'] as num?)?.toDouble() ?? 0.0,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [lat, lng];
|
||||
}
|
||||
@@ -81,6 +81,21 @@ class MqttConfig extends Equatable {
|
||||
);
|
||||
}
|
||||
|
||||
/// 🔥 割草机机器状态(TCP MQTT)——独立连接,地址 1.95.137.212:59007
|
||||
/// 仅用于 mower/{sn}/property/realtime/post 和 mower/{sn}/property/location/post
|
||||
factory MqttConfig.mowerRealtime() {
|
||||
return const MqttConfig(
|
||||
host: '1.95.137.212',
|
||||
port: 59007,
|
||||
username: 'maibu',
|
||||
password: 'jsmbzn520',
|
||||
protocol: MqttProtocol.tcp,
|
||||
clientId: 'mower_realtime_client',
|
||||
cleanSession: true,
|
||||
reconnectDelayMs: 3000,
|
||||
);
|
||||
}
|
||||
|
||||
MqttConfig copyWith({
|
||||
String? host,
|
||||
int? port,
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'dart:async';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
|
||||
import '../../../../../core/error/failure.dart';
|
||||
import '../entities/mower_location_entity.dart';
|
||||
import '../entities/real_time_message_entity.dart';
|
||||
import '../entities/route_status_entity.dart';
|
||||
|
||||
abstract class MowerRealtimeRepository {
|
||||
Stream<RealTimeMessageEntity> get vehicleStream;
|
||||
Stream<MowerLocationEntity> get locationStream;
|
||||
Stream<RouteStatusEntity> get routeStatusStream;
|
||||
|
||||
Future<Either<Failure, void>> startListening({
|
||||
required String deviceSn,
|
||||
});
|
||||
|
||||
Future<void> stopListening();
|
||||
}
|
||||
@@ -24,10 +24,12 @@ class MqttManager {
|
||||
|
||||
final droneOsdClient = sl<MqttClient>(instanceName: 'droneOsdClient');
|
||||
final taskMessageClient = sl<MqttClient>(instanceName: 'taskMessageClient');
|
||||
final mowerRealtimeClient = sl<MqttClient>(instanceName: 'mowerRealtimeClient');
|
||||
|
||||
// 独立连接两个 MQTT 客户端,互不影响
|
||||
// 独立连接三个 MQTT 客户端,互不影响
|
||||
await _connectClient(droneOsdClient, MqttConfig.droneOsd(), 'droneOsdClient');
|
||||
await _connectClient(taskMessageClient, MqttConfig.taskMessage(), 'taskMessageClient');
|
||||
await _connectClient(mowerRealtimeClient, MqttConfig.mowerRealtime(), 'mowerRealtimeClient');
|
||||
|
||||
_isInitialized = true;
|
||||
debugPrint('✅ [MqttManager] MQTT 初始化完成');
|
||||
@@ -53,9 +55,11 @@ class MqttManager {
|
||||
|
||||
final droneOsdClient = sl<MqttClient>(instanceName: 'droneOsdClient');
|
||||
final taskMessageClient = sl<MqttClient>(instanceName: 'taskMessageClient');
|
||||
final mowerRealtimeClient = sl<MqttClient>(instanceName: 'mowerRealtimeClient');
|
||||
|
||||
await droneOsdClient.disconnect();
|
||||
await taskMessageClient.disconnect();
|
||||
await mowerRealtimeClient.disconnect();
|
||||
|
||||
_isInitialized = false;
|
||||
debugPrint('✅ [MqttManager] 所有 MQTT 连接已断开');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_patcher/flutter_patcher.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
@@ -56,6 +57,7 @@ class VersionCheckService {
|
||||
final Dio _dio = Dio(BaseOptions(
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 10),
|
||||
validateStatus: (status) => true,
|
||||
));
|
||||
|
||||
static const String apiUrl = 'http://8.159.134.0:8012/api/update/check';
|
||||
@@ -67,29 +69,26 @@ class VersionCheckService {
|
||||
required int currentVersionCode,
|
||||
}) async {
|
||||
try {
|
||||
_logger.i('🔍 检查版本更新');
|
||||
_logger.i('📱 APK 自带版本: $currentVersion ($currentVersionCode)');
|
||||
debugPrint('');
|
||||
debugPrint('========================================');
|
||||
debugPrint(' UPDATE CHECK START');
|
||||
debugPrint('========================================');
|
||||
debugPrint('[1] APK version: $currentVersion ($currentVersionCode)');
|
||||
|
||||
// 从文件读取已应用的补丁版本
|
||||
final patchInfo = await _loadPatchVersionInfo();
|
||||
final appliedPatchVersion = patchInfo['version'] as String?;
|
||||
final appliedPatchVersionCode = patchInfo['versionCode'] as int?;
|
||||
|
||||
_logger.i('📂 读取到的补丁版本: $appliedPatchVersion ($appliedPatchVersionCode)');
|
||||
debugPrint('[2] Patch file version: $appliedPatchVersion ($appliedPatchVersionCode)');
|
||||
|
||||
// 如果打过补丁,使用补丁的版本;否则使用 APK 自带版本
|
||||
final requestVersion = appliedPatchVersion ?? currentVersion;
|
||||
final requestVersionCode = appliedPatchVersionCode ?? currentVersionCode;
|
||||
// 始终用 APK 基础版本请求服务端,让服务端返回最新版本
|
||||
// 客户端自己判断本地是否已经是最新
|
||||
final requestVersion = currentVersion;
|
||||
final requestVersionCode = currentVersionCode;
|
||||
|
||||
_logger.i('🚀 请求后端版本: $requestVersion ($requestVersionCode)');
|
||||
|
||||
// 🔥 打印完整的请求参数
|
||||
_logger.i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
_logger.i('📤 App 发送的请求参数:');
|
||||
_logger.i(' URL: $apiUrl');
|
||||
_logger.i(' version: $requestVersion');
|
||||
_logger.i(' versionCode: $requestVersionCode');
|
||||
_logger.i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
debugPrint('[3] Request to server: version=$requestVersion, versionCode=$requestVersionCode');
|
||||
debugPrint('[4] URL: $apiUrl?version=$requestVersion&versionCode=$requestVersionCode');
|
||||
|
||||
final response = await _dio.get(
|
||||
apiUrl,
|
||||
@@ -99,47 +98,67 @@ class VersionCheckService {
|
||||
},
|
||||
);
|
||||
|
||||
// 🔥 打印完整的后端返回数据
|
||||
_logger.i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
_logger.i('📥 后端返回的完整数据:');
|
||||
_logger.i(' statusCode: ${response.statusCode}');
|
||||
_logger.i(' 原始响应: ${response.data}');
|
||||
_logger.i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
debugPrint('[5] HTTP status: ${response.statusCode}');
|
||||
debugPrint('[6] Response body: ${response.data}');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final respCode = response.data['code'];
|
||||
final data = response.data['data'];
|
||||
final message = response.data['message'] ?? '';
|
||||
|
||||
debugPrint('[7] respCode=$respCode, data=$data, message=$message');
|
||||
|
||||
if (respCode != 200 || data == null) {
|
||||
debugPrint('[RESULT] No update - server returned code=$respCode, data=${data == null ? "NULL" : "NOT_NULL"}, message=$message');
|
||||
debugPrint('========================================');
|
||||
debugPrint('');
|
||||
return null;
|
||||
}
|
||||
|
||||
debugPrint('[7.1] data type: ${data.runtimeType}');
|
||||
debugPrint('[7.2] data keys: ${data is Map ? data.keys.toList() : "NOT A MAP"}');
|
||||
debugPrint('[7.3] hasUpdate = ${data['hasUpdate']} (type: ${data['hasUpdate']?.runtimeType})');
|
||||
debugPrint('[7.4] version = ${data['version']}');
|
||||
debugPrint('[7.5] versionCode = ${data['versionCode']}');
|
||||
debugPrint('[7.6] updateType = ${data['updateType']}');
|
||||
debugPrint('[7.7] patch = ${data['patch']}');
|
||||
debugPrint('[7.8] fullApk = ${data['fullApk']}');
|
||||
|
||||
if (data['hasUpdate'] == false) {
|
||||
_logger.i('✅ 已是最新版本');
|
||||
debugPrint('[RESULT] No update - hasUpdate=false');
|
||||
debugPrint('========================================');
|
||||
debugPrint('');
|
||||
return null;
|
||||
}
|
||||
|
||||
final versionInfo = AppVersionInfo.fromJson(data);
|
||||
debugPrint('[8] Server version: ${versionInfo.version} (${versionInfo.versionCode})');
|
||||
debugPrint('[9] Update type: ${versionInfo.updateType}, force: ${versionInfo.forceUpdate}');
|
||||
debugPrint('[10] patchUrl: ${versionInfo.patchUrl}');
|
||||
debugPrint('[11] patchMd5: ${versionInfo.patchMd5}');
|
||||
debugPrint('[12] apkUrl: ${versionInfo.apkUrl}');
|
||||
|
||||
// 🔥 关键修复:无论更新类型是什么,只要本地已应用此版本,就跳过
|
||||
// 只用版本字符串判断是否已更新:服务端 hasUpdate=true 说了算
|
||||
if (appliedPatchVersion != null && appliedPatchVersion == versionInfo.version) {
|
||||
_logger.i('✅ 本地已应用版本 ${versionInfo.version},跳过更新(updateType: ${versionInfo.updateType})');
|
||||
debugPrint('[RESULT] No update - already at version ${versionInfo.version}');
|
||||
debugPrint('========================================');
|
||||
debugPrint('');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 🔥 额外检查:如果本地补丁版本code >= 服务端返回的versionCode,也跳过
|
||||
if (appliedPatchVersionCode != null && appliedPatchVersionCode >= versionInfo.versionCode) {
|
||||
_logger.i('✅ 本地补丁版本code ($appliedPatchVersionCode) >= 服务端versionCode (${versionInfo.versionCode}),跳过更新');
|
||||
return null;
|
||||
}
|
||||
debugPrint('[8.1] Local patch version: ${appliedPatchVersion ?? "none"}');
|
||||
debugPrint('[8.2] Server version: ${versionInfo.version}');
|
||||
debugPrint('[8.3] Version different: ${appliedPatchVersion != versionInfo.version}');
|
||||
|
||||
_logger.i('✅ 发现新版本: ${versionInfo.version}');
|
||||
_logger.i('🔄 更新类型: ${versionInfo.updateType}');
|
||||
_logger.i('⚠️ 强制更新: ${versionInfo.forceUpdate}');
|
||||
debugPrint('[RESULT] NEW VERSION FOUND: ${versionInfo.version} (${versionInfo.versionCode})');
|
||||
debugPrint('========================================');
|
||||
debugPrint('');
|
||||
|
||||
return versionInfo;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('[RESULT] ERROR: $e');
|
||||
debugPrint('========================================');
|
||||
debugPrint('');
|
||||
return null;
|
||||
} catch (e, stack) {
|
||||
_logger.e('❌ 版本检查失败: $e');
|
||||
_logger.e('❌ 堆栈信息: $stack');
|
||||
throw Exception('网络请求失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ abstract class DeviceTaskDatasource {
|
||||
required int taskId,
|
||||
required int orgId,
|
||||
required int siteId,
|
||||
bool forceCancel = true,
|
||||
});
|
||||
|
||||
Future<bool> pauseTask({
|
||||
@@ -90,6 +91,7 @@ class DeviceTaskDatasourceImpl implements DeviceTaskDatasource {
|
||||
required int taskId,
|
||||
required int orgId,
|
||||
required int siteId,
|
||||
bool forceCancel = true,
|
||||
}) async {
|
||||
final url = '${HttpApiConsts.baseUrl}/iot/deviceTask/cancelTask';
|
||||
final body = {
|
||||
@@ -97,6 +99,7 @@ class DeviceTaskDatasourceImpl implements DeviceTaskDatasource {
|
||||
'taskId': taskId,
|
||||
'orgId': orgId,
|
||||
'siteId': siteId,
|
||||
'forceCancel': forceCancel,
|
||||
};
|
||||
_logger.logWithLevel('[cancelTask] 请求: POST $url');
|
||||
_logger.logWithLevel('[cancelTask] 参数: ${jsonEncode(body)}');
|
||||
|
||||
@@ -34,7 +34,7 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
|
||||
logger.logWithLevel(
|
||||
'用户点击bindDevice方法API 请求开始',
|
||||
level: 'INFO',
|
||||
data: {'url': 'http://1.95.137.212:59003/iot/device/bindDevice'},
|
||||
data: {'url': '${HttpApiConsts.baseUrl}/iot/device/bindDevice'},
|
||||
);
|
||||
var response = await dio.post(
|
||||
HttpApiConsts.bindDevice,
|
||||
@@ -44,7 +44,7 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
|
||||
'请求详细参数',
|
||||
level: 'DEBUG',
|
||||
data: {
|
||||
'url': 'http://1.95.137.212:59003/iot/device/bindDevice',
|
||||
'url': '${HttpApiConsts.baseUrl}/iot/device/bindDevice',
|
||||
'deviceId': deviceId,
|
||||
'deviceAlias': deviceAlias,
|
||||
},
|
||||
@@ -122,7 +122,7 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
|
||||
logger.logWithLevel(
|
||||
'用户点击switchDevice方法API 请求开始',
|
||||
level: 'INFO',
|
||||
data: {'url': 'http://1.95.137.212:59003/iot/device/switchDevice'},
|
||||
data: {'url': '${HttpApiConsts.baseUrl}/iot/device/switchDevice'},
|
||||
);
|
||||
var response = await dio.post(
|
||||
HttpApiConsts.switchDevice,
|
||||
@@ -138,7 +138,7 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
|
||||
'请求详细参数',
|
||||
level: 'DEBUG',
|
||||
data: {
|
||||
'url': 'http://1.95.137.212:59003/iot/device/switchDevice',
|
||||
'url': '${HttpApiConsts.baseUrl}/iot/device/switchDevice',
|
||||
'platform': platform,
|
||||
'deviceId': deviceId,
|
||||
},
|
||||
@@ -219,12 +219,12 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
|
||||
'请求详细参数',
|
||||
level: 'DEBUG',
|
||||
data: {
|
||||
'url': 'http://1.95.137.212:59003/iot/device/userDevice',
|
||||
'url': '${HttpApiConsts.baseUrl}/iot/device/userDevice',
|
||||
'tenantName': deviceame,
|
||||
},
|
||||
);
|
||||
final response = await dio.get(
|
||||
'http://1.95.137.212:59003/iot/device/userDevice',
|
||||
'${HttpApiConsts.baseUrl}/iot/device/userDevice',
|
||||
queryParameters: {'tenantName': deviceame},
|
||||
// options: Options(
|
||||
// headers: {
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:maibu_satabot_v2/features/devices/domain/errors/device_failure.d
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/repositories/device_hostrity_work_repository.dart';
|
||||
|
||||
import '../../../../core/logging/i_logger_service.dart';
|
||||
import '../../../../core/consts/http_api_consts.dart';
|
||||
|
||||
class DeviceHostrityWorkRepositoryImpl implements DeviceHostrityWorkRepositoryRepository {
|
||||
final Dio client;
|
||||
@@ -35,7 +36,7 @@ class DeviceHostrityWorkRepositoryImpl implements DeviceHostrityWorkRepositoryRe
|
||||
_logger.logWithLevel('用户信息获取成功,Token: $token', level: 'info');
|
||||
|
||||
final response = await client.get(
|
||||
'http://1.95.137.212:59003/iot/device/getDeviceRunStatistics?deviceId=$deviceId',
|
||||
'${HttpApiConsts.baseUrl}/iot/device/getDeviceRunStatistics?deviceId=$deviceId',
|
||||
options: Options(headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer $token'}),
|
||||
);
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ class DeviceTaskRepositoryImpl implements DeviceTaskRepository {
|
||||
required int taskId,
|
||||
required int orgId,
|
||||
required int siteId,
|
||||
bool forceCancel = true,
|
||||
}) async {
|
||||
try {
|
||||
final result = await datasource.cancelTask(
|
||||
@@ -44,6 +45,7 @@ class DeviceTaskRepositoryImpl implements DeviceTaskRepository {
|
||||
taskId: taskId,
|
||||
orgId: orgId,
|
||||
siteId: siteId,
|
||||
forceCancel: forceCancel,
|
||||
);
|
||||
return Right(result);
|
||||
} catch (e) {
|
||||
|
||||
@@ -18,6 +18,7 @@ abstract class DeviceTaskRepository {
|
||||
required int taskId,
|
||||
required int orgId,
|
||||
required int siteId,
|
||||
bool forceCancel = true,
|
||||
});
|
||||
|
||||
/// 暂停任务
|
||||
|
||||
@@ -15,6 +15,7 @@ class CancelTaskUseCase implements BaseUseCase<bool, CancelTaskParams> {
|
||||
taskId: params.taskId,
|
||||
orgId: params.orgId,
|
||||
siteId: params.siteId,
|
||||
forceCancel: params.forceCancel,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -24,11 +25,13 @@ class CancelTaskParams {
|
||||
final int taskId;
|
||||
final int orgId;
|
||||
final int siteId;
|
||||
final bool forceCancel;
|
||||
|
||||
CancelTaskParams({
|
||||
required this.deviceId,
|
||||
required this.taskId,
|
||||
required this.orgId,
|
||||
required this.siteId,
|
||||
this.forceCancel = true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,10 +7,12 @@ import 'package:maibu_satabot_v2/features/devices/domain/entities/running_status
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/entities/gps_entity.dart';
|
||||
import 'package:maibu_satabot_v2/core/network/protocol_decoder.dart';
|
||||
import 'package:maibu_satabot_v2/core/network/mqtt/domain/repositories/task_message_repository.dart';
|
||||
import 'package:maibu_satabot_v2/core/network/mqtt/domain/repositories/mower_realtime_repository.dart';
|
||||
import 'package:maibu_satabot_v2/core/network/mqtt/domain/entities/task_arrive_entity.dart';
|
||||
import 'package:maibu_satabot_v2/core/network/mqtt/domain/entities/task_status_entity.dart';
|
||||
|
||||
import '../../../../core/logging/i_logger_service.dart';
|
||||
import '../../../../core/logging/log_time.dart';
|
||||
import '../../../../core/network/net_message_dispatcher.dart';
|
||||
import '../../../../core/network/tcp/tcp_client.dart';
|
||||
import 'device_status_event.dart';
|
||||
@@ -20,6 +22,10 @@ import 'devices_cubit.dart';
|
||||
// 定义全局的TaskMessageRepository获取方式
|
||||
TaskMessageRepository get _taskMessageRepo => GetIt.I<TaskMessageRepository>();
|
||||
|
||||
// 🔥 MQTT机器状态数据源(替代TCP 0x02)
|
||||
MowerRealtimeRepository get _mowerRealtimeRepo =>
|
||||
GetIt.I<MowerRealtimeRepository>();
|
||||
|
||||
class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
final NetMessageDispatcher _dispatcher;
|
||||
final TcpClient tcpClient; // 🔥 新增:直接访问 TcpClient
|
||||
@@ -30,6 +36,10 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
StreamSubscription? _mqttArriveSubscription;
|
||||
StreamSubscription? _mqttStatusSubscription;
|
||||
|
||||
// 🔥 MQTT机器状态订阅(车辆实时+定位,替代TCP 0x02)
|
||||
StreamSubscription? _mqttVehicleSubscription;
|
||||
StreamSubscription? _mqttLocationSubscription;
|
||||
|
||||
// 🔥 节流相关:500ms节流控制0x02数据推送频率
|
||||
Timer? _throttleTimer;
|
||||
static const _throttleDuration = Duration(milliseconds: 500);
|
||||
@@ -42,11 +52,17 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
// 🔥 当前监听的设备ID
|
||||
String? _currentDeviceId;
|
||||
|
||||
// 🔥 MQTT机器状态收帧计数(用于链路日志)
|
||||
int _mqttVehicleFrameCount = 0;
|
||||
int _mqttLocationFrameCount = 0;
|
||||
|
||||
DeviceStatusBloc(this._dispatcher, {TcpClient? client})
|
||||
: tcpClient = client ?? GetIt.I<TcpClient>(),
|
||||
super(DeviceStatusInitial()) {
|
||||
// 🔥 核心改动:直接在构造函数中建立TCP监听,类似RemoteControlCubit
|
||||
_initDirectTcpListener();
|
||||
// 🔥 已停用:机器状态数据源已改为MQTT(见 startMqttRealtimeListening,
|
||||
// 由用户切换设备时触发)。TCP监听逻辑保留,回滚时取消下行注释即可。
|
||||
// _initDirectTcpListener();
|
||||
|
||||
// 🔥 初始化MQTT到达点监听
|
||||
_initMqttArriveListener();
|
||||
@@ -191,6 +207,86 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
_logger.log('✅ [DeviceStatusBloc] 直接TCP监听器已建立完成');
|
||||
}
|
||||
|
||||
/// 🔥 MQTT机器状态监听(替代TCP 0x02数据源)
|
||||
///
|
||||
/// [deviceSn] 与TCP时代 switchDevice / connectBySwitch 的设备号获取方式一致
|
||||
/// (即 DeviceEntity.deviceName),由用户点击设备列表切换设备时调用。
|
||||
///
|
||||
/// 第一阶段:订阅并打印原始报文;确认格式后在第二阶段解析为
|
||||
/// RunningStatusEntity + GPSEntity,复用现有500ms节流逻辑。
|
||||
void startMqttRealtimeListening(String deviceSn) {
|
||||
if (deviceSn.isEmpty) {
|
||||
debugPrint('${LogTime.now()} ⚠️ [DeviceStatusBloc] MQTT订阅跳过:设备SN为空');
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('${LogTime.now()} 🚀 [DeviceStatusBloc] 启动MQTT机器状态监听: $deviceSn');
|
||||
|
||||
// 按SN订阅(数据源内部自动退旧订新、同SN去重)
|
||||
_mowerRealtimeRepo.startListening(deviceSn: deviceSn).then((result) {
|
||||
result.fold(
|
||||
(failure) => debugPrint(
|
||||
'${LogTime.now()} ❌ [DeviceStatusBloc] MQTT机器状态订阅失败: ${failure.message}',
|
||||
),
|
||||
(_) => debugPrint(
|
||||
'${LogTime.now()} ✅ [DeviceStatusBloc] MQTT机器状态订阅完成: $deviceSn',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
// 切换设备时重置收帧计数,便于观察新设备的首帧与频率
|
||||
_mqttVehicleFrameCount = 0;
|
||||
_mqttLocationFrameCount = 0;
|
||||
|
||||
// 流监听只建立一次,第一阶段仅打印解析结果验证链路
|
||||
_mqttVehicleSubscription ??= _mowerRealtimeRepo.vehicleStream.listen((
|
||||
message,
|
||||
) {
|
||||
_mqttVehicleFrameCount++;
|
||||
debugPrint(
|
||||
'${LogTime.now()} 📊 [MQTT-机器状态] 车辆实时第$_mqttVehicleFrameCount帧 type=${message.type} 数据点=${message.data.length}',
|
||||
);
|
||||
// TODO 第二阶段:将数据点映射到 RunningStatusEntity 字段,复用500ms节流emit
|
||||
});
|
||||
|
||||
_mqttLocationSubscription ??= _mowerRealtimeRepo.locationStream.listen((
|
||||
location,
|
||||
) {
|
||||
_mqttLocationFrameCount++;
|
||||
debugPrint(
|
||||
'${LogTime.now()} 📍 [MQTT-location] 定位第$_mqttLocationFrameCount帧 lat=${location.latitude}, lng=${location.longitude}, heading=${location.heading}',
|
||||
);
|
||||
|
||||
// 🔥 核心:用 MQTT location 数据更新缓存并节流 emit
|
||||
if (location.isValid) {
|
||||
// 更新 _cachedGps
|
||||
_cachedGps = GPSEntity(location.latitude!, location.longitude!);
|
||||
|
||||
// 更新 _cachedStatus 中的航向角和坐标(复用已有缓存或新建空壳)
|
||||
final base = _cachedStatus ?? RunningStatusEntity();
|
||||
_cachedStatus = base.copyWith(
|
||||
latitude: location.latitude!,
|
||||
longitude: location.longitude!,
|
||||
yaw: location.heading ?? base.yaw,
|
||||
);
|
||||
|
||||
// 500ms节流:与 TCP 0x02 时代逻辑一致
|
||||
if (_throttleTimer == null || !_throttleTimer!.isActive) {
|
||||
_throttleTimer = Timer(_throttleDuration, () {
|
||||
_emitCachedStatus();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
debugPrint(
|
||||
'${LogTime.now()} ⚠️ [MQTT-location] 坐标无效,跳过 - lat=${location.latitude}, lng=${location.longitude}',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
debugPrint('${LogTime.now()} ✅ [DeviceStatusBloc] MQTT机器状态监听已启动: $deviceSn');
|
||||
_logger.log('✅ [DeviceStatusBloc] MQTT机器状态监听已启动: $deviceSn');
|
||||
}
|
||||
|
||||
// 🔥 初始化MQTT到达点监听
|
||||
void _initMqttArriveListener() {
|
||||
debugPrint('🔗 [DeviceStatusBloc] 初始化MQTT到达点监听器');
|
||||
@@ -304,7 +400,7 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
// 🔥 节流发射:500ms到期后发射缓存的最新数据
|
||||
void _emitCachedStatus() {
|
||||
if (_cachedStatus != null && _cachedGps != null && !isClosed) {
|
||||
debugPrint('📤 [0x02] 节流发射 | 电压=${_cachedStatus!.voltage}V 电量=${_cachedStatus!.battery}% 经纬度=(${_cachedGps!.latitude}, ${_cachedGps!.longitude}) | ${DateTime.now().toString().substring(11, 19)}');
|
||||
debugPrint('📤 [节流发射] 电压=${_cachedStatus!.voltage}V 电量=${_cachedStatus!.battery}% 经纬度=(${_cachedGps!.latitude}, ${_cachedGps!.longitude}) yaw=${_cachedStatus!.yaw} | ${DateTime.now().toString().substring(11, 19)}');
|
||||
emit(DeviceStatusUpdated(_cachedStatus!, _cachedGps!));
|
||||
}
|
||||
}
|
||||
@@ -423,6 +519,12 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
_mqttStatusSubscription?.cancel();
|
||||
_mqttStatusSubscription = null;
|
||||
|
||||
// 🔥 清理MQTT机器状态订阅
|
||||
_mqttVehicleSubscription?.cancel();
|
||||
_mqttVehicleSubscription = null;
|
||||
_mqttLocationSubscription?.cancel();
|
||||
_mqttLocationSubscription = null;
|
||||
|
||||
// 🔥 清理节流timer和缓存
|
||||
_throttleTimer?.cancel();
|
||||
_throttleTimer = null;
|
||||
|
||||
@@ -34,21 +34,17 @@ class DeviceTaskCubit extends Cubit<DeviceTaskState> {
|
||||
final userCubit = GetIt.I<AppUserCubit>();
|
||||
final user = userCubit.state.user;
|
||||
if (user == null) {
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: '用户未登录',
|
||||
));
|
||||
emit(state.copyWith(isLoading: false, errorMessage: '用户未登录'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取场站ID
|
||||
// 获取场站信息(siteId 和 orgId 都从当前场站取)
|
||||
final siteCubit = GetIt.I<SiteCubit>();
|
||||
final siteId = siteCubit.state.selectedSite?.id;
|
||||
final selectedSite = siteCubit.state.selectedSite;
|
||||
final siteId = selectedSite?.id;
|
||||
final orgId = selectedSite?.orgId ?? 0;
|
||||
if (siteId == null) {
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: '未选择场站',
|
||||
));
|
||||
emit(state.copyWith(isLoading: false, errorMessage: '未选择场站'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -57,125 +53,153 @@ class DeviceTaskCubit extends Cubit<DeviceTaskState> {
|
||||
GetDeviceTaskPoolParams(
|
||||
userId: user.userId ?? '',
|
||||
siteId: siteId,
|
||||
orgId: user.orgId ?? 0,
|
||||
orgId: orgId,
|
||||
),
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
_logger.logWithLevel('❌ 获取任务池失败: ${failure.message}');
|
||||
emit(state.copyWith(
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: ErrorHandler.getErrorMessage(failure.message),
|
||||
shouldShowError: true, // 🔥 标记需要显示错误弹窗
|
||||
));
|
||||
),
|
||||
);
|
||||
},
|
||||
(taskList) {
|
||||
// 🔥 过滤出当前设备 + 活跃状态的任务(NEW, EXECUTING, PAUSE)
|
||||
// 🔥 过滤出当前设备 + 活跃状态的任务(NEW, EXECUTING, PAUSE, CANCELING, FAILED)
|
||||
final activeTasks = taskList.where((task) {
|
||||
// 1. 设备号匹配
|
||||
if (task.deviceId != deviceId) return false;
|
||||
|
||||
// 2. 状态过滤:只保留新建、执行中、暂停中的任务
|
||||
// 2. 状态过滤:保留新建、执行中、暂停中、取消中、执行失败的任务
|
||||
final status = task.taskStatus;
|
||||
return status == 'NEW' || // 新建
|
||||
status == 'EXECUTING' || // 执行中
|
||||
status == 'PAUSE'; // 暂停中
|
||||
status == 'PAUSE' || // 暂停中
|
||||
status == 'CANCELING' || // 取消中(用户可处理)
|
||||
status == 'FAILED'; // 执行失败(用户可恢复或取消)
|
||||
}).toList();
|
||||
|
||||
_logger.logWithLevel(
|
||||
'✅ 找到 ${activeTasks.length} 个活跃任务',
|
||||
'✅ [fetchAndFilterTask] deviceId=$deviceId, 任务池总数=${taskList.length}, 过滤后活跃任务数=${activeTasks.length}',
|
||||
);
|
||||
for (final t in taskList) {
|
||||
_logger.logWithLevel(
|
||||
' - taskId=${t.id}, deviceId=${t.deviceId}, status=${t.taskStatus}',
|
||||
);
|
||||
}
|
||||
|
||||
// 🔥 如果有多个任务,保存所有选项供用户选择
|
||||
// 如果只有1个,直接选中
|
||||
final currentTask = activeTasks.isNotEmpty ? activeTasks.first : null;
|
||||
|
||||
emit(state.copyWith(
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
taskPool: taskList,
|
||||
currentTask: currentTask,
|
||||
currentTaskId: currentTask?.id,
|
||||
activeTasks: activeTasks,
|
||||
));
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_logger.logWithLevel('❌ 获取任务池异常: $e');
|
||||
emit(state.copyWith(
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: ErrorHandler.getErrorMessage(e),
|
||||
shouldShowError: true, // 🔥 标记需要显示错误弹窗
|
||||
));
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 取消任务
|
||||
Future<void> cancelTask(String deviceId) async {
|
||||
Future<void> cancelTask(String deviceId, {bool forceCancel = true}) async {
|
||||
final taskId = state.currentTaskId;
|
||||
if (taskId == null) {
|
||||
emit(state.copyWith(errorMessage: '无可用任务'));
|
||||
return;
|
||||
}
|
||||
|
||||
emit(state.copyWith(
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: true,
|
||||
operationType: DeviceTaskOperationType.cancel,
|
||||
));
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
final userCubit = GetIt.I<AppUserCubit>();
|
||||
final user = userCubit.state.user;
|
||||
final siteCubit = GetIt.I<SiteCubit>();
|
||||
final siteId = siteCubit.state.selectedSite?.id;
|
||||
final selectedSite = siteCubit.state.selectedSite;
|
||||
final siteId = selectedSite?.id;
|
||||
final orgId = selectedSite?.orgId ?? 0;
|
||||
|
||||
if (user == null || siteId == null) {
|
||||
emit(state.copyWith(
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: '参数不完整',
|
||||
operationType: DeviceTaskOperationType.none,
|
||||
));
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final params = CancelTaskParams(
|
||||
deviceId: deviceId,
|
||||
taskId: taskId,
|
||||
orgId: user.orgId ?? 0,
|
||||
orgId: orgId,
|
||||
siteId: siteId,
|
||||
forceCancel: forceCancel,
|
||||
);
|
||||
_logger.logWithLevel('[取消任务] 请求: POST /iot/deviceTask/cancelTask');
|
||||
_logger.logWithLevel('[取消任务] 参数: deviceId=$deviceId, taskId=$taskId, orgId=${user.orgId ?? 0}, siteId=$siteId');
|
||||
_logger.logWithLevel(
|
||||
'[取消任务] 参数: deviceId=$deviceId, taskId=$taskId, orgId=$orgId, siteId=$siteId, forceCancel=$forceCancel',
|
||||
);
|
||||
final result = await _cancelTaskUseCase.call(params);
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
_logger.logWithLevel('[取消任务] 响应失败: ${failure.message}');
|
||||
_logger.logWithLevel('❌ 取消任务失败: ${failure.message}');
|
||||
emit(state.copyWith(
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: ErrorHandler.getErrorMessage(failure.message),
|
||||
operationType: DeviceTaskOperationType.none,
|
||||
shouldShowError: true, // 🔥 标记需要显示错误弹窗
|
||||
));
|
||||
),
|
||||
);
|
||||
},
|
||||
(success) {
|
||||
_logger.logWithLevel('[取消任务] 响应成功: $success');
|
||||
_logger.logWithLevel('✅ 取消任务成功');
|
||||
emit(state.copyWith(
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
operationType: DeviceTaskOperationType.none,
|
||||
));
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_logger.logWithLevel('❌ 取消任务异常: $e');
|
||||
emit(state.copyWith(
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: ErrorHandler.getErrorMessage(e),
|
||||
operationType: DeviceTaskOperationType.none,
|
||||
shouldShowError: true, // 🔥 标记需要显示错误弹窗
|
||||
));
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,64 +211,78 @@ class DeviceTaskCubit extends Cubit<DeviceTaskState> {
|
||||
return;
|
||||
}
|
||||
|
||||
emit(state.copyWith(
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: true,
|
||||
operationType: DeviceTaskOperationType.pause,
|
||||
));
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
final userCubit = GetIt.I<AppUserCubit>();
|
||||
final user = userCubit.state.user;
|
||||
final siteCubit = GetIt.I<SiteCubit>();
|
||||
final siteId = siteCubit.state.selectedSite?.id;
|
||||
final selectedSite = siteCubit.state.selectedSite;
|
||||
final siteId = selectedSite?.id;
|
||||
final orgId = selectedSite?.orgId ?? 0;
|
||||
|
||||
if (user == null || siteId == null) {
|
||||
emit(state.copyWith(
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: '参数不完整',
|
||||
operationType: DeviceTaskOperationType.none,
|
||||
));
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final params2 = PauseTaskParams(
|
||||
deviceId: deviceId,
|
||||
taskId: taskId,
|
||||
orgId: user.orgId ?? 0,
|
||||
orgId: orgId,
|
||||
siteId: siteId,
|
||||
);
|
||||
_logger.logWithLevel('[暂停任务] 请求: POST /iot/deviceTask/pauseTask');
|
||||
_logger.logWithLevel('[暂停任务] 参数: deviceId=$deviceId, taskId=$taskId, orgId=${user.orgId ?? 0}, siteId=$siteId');
|
||||
_logger.logWithLevel(
|
||||
'[暂停任务] 参数: deviceId=$deviceId, taskId=$taskId, orgId=$orgId, siteId=$siteId',
|
||||
);
|
||||
final result = await _pauseTaskUseCase.call(params2);
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
_logger.logWithLevel('[暂停任务] 响应失败: ${failure.message}');
|
||||
_logger.logWithLevel('❌ 暂停任务失败: ${failure.message}');
|
||||
emit(state.copyWith(
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: ErrorHandler.getErrorMessage(failure.message),
|
||||
operationType: DeviceTaskOperationType.none,
|
||||
shouldShowError: true, // 🔥 标记需要显示错误弹窗
|
||||
));
|
||||
),
|
||||
);
|
||||
},
|
||||
(success) {
|
||||
_logger.logWithLevel('[暂停任务] 响应成功: $success');
|
||||
_logger.logWithLevel('✅ 暂停任务成功');
|
||||
emit(state.copyWith(
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
operationType: DeviceTaskOperationType.none,
|
||||
));
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_logger.logWithLevel('❌ 暂停任务异常: $e');
|
||||
emit(state.copyWith(
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: ErrorHandler.getErrorMessage(e),
|
||||
operationType: DeviceTaskOperationType.none,
|
||||
shouldShowError: true, // 🔥 标记需要显示错误弹窗
|
||||
));
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,88 +294,111 @@ class DeviceTaskCubit extends Cubit<DeviceTaskState> {
|
||||
return;
|
||||
}
|
||||
|
||||
emit(state.copyWith(
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: true,
|
||||
operationType: DeviceTaskOperationType.recovery,
|
||||
));
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
final userCubit = GetIt.I<AppUserCubit>();
|
||||
final user = userCubit.state.user;
|
||||
final siteCubit = GetIt.I<SiteCubit>();
|
||||
final siteId = siteCubit.state.selectedSite?.id;
|
||||
final selectedSite = siteCubit.state.selectedSite;
|
||||
final siteId = selectedSite?.id;
|
||||
final orgId = selectedSite?.orgId ?? 0;
|
||||
|
||||
if (user == null || siteId == null) {
|
||||
emit(state.copyWith(
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: '参数不完整',
|
||||
operationType: DeviceTaskOperationType.none,
|
||||
));
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final params3 = RecoveryTaskParams(
|
||||
deviceId: deviceId,
|
||||
taskId: taskId,
|
||||
orgId: user.orgId ?? 0,
|
||||
orgId: orgId,
|
||||
siteId: siteId,
|
||||
);
|
||||
_logger.logWithLevel('[恢复任务] 请求: POST /iot/deviceTask/recoveryTask');
|
||||
_logger.logWithLevel('[恢复任务] 参数: deviceId=$deviceId, taskId=$taskId, orgId=${user.orgId ?? 0}, siteId=$siteId');
|
||||
_logger.logWithLevel(
|
||||
'[恢复任务] 参数: deviceId=$deviceId, taskId=$taskId, orgId=$orgId, siteId=$siteId',
|
||||
);
|
||||
final result = await _recoveryTaskUseCase.call(params3);
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
_logger.logWithLevel('[恢复任务] 响应失败: ${failure.message}');
|
||||
_logger.logWithLevel('❌ 恢复任务失败: ${failure.message}');
|
||||
emit(state.copyWith(
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: ErrorHandler.getErrorMessage(failure.message),
|
||||
operationType: DeviceTaskOperationType.none,
|
||||
shouldShowError: true, // 🔥 标记需要显示错误弹窗
|
||||
));
|
||||
),
|
||||
);
|
||||
},
|
||||
(data) {
|
||||
_logger.logWithLevel('[恢复任务] 响应成功: $data');
|
||||
_logger.logWithLevel('✅ 恢复任务成功: $data');
|
||||
emit(state.copyWith(
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
operationType: DeviceTaskOperationType.none,
|
||||
));
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_logger.logWithLevel('❌ 恢复任务异常: $e');
|
||||
emit(state.copyWith(
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: ErrorHandler.getErrorMessage(e),
|
||||
operationType: DeviceTaskOperationType.none,
|
||||
shouldShowError: true, // 🔥 标记需要显示错误弹窗
|
||||
));
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新当前任务ID(当选择新航线时调用)
|
||||
void updateCurrentTaskId(int taskId) {
|
||||
emit(state.copyWith(currentTaskId: taskId));
|
||||
_logger.logWithLevel('🔄 更新当前任务ID: $taskId');
|
||||
// 🔥 只有 activeTasks 非空时才设置 currentTask,避免信息栏一直显示
|
||||
DeviceTaskEntity? taskEntity;
|
||||
if (state.activeTasks.isNotEmpty) {
|
||||
taskEntity = state.activeTasks.firstWhere(
|
||||
(t) => t.id == taskId,
|
||||
orElse: () => DeviceTaskEntity(
|
||||
id: taskId,
|
||||
deviceId: '',
|
||||
taskStatus: 'EXECUTING',
|
||||
taskStatusTranslate: '执行中',
|
||||
),
|
||||
);
|
||||
}
|
||||
emit(state.copyWith(currentTaskId: taskId, currentTask: taskEntity));
|
||||
_logger.logWithLevel(
|
||||
'🔄 更新当前任务ID: $taskId, activeTasks: ${state.activeTasks.length}, currentTask: ${taskEntity?.taskStatus ?? "null"}',
|
||||
);
|
||||
}
|
||||
|
||||
/// 🔥 手动选择任务(用户从弹窗中选择)
|
||||
void selectTask(DeviceTaskEntity task) {
|
||||
_logger.logWithLevel('✅ 用户选择任务ID: ${task.id}, 状态: ${task.taskStatus}');
|
||||
emit(state.copyWith(
|
||||
currentTask: task,
|
||||
currentTaskId: task.id,
|
||||
));
|
||||
emit(state.copyWith(currentTask: task, currentTaskId: task.id));
|
||||
}
|
||||
|
||||
/// 清除当前任务
|
||||
void clearCurrentTask() {
|
||||
emit(state.copyWith(
|
||||
currentTask: null,
|
||||
currentTaskId: null,
|
||||
));
|
||||
emit(state.copyWith(currentTask: null, currentTaskId: null));
|
||||
_logger.logWithLevel('🧹 清除当前任务');
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import 'package:maibu_satabot_v2/features/devices/domain/usecases/update_devicen
|
||||
|
||||
import '../../../../core/consts/tcp_consts.dart';
|
||||
import '../../../../core/logging/i_logger_service.dart';
|
||||
import '../../../../core/logging/log_time.dart';
|
||||
import '../../../../core/network/tcp/tcp_client.dart';
|
||||
import '../../../../core/network/mqtt/domain/repositories/task_message_repository.dart';
|
||||
import '../../data/models/device_add_path_point_model.dart';
|
||||
@@ -268,6 +269,7 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
|
||||
/// 切换设备
|
||||
Future<void> switchDevice(DeviceEntity device) async {
|
||||
debugPrint('${LogTime.now()} 👆 [DevicesCubit] 开始切换设备: ${device.deviceName}');
|
||||
// 保持现有列表,只改 loading
|
||||
emit(state.copyWith(isLoading: true));
|
||||
|
||||
@@ -276,6 +278,7 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
result.fold(
|
||||
// 失败处理
|
||||
(l) {
|
||||
debugPrint('${LogTime.now()} ❌ [DevicesCubit] 切换设备失败: ${device.deviceName}, 原因: ${l.message}');
|
||||
emit(state.copyWith(isLoading: false, errorMessage: l.message));
|
||||
selectDevice(device);
|
||||
},
|
||||
@@ -288,7 +291,12 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
// 重置设备状态 Bloc,清除旧设备图表数据
|
||||
_deviceStatusBloc.add(DeviceStatusReset());
|
||||
|
||||
debugPrint('✅ 新设备切换完成(HTTP-only,无新TCP连接): ${device.deviceName}');
|
||||
// 🔥 机器状态数据源已切换为MQTT:用与TCP相同的方式获取设备号(deviceName),
|
||||
// 订阅 mower/{sn}/property/* 主题(内部自动退订旧设备、同SN去重)
|
||||
debugPrint('${LogTime.now()} 📡 [DevicesCubit] HTTP切换成功,开始MQTT订阅: ${device.deviceName}');
|
||||
_deviceStatusBloc.startMqttRealtimeListening(device.deviceName);
|
||||
|
||||
debugPrint('${LogTime.now()} ✅ 新设备切换完成(HTTP-only,无新TCP连接): ${device.deviceName}');
|
||||
|
||||
emit(state.copyWith(isLoading: false, selectedDevice: device));
|
||||
},
|
||||
@@ -378,7 +386,7 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
emit(state.copyWith(isLoading: false, errorMessage: failure.message));
|
||||
},
|
||||
(records) {
|
||||
// print('🔍 [DevicesCubit] 加载成功,记录数: ${records.length}');
|
||||
print('🔍 [DevicesCubit] selectBySiteId 加载成功,记录数: ${records.length}');
|
||||
// 将 WorkRecordEntity 转换为 Map<String, dynamic> 以兼容现有UI
|
||||
final mappedRecords = records.map((record) {
|
||||
// 🔥 核心修复:根据 jsonData 类型决定存储方式
|
||||
@@ -390,14 +398,27 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
} else if (record.jsonData is WorkRecordJsonData) {
|
||||
jsonDataStr = _workRecordJsonDataToJson(record.jsonData as WorkRecordJsonData);
|
||||
}
|
||||
return <String, dynamic>{
|
||||
final mapped = <String, dynamic>{
|
||||
'id': record.id.toString(),
|
||||
'workName': record.workName,
|
||||
'imgUrl': record.imgUrl ?? '',
|
||||
'jsonData': jsonDataStr,
|
||||
};
|
||||
print(' ├─ [${record.workName}] id=${record.id}, jsonData长度=${jsonDataStr?.length ?? "null"}');
|
||||
// 打印 jsonData 内部的 path/outer 信息
|
||||
if (jsonDataStr != null && jsonDataStr.isNotEmpty) {
|
||||
try {
|
||||
final decoded = jsonDecode(jsonDataStr);
|
||||
if (decoded is Map) {
|
||||
print(' │ path类型=${decoded['path']?.runtimeType}, outer类型=${decoded['outer']?.runtimeType}, planModel=${decoded['planModel']}');
|
||||
if (decoded['path'] is List) print(' │ path长度=${(decoded['path'] as List).length}');
|
||||
if (decoded['outer'] is List) print(' │ outer长度=${(decoded['outer'] as List).length}');
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
return mapped;
|
||||
}).toList();
|
||||
// print('🔍 [DevicesCubit] 转换后的数据: $mappedRecords');
|
||||
print('🔍 [DevicesCubit] 转换完成,emit workRecords 数量: ${mappedRecords.length}');
|
||||
emit(state.copyWith(isLoading: false, workRecords: mappedRecords));
|
||||
},
|
||||
);
|
||||
@@ -439,15 +460,36 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
/// 加载指定作业名的路径数据
|
||||
Future<void> loadSelectedPath(String workName) async {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
debugPrint('📡 [loadSelectedPath] 开始查询, workName=$workName');
|
||||
try {
|
||||
final result = await _selectWorkRecordUseCase.call(workName);
|
||||
result.fold(
|
||||
(failure) => emit(
|
||||
(failure) {
|
||||
debugPrint('❌ [loadSelectedPath] 接口失败: ${failure.message}');
|
||||
emit(
|
||||
state.copyWith(isLoading: false, errorMessage: failure.message),
|
||||
),
|
||||
(data) => emit(state.copyWith(isLoading: false, pathData: data)),
|
||||
);
|
||||
},
|
||||
(data) {
|
||||
debugPrint('✅ [loadSelectedPath] 接口成功, 记录数: ${data.length}');
|
||||
if (data.isNotEmpty) {
|
||||
final first = data.first;
|
||||
debugPrint(' ├─ 第一条记录 keys: ${first.keys.toList()}');
|
||||
debugPrint(' ├─ path 类型: ${first['path']?.runtimeType}, 长度: ${(first['path'] as List?)?.length ?? "null"}');
|
||||
debugPrint(' ├─ outer 类型: ${first['outer']?.runtimeType}, 长度: ${(first['outer'] as List?)?.length ?? "null"}');
|
||||
debugPrint(' └─ planModel: ${first['planModel']}');
|
||||
if (first['path'] is List && (first['path'] as List).isNotEmpty) {
|
||||
debugPrint(' └─ path[0]: ${(first['path'] as List).first}');
|
||||
}
|
||||
if (first['outer'] is List && (first['outer'] as List).isNotEmpty) {
|
||||
debugPrint(' └─ outer[0]: ${(first['outer'] as List).first}');
|
||||
}
|
||||
}
|
||||
emit(state.copyWith(isLoading: false, pathData: data));
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ [loadSelectedPath] 异常: $e');
|
||||
emit(state.copyWith(isLoading: false, errorMessage: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
/// ESRI World Imagery 底图地址(国内网络可能不可达)
|
||||
const String kEsriWorldImageryUrl =
|
||||
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}';
|
||||
|
||||
/// 网络超时时长(瓦片加载)
|
||||
const Duration _kTileTimeout = Duration(seconds: 8);
|
||||
|
||||
/// ─────────────────────────────────────────────────────────
|
||||
/// 容错网络瓦片图
|
||||
/// ─────────────────────────────────────────────────────────
|
||||
///
|
||||
/// 加载失败(超时 / 断网 / 404 等)时返回 1×1 透明 PNG,
|
||||
/// 避免 SocketException 刷屏并保证地图其余部分正常渲染。
|
||||
class ResilientNetworkImage extends ImageProvider<ResilientNetworkImage> {
|
||||
const ResilientNetworkImage(this.url);
|
||||
|
||||
final String url;
|
||||
|
||||
@override
|
||||
Future<ResilientNetworkImage> obtainKey(ImageConfiguration configuration) =>
|
||||
SynchronousFuture<ResilientNetworkImage>(this);
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(
|
||||
ResilientNetworkImage key,
|
||||
ImageDecoderCallback decode,
|
||||
) {
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _loadAsync(decode),
|
||||
scale: 1.0,
|
||||
debugLabel: 'ResilientNetworkImage(${url.split('/').last})',
|
||||
);
|
||||
}
|
||||
|
||||
Future<ui.Codec> _loadAsync(ImageDecoderCallback decode) async {
|
||||
try {
|
||||
final client = http.Client();
|
||||
final response = await client
|
||||
.get(Uri.parse(url))
|
||||
.timeout(_kTileTimeout);
|
||||
|
||||
if (response.statusCode == 200 && response.bodyBytes.isNotEmpty) {
|
||||
final buffer = await ui.ImmutableBuffer.fromUint8List(
|
||||
response.bodyBytes,
|
||||
);
|
||||
return decode(buffer);
|
||||
}
|
||||
} catch (_) {
|
||||
// 超时 / 网络异常 / 解码失败 → 走下面的透明兜底
|
||||
}
|
||||
// 1×1 透明 PNG,避免 Flutter 框架抛出 MissingImageException
|
||||
return decode(
|
||||
await ui.ImmutableBuffer.fromUint8List(_transparentPng),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is ResilientNetworkImage && other.url == url;
|
||||
|
||||
@override
|
||||
int get hashCode => url.hashCode;
|
||||
}
|
||||
|
||||
/// ─────────────────────────────────────────────────────────
|
||||
/// 容错 TileProvider —— 直接在 TileLayer.tileProvider 中使用
|
||||
/// ─────────────────────────────────────────────────────────
|
||||
///
|
||||
/// ```dart
|
||||
/// TileLayer(
|
||||
/// urlTemplate: kEsriWorldImageryUrl,
|
||||
/// tileProvider: ResilientTileProvider(),
|
||||
/// minZoom: 0,
|
||||
/// maxZoom: 19,
|
||||
/// )
|
||||
/// ```
|
||||
class ResilientTileProvider extends TileProvider {
|
||||
@override
|
||||
ImageProvider<Object> getImage(TileCoordinates coordinates, TileLayer options) {
|
||||
final url = getTileUrl(coordinates, options);
|
||||
return ResilientNetworkImage(url);
|
||||
}
|
||||
}
|
||||
|
||||
/// 1×1 透明 PNG 字节(兜底用)
|
||||
final Uint8List _transparentPng = Uint8List.fromList(<int>[
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
|
||||
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, // IHDR chunk
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, // 1×1
|
||||
0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, // RGBA 8bit
|
||||
0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, // IDAT chunk
|
||||
0x54, 0x78, 0x9C, 0x62, 0x00, 0x00, 0x00, 0x02,
|
||||
0x00, 0x01, 0xE5, 0x27, 0xDE, 0xFC, 0x00, 0x00, // compressed data
|
||||
0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, // IEND chunk
|
||||
0x60, 0x82,
|
||||
]);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -187,8 +187,9 @@ class _VerticalFloatMenuState extends State<VerticalFloatMenu> {
|
||||
Text(loc.translate('route_planning.select_work_mode'), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 20),
|
||||
_modeItem(
|
||||
title: loc.translate('route_planning.bow_mode'),
|
||||
subtitle: loc.translate('route_planning.bow_mode_subtitle'),
|
||||
// 🔥 硬编码:差包更新不更新 assets,必须写死文案
|
||||
title: '覆盖模式',
|
||||
subtitle: '标准全覆盖路径规划',
|
||||
isSelected: selectedMode == WorkMode.bow,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
@@ -198,8 +199,8 @@ class _VerticalFloatMenuState extends State<VerticalFloatMenu> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_modeItem(
|
||||
title: loc.translate('route_planning.custom_mode'),
|
||||
subtitle: loc.translate('route_planning.custom_mode_subtitle'),
|
||||
title: '自定义模式',
|
||||
subtitle: '手动设定作业区域',
|
||||
isSelected: selectedMode == WorkMode.custom,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:maibu_satabot_v2/features/my/repository/my_repository.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../core/consts/http_api_consts.dart';
|
||||
import '../../../../core/storage/user_storage.dart';
|
||||
// 必须导入 DeviceFailure 类
|
||||
import '../../../../features/devices/domain/errors/device_failure.dart';
|
||||
@@ -26,7 +27,7 @@ class MyRepositoryImpl implements MyRepository {
|
||||
// 核心修复:严格匹配抽象类的方法签名
|
||||
@override
|
||||
Future<Either<DeviceFailure, int>> updateName(String nickName) async {
|
||||
final url = Uri.parse('http://1.95.137.212:59003/system/user/profile');
|
||||
final url = Uri.parse('${HttpApiConsts.baseUrl}/system/user/profile');
|
||||
|
||||
// 补充:从 UserStorage 获取 token(接口通常需要认证)
|
||||
final token = await _getToken();
|
||||
|
||||
@@ -1029,6 +1029,10 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
emit(state.copyWith(targetDevice: device));
|
||||
debugPrint('✅ [RemoteControl] targetDevice状态已更新');
|
||||
|
||||
// 🔥 MQTT机器状态订阅:切换设备时订阅新设备的topic(与DevicesCubit.switchDevice相同逻辑)
|
||||
debugPrint('📡 [RemoteControl] 开始MQTT机器状态订阅: ${device.deviceName}');
|
||||
deviceStatusBloc.startMqttRealtimeListening(device.deviceName);
|
||||
|
||||
// 🔥 关键修复:TCP 已在登录时建立,设备切换只用 HTTP
|
||||
// 不再创建新 TCP 连接,避免重复发送 0x03 触发服务端推送 have_logged_in
|
||||
if (tcpClient.isConnected) {
|
||||
|
||||
@@ -12,7 +12,6 @@ import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
|
||||
import '../../../../core/app/app_user_cubit.dart';
|
||||
import '../../../../core/router/route_paths.dart';
|
||||
import '../../../auth/presentation/bloc/auth_cubit.dart'; // 🔥 导入 AuthCubit
|
||||
import '../../../devices/domain/entities/device_entity.dart';
|
||||
import '../../../devices/presentation/bloc/devices_cubit.dart';
|
||||
import '../bloc/remote_control_cubit.dart';
|
||||
import '../bloc/remote_control_state.dart';
|
||||
@@ -116,9 +115,6 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔥 弹窗显示当前控制的设备信息
|
||||
_showTargetDeviceDialog(context, remoteCubit.state.targetDevice!);
|
||||
|
||||
// 🔥 只在控制循环未启动时才启动
|
||||
remoteCubit.startControlLoop();
|
||||
// debugPrint('✅ [RemoteControl] 控制循环已启动');
|
||||
@@ -539,36 +535,6 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 🔥 显示当前控制设备的弹窗
|
||||
void _showTargetDeviceDialog(BuildContext context, DeviceEntity device) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('🎮 远程控制'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'设备名称: ${device.deviceName}',
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'设备ID: ${device.deviceName}',
|
||||
style: const TextStyle(fontSize: 14, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
/// 🔥 显示当前控制设备的弹窗 - 已移除:进入远程遥控页不再弹出设备确认弹窗
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../../../../core/consts/http_api_consts.dart';
|
||||
import '../../domain/entities/device_entity.dart';
|
||||
import '../../domain/entities/device_status_entity.dart';
|
||||
@@ -12,6 +13,7 @@ abstract class DeviceRemoteDataSource {
|
||||
int? siteId,
|
||||
String? typeFilter,
|
||||
});
|
||||
Future<List<DeviceDataModel>> getAllDevices();
|
||||
}
|
||||
|
||||
/// 设备远程数据源实现类 - 从真实 API 获取数据
|
||||
@@ -64,6 +66,37 @@ class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<DeviceDataModel>> getAllDevices() async {
|
||||
try {
|
||||
final response = await dio.get(
|
||||
HttpApiConsts.getDeviceList,
|
||||
queryParameters: {'pageNum': 1, 'pageSize': 9999},
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('网络请求失败: ${response.statusCode}');
|
||||
}
|
||||
|
||||
final responseData = response.data;
|
||||
if (responseData['code'] != 200) {
|
||||
throw Exception(responseData['msg'] ?? '业务异常');
|
||||
}
|
||||
|
||||
final List<dynamic> rows = responseData['rows'] ?? [];
|
||||
debugPrint(
|
||||
'>>> [DeviceRemoteDataSource] getAllDevices 返回 ${rows.length} 条设备',
|
||||
);
|
||||
for (int i = 0; i < rows.length && i < 10; i++) {
|
||||
debugPrint('>>> [DeviceRemoteDataSource] rows[$i]: ${rows[i]}');
|
||||
}
|
||||
return rows.map((item) => _parseDeviceFromJson(item)).toList();
|
||||
} catch (e) {
|
||||
debugPrint('>>> [DeviceRemoteDataSource] getAllDevices error: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<DeviceDataModel>> _fetchDevicesFromAPI(int siteId) async {
|
||||
final response = await dio.get(
|
||||
HttpApiConsts.getSiteDeviceList,
|
||||
@@ -107,10 +140,19 @@ class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource {
|
||||
DeviceDataModel _parseDeviceFromJson(Map<String, dynamic> json) {
|
||||
// 在线状态:使用 onlineStatus 字段(1=在线, 0=离线)
|
||||
final onlineStatus = json['onlineStatus'] ?? 0;
|
||||
final deviceId = json['deviceId']?.toString() ?? '';
|
||||
final name = json['deviceName'] ?? json['name'] ?? '';
|
||||
final type = json['deviceTypeName'] ?? json['type'] ?? '未知设备';
|
||||
final isRobot = _isRobotDevice(type, name, deviceId);
|
||||
|
||||
debugPrint(
|
||||
'🔍 [DeviceListDataSource] deviceId=$deviceId name=$name type=$type isRobot=$isRobot',
|
||||
);
|
||||
|
||||
return DeviceDataModel(
|
||||
deviceId: json['deviceId']?.toString() ?? '',
|
||||
name: json['deviceName'] ?? json['name'] ?? '',
|
||||
type: json['deviceTypeName'] ?? json['type'] ?? '未知设备',
|
||||
deviceId: deviceId,
|
||||
name: name,
|
||||
type: type,
|
||||
status: onlineStatus == 1 ? '在线' : '离线',
|
||||
power: (json['power'] as num?)?.toDouble() ?? 0,
|
||||
todayEnergy: (json['todayEnergy'] as num?)?.toDouble() ?? 0,
|
||||
@@ -118,6 +160,7 @@ class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource {
|
||||
current: (json['current'] as num?)?.toDouble(),
|
||||
voltage: (json['voltage'] as num?)?.toDouble(),
|
||||
radiation: (json['radiation'] as num?)?.toDouble(),
|
||||
isRobot: isRobot,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -131,9 +174,32 @@ class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource {
|
||||
power: (json['capacity_percent'] as num?)?.toDouble() ?? 0,
|
||||
todayEnergy: 0,
|
||||
temperature: (json['environment_temperature'] as num?)?.toDouble(),
|
||||
isRobot: false,
|
||||
);
|
||||
}
|
||||
|
||||
/// 🔥 通用判断:是否机器人(型号前缀 + 中文关键字双重匹配,新型号也能自动识别)
|
||||
bool _isRobotDevice(String type, String name, String deviceId) {
|
||||
final haystacks = [
|
||||
type.toLowerCase(),
|
||||
name.toLowerCase(),
|
||||
deviceId.toLowerCase(),
|
||||
];
|
||||
// 型号家族前缀(MC700/MC700STD/MC700PLUS/MC700AIR/MC700PRO 等;RCHWD/RCHETD 等)
|
||||
const prefixes = ['mc', 'rch', 'rcetd', 'rchetd'];
|
||||
// 中文类型关键字
|
||||
const keywords = ['除草', '巡检', '清洗', '割草', '机器人'];
|
||||
for (final s in haystacks) {
|
||||
for (final p in prefixes) {
|
||||
if (s.startsWith(p)) return true;
|
||||
}
|
||||
}
|
||||
for (final k in keywords) {
|
||||
if (type.contains(k) || name.contains(k)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
String _getStatusText(int status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
|
||||
@@ -12,6 +12,7 @@ class DeviceDataModel {
|
||||
final double? current;
|
||||
final double? voltage;
|
||||
final double? radiation;
|
||||
final bool isRobot;
|
||||
|
||||
const DeviceDataModel({
|
||||
required this.deviceId,
|
||||
@@ -24,6 +25,7 @@ class DeviceDataModel {
|
||||
this.current,
|
||||
this.voltage,
|
||||
this.radiation,
|
||||
this.isRobot = false,
|
||||
});
|
||||
|
||||
/// 从 JSON 创建数据模型
|
||||
@@ -71,6 +73,7 @@ class DeviceDataModel {
|
||||
current: current,
|
||||
voltage: voltage,
|
||||
radiation: radiation,
|
||||
isRobot: isRobot,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,4 +20,10 @@ class DeviceRepositoryImpl implements DeviceRepository {
|
||||
final dataModels = await remoteDataSource.getDeviceList(siteId: siteId, typeFilter: typeFilter);
|
||||
return dataModels.map((model) => model.toEntity()).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<DeviceEntity>> getAllDevices() async {
|
||||
final dataModels = await remoteDataSource.getAllDevices();
|
||||
return dataModels.map((model) => model.toEntity()).toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ class DeviceEntity {
|
||||
final double? current;
|
||||
final double? voltage;
|
||||
final double? radiation;
|
||||
final bool isRobot;
|
||||
|
||||
const DeviceEntity({
|
||||
required this.deviceId,
|
||||
@@ -22,5 +23,6 @@ class DeviceEntity {
|
||||
this.current,
|
||||
this.voltage,
|
||||
this.radiation,
|
||||
this.isRobot = false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,4 +5,5 @@ import '../entities/device_status_entity.dart';
|
||||
abstract class DeviceRepository {
|
||||
Future<DeviceStatusEntity> getDeviceStatus();
|
||||
Future<List<DeviceEntity>> getDeviceList({int? siteId, String? typeFilter});
|
||||
Future<List<DeviceEntity>> getAllDevices();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import '../entities/device_entity.dart';
|
||||
import '../repositories/device_repository.dart';
|
||||
|
||||
/// 获取全部设备列表用例(用于蓝牙绑定匹配)
|
||||
class GetAllDevicesUseCase {
|
||||
final DeviceRepository repository;
|
||||
|
||||
const GetAllDevicesUseCase({required this.repository});
|
||||
|
||||
Future<List<DeviceEntity>> execute() async {
|
||||
return await repository.getAllDevices();
|
||||
}
|
||||
}
|
||||
@@ -10,15 +10,15 @@ class FloatBarSettingService {
|
||||
/// 🔥 静态实例引用
|
||||
static FloatBarSettingService? _instance;
|
||||
|
||||
/// 🔥 静态状态变量 - 所有组件共享
|
||||
static bool _isEnabled = true;
|
||||
/// 🔥 静态状态变量 - 所有组件共享(默认关闭)
|
||||
static bool _isEnabled = false;
|
||||
|
||||
/// 🔥 静态 ValueNotifier - 用于通知UI变化
|
||||
static final ValueNotifier<bool> _settingNotifier = ValueNotifier<bool>(true);
|
||||
/// 🔥 静态 ValueNotifier - 用于通知UI变化(默认关闭)
|
||||
static final ValueNotifier<bool> _settingNotifier = ValueNotifier<bool>(false);
|
||||
|
||||
FloatBarSettingService(this._prefs) {
|
||||
_instance = this;
|
||||
_isEnabled = _prefs.getBool(_key) ?? true;
|
||||
_isEnabled = _prefs.getBool(_key) ?? false;
|
||||
_settingNotifier.value = _isEnabled;
|
||||
debugPrint('✅ [FloatBarSettingService] 初始化完成,初始状态: $_isEnabled');
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import '../../../../../../core/network/mqtt/data/datasources/drone_osd_datasourc
|
||||
enum SimulationMode {
|
||||
circle, // 圆形路径
|
||||
polygon, // 多边形路径(六边形)
|
||||
bow, // 弓字形路径(Zigzag)
|
||||
bow, // 覆盖模式路径(Coverage)
|
||||
rectangle, // 矩形路径
|
||||
}
|
||||
|
||||
@@ -524,7 +524,7 @@ class FloatBarWidgetState extends State<FloatBarWidget> {
|
||||
return path;
|
||||
}
|
||||
|
||||
/// 生成弓字形路径(Zigzag)
|
||||
/// 生成覆盖模式路径(Coverage)
|
||||
List<LatLng> _generateBowPath(double centerLat, double centerLng, int points) {
|
||||
final List<LatLng> path = [];
|
||||
const double width = 0.004; // 宽度约400米
|
||||
@@ -544,7 +544,7 @@ class FloatBarWidgetState extends State<FloatBarWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('〰️ 生成弓字形路径: ${path.length}个点, $rows行');
|
||||
debugPrint('〰️ 生成覆盖模式路径: ${path.length}个点, $rows行');
|
||||
return path;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:maibu_satabot_v2/core/theme/AppTheme.dart';
|
||||
import 'dart:developer' as developer;
|
||||
import 'dart:typed_data';
|
||||
@@ -11,6 +12,9 @@ import '../../../../../core/bluetooth/protocol_parser.dart';
|
||||
import '../../../../../core/protocol/machine_protocol_constants.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import '../../../../../core/services/device_permission_service.dart';
|
||||
import '../../domain/entities/device_entity.dart';
|
||||
import '../../domain/usecases/get_all_devices_usecase.dart';
|
||||
import 'bind_device_page.dart';
|
||||
|
||||
class BleDeviceDetailPage extends StatefulWidget {
|
||||
final BluetoothDevice device;
|
||||
@@ -34,6 +38,7 @@ class _BleDeviceDetailPageState extends State<BleDeviceDetailPage> {
|
||||
StreamSubscription<BluetoothDevice?>? _connectingSubscription;
|
||||
StreamSubscription<BlePacket>? _packetSubscription;
|
||||
final List<_ReceivedPacket> _receivedPackets = [];
|
||||
|
||||
/// 按命令类型分组的最新推送:command → 最新数据
|
||||
final Map<int, _ReceivedPacket> _latestPushByCommand = {};
|
||||
bool _pushExpanded = false; // 数据推送默认折叠
|
||||
@@ -44,6 +49,7 @@ class _BleDeviceDetailPageState extends State<BleDeviceDetailPage> {
|
||||
|
||||
/// 等待设备返回 0x06 写配置应答
|
||||
Completer<bool>? _writeRespCompleter;
|
||||
|
||||
/// 等待回读 0x05 应答,携带期望字段值用于对比
|
||||
Completer<Uint8List?>? _readbackCompleter;
|
||||
Map<String, String>? _expectedFieldValues;
|
||||
@@ -247,6 +253,107 @@ class _BleDeviceDetailPageState extends State<BleDeviceDetailPage> {
|
||||
await BleManager.instance.disconnect();
|
||||
}
|
||||
|
||||
/// 获取当前蓝牙设备的显示名称
|
||||
String get _bleDeviceName {
|
||||
final advData = widget.scanResult?.advertisementData;
|
||||
return _resolvedName ??
|
||||
(advData?.advName.isNotEmpty == true
|
||||
? advData!.advName
|
||||
: (widget.device.platformName.isNotEmpty
|
||||
? widget.device.platformName
|
||||
: (advData?.localName?.isNotEmpty == true
|
||||
? advData!.localName!
|
||||
: (widget.device.advName.isNotEmpty
|
||||
? widget.device.advName
|
||||
: ''))));
|
||||
}
|
||||
|
||||
/// 绑定设备:调接口获取设备列表,用蓝牙名(时间戳)匹配后跳转绑定页
|
||||
Future<void> _bindDevice() async {
|
||||
final bleName = _bleDeviceName;
|
||||
if (bleName.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('无法获取蓝牙设备名称')));
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示 loading
|
||||
BuildContext? loadingCtx;
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (ctx) {
|
||||
loadingCtx = ctx;
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
final getAllDevicesUseCase = GetIt.instance<GetAllDevicesUseCase>();
|
||||
final devices = await getAllDevicesUseCase.execute();
|
||||
|
||||
if (loadingCtx != null && Navigator.canPop(loadingCtx!)) {
|
||||
Navigator.pop(loadingCtx!);
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'>>> [BindDevice] bleName=$bleName, devices=${devices.length}',
|
||||
);
|
||||
for (int i = 0; i < devices.length && i < 10; i++) {
|
||||
debugPrint(
|
||||
'>>> [BindDevice] device[$i]: id=${devices[i].deviceId}, name=${devices[i].name}',
|
||||
);
|
||||
}
|
||||
|
||||
// 用蓝牙名(时间戳)匹配设备名中包含该时间戳的设备
|
||||
DeviceEntity? matchedDevice;
|
||||
for (final device in devices) {
|
||||
if (device.name.contains(bleName)) {
|
||||
matchedDevice = device;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedDevice == null) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('未找到名称包含「$bleName」的设备')));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (matchedDevice.deviceId.isEmpty) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('匹配到的设备缺少设备ID')));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
// 复用扫一扫的绑定页面流程
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => BindDevicePage(scanResult: matchedDevice!.deviceId),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
if (loadingCtx != null && Navigator.canPop(loadingCtx!)) {
|
||||
Navigator.pop(loadingCtx!);
|
||||
}
|
||||
debugPrint('>>> [BindDevice] error: $e');
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('绑定设备失败: $e')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_connSubscription?.cancel();
|
||||
@@ -372,7 +479,6 @@ class _BleDeviceDetailPageState extends State<BleDeviceDetailPage> {
|
||||
await _sendAndLog(MachineProtocolConstants.cmdReadConfig, []);
|
||||
}
|
||||
|
||||
|
||||
String _bytesToHex(List<int> bytes) {
|
||||
if (bytes.isEmpty) return '';
|
||||
return bytes
|
||||
@@ -382,16 +488,26 @@ class _BleDeviceDetailPageState extends State<BleDeviceDetailPage> {
|
||||
|
||||
String _commandName(int command) {
|
||||
switch (command) {
|
||||
case MachineProtocolConstants.cmdRemoteControl: return '远程遥控';
|
||||
case MachineProtocolConstants.cmdPathPlanning: return '路径规划';
|
||||
case MachineProtocolConstants.cmdStatusInfo: return '状态信息';
|
||||
case MachineProtocolConstants.cmdGetId: return '查询ID';
|
||||
case MachineProtocolConstants.cmdGetAuth: return '获取授权';
|
||||
case MachineProtocolConstants.cmdReadConfig: return '读配置';
|
||||
case MachineProtocolConstants.cmdWriteConfig: return '写配置';
|
||||
case MachineProtocolConstants.cmdObstacleAvoid: return '避障';
|
||||
case MachineProtocolConstants.cmdHeartbeat: return '心跳包';
|
||||
default: return '未知(0x${command.toRadixString(16).toUpperCase().padLeft(2, '0')})';
|
||||
case MachineProtocolConstants.cmdRemoteControl:
|
||||
return '远程遥控';
|
||||
case MachineProtocolConstants.cmdPathPlanning:
|
||||
return '路径规划';
|
||||
case MachineProtocolConstants.cmdStatusInfo:
|
||||
return '状态信息';
|
||||
case MachineProtocolConstants.cmdGetId:
|
||||
return '查询ID';
|
||||
case MachineProtocolConstants.cmdGetAuth:
|
||||
return '获取授权';
|
||||
case MachineProtocolConstants.cmdReadConfig:
|
||||
return '读配置';
|
||||
case MachineProtocolConstants.cmdWriteConfig:
|
||||
return '写配置';
|
||||
case MachineProtocolConstants.cmdObstacleAvoid:
|
||||
return '避障';
|
||||
case MachineProtocolConstants.cmdHeartbeat:
|
||||
return '心跳包';
|
||||
default:
|
||||
return '未知(0x${command.toRadixString(16).toUpperCase().padLeft(2, '0')})';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,13 +702,19 @@ class _BleDeviceDetailPageState extends State<BleDeviceDetailPage> {
|
||||
width: 80,
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(fontSize: 13, color: context.appColors.textTertiary),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: context.appColors.textTertiary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(fontSize: 13, color: context.appColors.textPrimary),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: context.appColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -626,8 +748,10 @@ class _BleDeviceDetailPageState extends State<BleDeviceDetailPage> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
Row(
|
||||
children: [
|
||||
// 连接/断开 按钮
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: _isConnecting
|
||||
? null
|
||||
@@ -654,6 +778,37 @@ class _BleDeviceDetailPageState extends State<BleDeviceDetailPage> {
|
||||
: Text(_isConnected ? '断开连接' : '连接设备'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// 绑定设备 按钮(未连接时灰色禁用)
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: _isConnected ? _bindDevice : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
backgroundColor: _isConnected
|
||||
? context.appColors.primary
|
||||
: context.appColors.fillBackground,
|
||||
foregroundColor: _isConnected
|
||||
? Colors.white
|
||||
: context.appColors.textTertiary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'绑定设备',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: _isConnected
|
||||
? Colors.white
|
||||
: context.appColors.textTertiary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -785,7 +940,10 @@ class _BleDeviceDetailPageState extends State<BleDeviceDetailPage> {
|
||||
),
|
||||
child: Text(
|
||||
p,
|
||||
style: TextStyle(fontSize: 10, color: context.appColors.primary),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: context.appColors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -871,14 +1029,21 @@ class _BleDeviceDetailPageState extends State<BleDeviceDetailPage> {
|
||||
if (pushes.isNotEmpty) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: context.appColors.primary,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
'${pushes.length}',
|
||||
style: const TextStyle(fontSize: 11, color: Colors.white, fontWeight: FontWeight.w600),
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -894,7 +1059,10 @@ class _BleDeviceDetailPageState extends State<BleDeviceDetailPage> {
|
||||
},
|
||||
child: Text(
|
||||
'清空',
|
||||
style: TextStyle(fontSize: 13, color: context.appColors.primary),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: context.appColors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -906,16 +1074,23 @@ class _BleDeviceDetailPageState extends State<BleDeviceDetailPage> {
|
||||
child: Center(
|
||||
child: Text(
|
||||
'等待接收数据...',
|
||||
style: TextStyle(fontSize: 13, color: context.appColors.textTertiary),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: context.appColors.textTertiary,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Column(
|
||||
children: pushes.map((p) => Padding(
|
||||
children: pushes
|
||||
.map(
|
||||
(p) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: _buildPushCard(p),
|
||||
)).toList(),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -970,13 +1145,17 @@ class _BleDeviceDetailPageState extends State<BleDeviceDetailPage> {
|
||||
'${p.time.hour.toString().padLeft(2, '0')}:'
|
||||
'${p.time.minute.toString().padLeft(2, '0')}:'
|
||||
'${p.time.second.toString().padLeft(2, '0')}',
|
||||
style: TextStyle(fontSize: 11, color: context.appColors.textTertiary),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: context.appColors.textTertiary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (hasParsed) ...[
|
||||
const SizedBox(height: 8),
|
||||
...p.parsedFields!.map((f) => Padding(
|
||||
...p.parsedFields!.map(
|
||||
(f) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -985,7 +1164,10 @@ class _BleDeviceDetailPageState extends State<BleDeviceDetailPage> {
|
||||
width: 68,
|
||||
child: Text(
|
||||
f.label,
|
||||
style: TextStyle(fontSize: 11, color: context.appColors.textTertiary),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: context.appColors.textTertiary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
@@ -993,12 +1175,16 @@ class _BleDeviceDetailPageState extends State<BleDeviceDetailPage> {
|
||||
f.value,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: 11, color: context.appColors.textPrimary),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: context.appColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
] else if (p.hex.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
@@ -1019,12 +1205,7 @@ class _BleDeviceDetailPageState extends State<BleDeviceDetailPage> {
|
||||
}
|
||||
|
||||
/// 可编辑的字段白名单:仅这四个增益属性可修改
|
||||
static const _editableFields = {
|
||||
'左轮前进增益',
|
||||
'左轮后退增益',
|
||||
'右轮前进增益',
|
||||
'右轮后退增益',
|
||||
};
|
||||
static const _editableFields = {'左轮前进增益', '左轮后退增益', '右轮前进增益', '右轮后退增益'};
|
||||
|
||||
bool _isEditableField(String label) => _editableFields.contains(label);
|
||||
|
||||
@@ -1156,7 +1337,9 @@ class _BleDeviceDetailPageState extends State<BleDeviceDetailPage> {
|
||||
),
|
||||
TextSpan(
|
||||
text: ' | ',
|
||||
style: TextStyle(color: context.appColors.divider),
|
||||
style: TextStyle(
|
||||
color: context.appColors.divider,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: mod.newValue,
|
||||
@@ -1219,7 +1402,10 @@ class _BleDeviceDetailPageState extends State<BleDeviceDetailPage> {
|
||||
hintText: '输入新值',
|
||||
border: const OutlineInputBorder(),
|
||||
suffixText: '当前: $currentValue',
|
||||
suffixStyle: TextStyle(fontSize: 11, color: context.appColors.textTertiary),
|
||||
suffixStyle: TextStyle(
|
||||
fontSize: 11,
|
||||
color: context.appColors.textTertiary,
|
||||
),
|
||||
errorText: errorText,
|
||||
),
|
||||
),
|
||||
@@ -1236,7 +1422,9 @@ class _BleDeviceDetailPageState extends State<BleDeviceDetailPage> {
|
||||
Navigator.pop(ctx);
|
||||
return;
|
||||
}
|
||||
final pkt = _latestPushByCommand[MachineProtocolConstants.cmdReadConfig];
|
||||
final pkt =
|
||||
_latestPushByCommand[MachineProtocolConstants
|
||||
.cmdReadConfig];
|
||||
final entity = pkt?.configEntity;
|
||||
if (entity == null) {
|
||||
Navigator.pop(ctx);
|
||||
@@ -1269,7 +1457,8 @@ class _BleDeviceDetailPageState extends State<BleDeviceDetailPage> {
|
||||
|
||||
try {
|
||||
// 🔐 前置权限校验:使用读配置(0x05)中的芯片序列号(chipUid)作为设备ID
|
||||
final readConfigPacket = _latestPushByCommand[MachineProtocolConstants.cmdReadConfig];
|
||||
final readConfigPacket =
|
||||
_latestPushByCommand[MachineProtocolConstants.cmdReadConfig];
|
||||
final chipUid = readConfigPacket?.configEntity?.chipUid ?? '';
|
||||
if (chipUid.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -1305,7 +1494,11 @@ class _BleDeviceDetailPageState extends State<BleDeviceDetailPage> {
|
||||
builder: (dialogCtx) => AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.check_circle, color: context.appColors.success, size: 28),
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
color: context.appColors.success,
|
||||
size: 28,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text('权限通过'),
|
||||
],
|
||||
@@ -1343,7 +1536,10 @@ class _BleDeviceDetailPageState extends State<BleDeviceDetailPage> {
|
||||
|
||||
// 2. 发送 0x06 并等待设备应答
|
||||
_writeRespCompleter = Completer<bool>();
|
||||
developer.log('[WriteConfig] ⏳ 发送 0x06, 等待应答...', name: 'BleDeviceDetail');
|
||||
developer.log(
|
||||
'[WriteConfig] ⏳ 发送 0x06, 等待应答...',
|
||||
name: 'BleDeviceDetail',
|
||||
);
|
||||
await BleManager.instance.sendCommand(
|
||||
MachineProtocolConstants.cmdWriteConfig,
|
||||
configBytes.toList(),
|
||||
@@ -1363,7 +1559,9 @@ class _BleDeviceDetailPageState extends State<BleDeviceDetailPage> {
|
||||
if (!mounted) return;
|
||||
|
||||
// 3. 记录期望值,自动回读对比(无论是否收到显式确认都回读)
|
||||
_expectedFieldValues = Map<String, String>.from(p.configEntity!.toFieldMap());
|
||||
_expectedFieldValues = Map<String, String>.from(
|
||||
p.configEntity!.toFieldMap(),
|
||||
);
|
||||
|
||||
// 4. 自动回读配置
|
||||
_readbackCompleter = Completer<Uint8List?>();
|
||||
@@ -1430,14 +1628,13 @@ class _BleDeviceDetailPageState extends State<BleDeviceDetailPage> {
|
||||
rawPayload: readbackPayload,
|
||||
configEntity: newEntity,
|
||||
);
|
||||
_latestPushByCommand[MachineProtocolConstants.cmdReadConfig] = updatedEntry;
|
||||
_latestPushByCommand[MachineProtocolConstants.cmdReadConfig] =
|
||||
updatedEntry;
|
||||
});
|
||||
}
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'校验不一致: ${mismatched.join('、')},已恢复为设备实际值',
|
||||
),
|
||||
content: Text('校验不一致: ${mismatched.join('、')},已恢复为设备实际值'),
|
||||
backgroundColor: context.appColors.warning,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
@@ -1462,7 +1659,8 @@ class _BleDeviceDetailPageState extends State<BleDeviceDetailPage> {
|
||||
rawPayload: readbackPayload,
|
||||
configEntity: newEntity,
|
||||
);
|
||||
_latestPushByCommand[MachineProtocolConstants.cmdReadConfig] = updatedEntry;
|
||||
_latestPushByCommand[MachineProtocolConstants.cmdReadConfig] =
|
||||
updatedEntry;
|
||||
_configModifications = {};
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
|
||||
@@ -119,15 +119,29 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔥 机器人设备名称前缀,用于从通用设备列表中过滤掉机器人
|
||||
static const _robotPrefixes = [
|
||||
'RCHETD-CN',
|
||||
'MC700PLUS-CN',
|
||||
'MC700-CN',
|
||||
'MC700PRO-CN',
|
||||
'RCETD-CN',
|
||||
'MC700AIR-CN',
|
||||
/// 🔥 机器人型号家族前缀(小写,startsWith 匹配,覆盖 MC700/MC700STD/MC700PLUS/MCAIR/RCHWD/RCHETD 等全系列)
|
||||
static const _robotPrefixes = ['mc', 'rch', 'rcetd', 'rchetd'];
|
||||
|
||||
/// 🔥 机器人类型中文名关键字(接口 deviceTypeName 可能是纯中文)
|
||||
static const _robotTypeKeywords = ['除草', '巡检', '清洗', '割草', '机器人'];
|
||||
|
||||
/// 🔥 通用判断:是否机器人(型号前缀 + 中文关键字双重匹配,新型号也能自动识别)
|
||||
static bool _isRobotDevice(String type, String name, String deviceId) {
|
||||
final haystacks = [
|
||||
type.toLowerCase(),
|
||||
name.toLowerCase(),
|
||||
deviceId.toLowerCase(),
|
||||
];
|
||||
for (final s in haystacks) {
|
||||
for (final p in _robotPrefixes) {
|
||||
if (s.startsWith(p)) return true;
|
||||
}
|
||||
}
|
||||
for (final k in _robotTypeKeywords) {
|
||||
if (type.contains(k) || name.contains(k)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -186,7 +200,11 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
||||
children: [
|
||||
const Flexible(child: SiteSelectorWidget(compact: true)),
|
||||
const SizedBox(width: 8),
|
||||
Container(width: 1, height: 20, color: context.appColors.divider),
|
||||
Container(
|
||||
width: 1,
|
||||
height: 20,
|
||||
color: context.appColors.divider,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
AppLocalizations.of(
|
||||
@@ -310,7 +328,10 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
style: TextStyle(fontSize: 14, color: context.appColors.textPrimary),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: context.appColors.textPrimary,
|
||||
),
|
||||
onSubmitted: (value) {
|
||||
context.read<DeviceListBloc.DeviceStatusBloc>().add(
|
||||
DeviceListEvent.DeviceStatusSearch(value),
|
||||
@@ -490,13 +511,16 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
||||
} else {
|
||||
// 🔥 "全部"模式下,排除已由专用组件渲染的类型,避免重复展示
|
||||
// drone_station 已通过 _buildDroneStationList 渲染
|
||||
// 机器人(通过名称前缀识别)已通过 _buildEmbeddedRobotList 渲染
|
||||
// 机器人通过下面的 _isRobotDevice 通用匹配识别,已由 _buildEmbeddedRobotList 渲染
|
||||
filteredByType = state.devices.where((device) {
|
||||
if (device.type == 'drone_station') return false;
|
||||
final isRobot = _robotPrefixes.any(
|
||||
(prefix) =>
|
||||
device.name.startsWith(prefix) ||
|
||||
device.deviceId.startsWith(prefix),
|
||||
final isRobot = _isRobotDevice(
|
||||
device.type,
|
||||
device.name,
|
||||
device.deviceId,
|
||||
);
|
||||
debugPrint(
|
||||
'🔍 [全部-过滤] deviceId=${device.deviceId} type=${device.type} name=${device.name} → isRobot=$isRobot ${isRobot ? '✅排除' : ''}',
|
||||
);
|
||||
return !isRobot;
|
||||
}).toList();
|
||||
@@ -722,7 +746,9 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
||||
builder: (context, state) {
|
||||
if (state is DroneStationLoading) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(color: context.appColors.primary),
|
||||
child: CircularProgressIndicator(
|
||||
color: context.appColors.primary,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1042,9 +1068,8 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
||||
debugPrint('🔍 [DeviceStatusPage] 设备ID: $deviceId');
|
||||
debugPrint('🔍 [DeviceStatusPage] 设备类型: $deviceType');
|
||||
|
||||
// 🔥 判断是否为机器人(根据设备类型前缀)
|
||||
final robotPrefixes = _robotPrefixes;
|
||||
bool isRobot = robotPrefixes.any((prefix) => deviceType.startsWith(prefix));
|
||||
// 🔥 判断是否为机器人:用通用匹配(型号前缀 + 中文关键字)
|
||||
bool isRobot = _isRobotDevice(deviceType, deviceName, deviceId);
|
||||
|
||||
// 🔥 判断是否为无人机机场(根据设备类型包含关键字)
|
||||
final droneStationKeywords = [
|
||||
@@ -1065,22 +1090,8 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
||||
(deviceType.isEmpty || deviceType == '未知设备' || deviceType == '未知类型')) {
|
||||
debugPrint('⚠️ [DeviceStatusPage] type 字段无效,使用设备名称进行二次判断');
|
||||
|
||||
// 检查每个机器人前缀是否匹配
|
||||
for (final prefix in robotPrefixes) {
|
||||
final nameMatch = deviceName.startsWith(prefix);
|
||||
final idMatch = deviceId.startsWith(prefix);
|
||||
if (nameMatch || idMatch) {
|
||||
debugPrint(
|
||||
'🔍 [DeviceStatusPage] 机器人前缀匹配: "$prefix" -> 名称匹配=$nameMatch, ID匹配=$idMatch',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 根据设备名称前缀判断机器人
|
||||
isRobot = robotPrefixes.any(
|
||||
(prefix) =>
|
||||
deviceName.startsWith(prefix) || deviceId.startsWith(prefix),
|
||||
);
|
||||
// 根据通用匹配重新判断(已统一用 lowercase)
|
||||
isRobot = _isRobotDevice(deviceType, deviceName, deviceId);
|
||||
|
||||
// 检查每个无人机关键字是否匹配
|
||||
for (final keyword in droneStationKeywords) {
|
||||
|
||||
@@ -1460,7 +1460,7 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
||||
try {
|
||||
final dio = DioClient.create();
|
||||
final response = await dio.post(
|
||||
'http://1.95.137.212:59003/iot/UAV/flightTaskCommand',
|
||||
'${HttpApiConsts.baseUrl}/iot/UAV/flightTaskCommand',
|
||||
data: {'command': command, 'deviceSn': widget.droneSn},
|
||||
);
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ class _BluetoothScanModalState extends State<BluetoothScanModal> {
|
||||
int _connectingCountdown = 15;
|
||||
Timer? _connectingTimer;
|
||||
bool _isStartingScan = false;
|
||||
String _searchQuery = '';
|
||||
|
||||
StreamSubscription<BluetoothAdapterState>? _stateSubscription;
|
||||
StreamSubscription<List<ScanResult>>? _scanSubscription;
|
||||
@@ -123,7 +124,7 @@ class _BluetoothScanModalState extends State<BluetoothScanModal> {
|
||||
await BleManager.instance.openBluetooth();
|
||||
}
|
||||
|
||||
/// 排序:已连接设备排第一,有名称设备优先
|
||||
/// 排序:已连接设备排第一,纯数字名称(时间戳样式)置顶,有名称设备优先
|
||||
List<ScanResult> _sortResults(List<ScanResult> results) {
|
||||
final mgrConnected = BleManager.instance.connectedDevice;
|
||||
final sorted = List<ScanResult>.from(results);
|
||||
@@ -133,7 +134,12 @@ class _BluetoothScanModalState extends State<BluetoothScanModal> {
|
||||
final bConnected = b.device.remoteId == mgrConnected?.remoteId;
|
||||
if (aConnected && !bConnected) return -1;
|
||||
if (!aConnected && bConnected) return 1;
|
||||
// 第二级:有名称的排前面
|
||||
// 第二级:纯数字名称(时间戳样式,如 17xxxx)排前面
|
||||
final aIsNumeric = _isNumericName(a);
|
||||
final bIsNumeric = _isNumericName(b);
|
||||
if (aIsNumeric && !bIsNumeric) return -1;
|
||||
if (!aIsNumeric && bIsNumeric) return 1;
|
||||
// 第三级:有名称的排前面
|
||||
final aHasName = _hasDeviceName(a);
|
||||
final bHasName = _hasDeviceName(b);
|
||||
if (aHasName && !bHasName) return -1;
|
||||
@@ -152,6 +158,27 @@ class _BluetoothScanModalState extends State<BluetoothScanModal> {
|
||||
r.device.advName.isNotEmpty;
|
||||
}
|
||||
|
||||
/// 获取设备显示名称
|
||||
String _getDeviceName(ScanResult r) {
|
||||
final advData = r.advertisementData;
|
||||
return advData.advName.isNotEmpty
|
||||
? advData.advName
|
||||
: (r.device.platformName.isNotEmpty
|
||||
? r.device.platformName
|
||||
: (advData.localName?.isNotEmpty == true
|
||||
? advData.localName!
|
||||
: (r.device.advName.isNotEmpty
|
||||
? r.device.advName
|
||||
: '${r.device.remoteId}')));
|
||||
}
|
||||
|
||||
/// 判断设备名称是否为纯数字(时间戳样式,如 1777272534998)
|
||||
bool _isNumericName(ScanResult r) {
|
||||
final name = _getDeviceName(r);
|
||||
if (name.isEmpty) return false;
|
||||
return RegExp(r'^\d+$').hasMatch(name);
|
||||
}
|
||||
|
||||
/// 从详情页返回时同步连接状态(包括连接中和已连接)
|
||||
void _syncConnectedDevice() {
|
||||
final mgrConnected = BleManager.instance.connectedDevice;
|
||||
@@ -448,12 +475,22 @@ class _BluetoothScanModalState extends State<BluetoothScanModal> {
|
||||
|
||||
Widget _buildDeviceList() {
|
||||
// 过滤掉已连接设备(它已在状态卡片下方单独显示)
|
||||
final otherResults = _connectedDevice != null
|
||||
var otherResults = _connectedDevice != null
|
||||
? _scanResults
|
||||
.where((r) => r.device.remoteId != _connectedDevice!.remoteId)
|
||||
.toList()
|
||||
: _scanResults;
|
||||
|
||||
// 按搜索关键词过滤
|
||||
if (_searchQuery.isNotEmpty) {
|
||||
final query = _searchQuery.toLowerCase();
|
||||
otherResults = otherResults.where((r) {
|
||||
final name = _getDeviceName(r).toLowerCase();
|
||||
final mac = r.device.remoteId.toString().toLowerCase();
|
||||
return name.contains(query) || mac.contains(query);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -466,6 +503,53 @@ class _BluetoothScanModalState extends State<BluetoothScanModal> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 搜索框
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF2F3F5),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.search, size: 18, color: Color(0xFF86909C)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
onChanged: (value) {
|
||||
setState(() => _searchQuery = value);
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
hintText: '搜索设备名称或MAC',
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 10),
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_searchQuery.isNotEmpty)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() => _searchQuery = '');
|
||||
},
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
size: 16,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!_isBluetoothOn)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 24),
|
||||
@@ -487,20 +571,20 @@ class _BluetoothScanModalState extends State<BluetoothScanModal> {
|
||||
),
|
||||
)
|
||||
else if (otherResults.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 24),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
child: const CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'正在扫描附近设备...',
|
||||
style: TextStyle(color: Color(0xFF86909C)),
|
||||
_searchQuery.isNotEmpty ? '未找到匹配的设备' : '正在扫描附近设备...',
|
||||
style: const TextStyle(color: Color(0xFF86909C)),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -581,10 +665,7 @@ class _BluetoothScanModalState extends State<BluetoothScanModal> {
|
||||
),
|
||||
child: const Text(
|
||||
'已连接',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white,
|
||||
),
|
||||
style: TextStyle(fontSize: 10, color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -57,7 +57,7 @@ class MyRemoteDataSourceImpl implements MyRemoteDataSource {
|
||||
id: 'about_us',
|
||||
title: '关于我们',
|
||||
icon: 'info',
|
||||
subTitle: 'V2.1.0',
|
||||
subTitle: 'V1.0.1',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -10,6 +11,8 @@ import 'package:maibu_satabot_v2/core/storage/user_storage.dart';
|
||||
import 'package:maibu_satabot_v2/features/auth/presentation/bloc/auth_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/main_container/presentation/cubit/tab_config_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/cubit/float_bar_setting_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/core/update/update_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/core/update/update_state.dart';
|
||||
|
||||
/// 系统设置综合页面
|
||||
class SystemSettingsPage extends StatefulWidget {
|
||||
@@ -20,6 +23,25 @@ class SystemSettingsPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||
String _currentVersion = '';
|
||||
int _currentVersionCode = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadVersion();
|
||||
}
|
||||
|
||||
Future<void> _loadVersion() async {
|
||||
try {
|
||||
final String versionString = await rootBundle.loadString('pubspec.yaml');
|
||||
final versionMatch = RegExp(r'version:\s*(\d+\.\d+\.\d+)\+(\d+)').firstMatch(versionString);
|
||||
setState(() {
|
||||
_currentVersion = versionMatch?.group(1) ?? '1.0.0';
|
||||
_currentVersionCode = int.tryParse(versionMatch?.group(2) ?? '1') ?? 1;
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -52,6 +74,9 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||
// 语言设置
|
||||
_buildLanguageSection(),
|
||||
const SizedBox(height: 12),
|
||||
// 版本更新
|
||||
_buildUpdateSection(),
|
||||
const SizedBox(height: 12),
|
||||
// 退出登录
|
||||
_buildLogoutButton(context),
|
||||
const SizedBox(height: 20),
|
||||
@@ -297,6 +322,105 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 版本更新
|
||||
Widget _buildUpdateSection() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: context.appColors.cardBackground,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: context.appColors.cardShadow,
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: BlocConsumer<UpdateCubit, UpdateState>(
|
||||
listener: (context, state) {
|
||||
if (state is UpdateUpToDate) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('当前已是最新版本'),
|
||||
backgroundColor: context.appColors.success,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
final isChecking = state is UpdateChecking;
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'版本更新',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: context.appColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'当前版本: $_currentVersion ($_currentVersionCode)',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: context.appColors.textTertiary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
isChecking ? '正在检查更新...' : '点击检查是否有新版本',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: context.appColors.textTertiary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isChecking)
|
||||
const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
else
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.read<UpdateCubit>().checkUpdate();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF165DFF),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
'检查更新',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 退出登录按钮
|
||||
Widget _buildLogoutButton(BuildContext context) {
|
||||
return GestureDetector(
|
||||
|
||||
Reference in New Issue
Block a user