diff --git a/.gitignore b/.gitignore
index 78443a9f..e202311b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -114,4 +114,23 @@ test_run.log
Thumbs.db
# 环境变量敏感信息
-.env*
\ No newline at end of file
+.env*
+
+# ----------------------------------------------------------------------------
+# 备份文件与一次性调试产物(不需要提交)
+# ----------------------------------------------------------------------------
+*.bak
+*.bak-*
+pubspec.lock.bak*
+fix_lock.py
+git_lock_versions.json
+
+# ----------------------------------------------------------------------------
+# 临时文档 / 软著手册生成物料(不需要提交)
+# ----------------------------------------------------------------------------
+temp_*.md
+temp_*.txt
+temp_*.py
+temp_*
+temp_docx_gen/
+*.docx
\ No newline at end of file
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 0d290213..b0653c0a 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -9,6 +9,10 @@
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
+analyzer:
+ exclude:
+ - packages/**
+
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
diff --git a/android/app/src/main/res/drawable-hdpi/launch_image.png b/android/app/src/main/res/drawable-hdpi/launch_image.png
new file mode 100644
index 00000000..4c2dac7d
Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/launch_image.png differ
diff --git a/android/app/src/main/res/drawable-mdpi/launch_image.png b/android/app/src/main/res/drawable-mdpi/launch_image.png
new file mode 100644
index 00000000..71faafbd
Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/launch_image.png differ
diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml
index f74085f3..cce2eaf2 100644
--- a/android/app/src/main/res/drawable-v21/launch_background.xml
+++ b/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -3,10 +3,10 @@
-
-
+ -
-
-->
+ android:src="@drawable/launch_image" />
+
diff --git a/android/app/src/main/res/drawable-xhdpi/launch_image.png b/android/app/src/main/res/drawable-xhdpi/launch_image.png
new file mode 100644
index 00000000..cdedd2f2
Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/launch_image.png differ
diff --git a/android/app/src/main/res/drawable-xxhdpi/launch_image.png b/android/app/src/main/res/drawable-xxhdpi/launch_image.png
new file mode 100644
index 00000000..b33c9889
Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/launch_image.png differ
diff --git a/android/app/src/main/res/drawable-xxxhdpi/launch_image.png b/android/app/src/main/res/drawable-xxxhdpi/launch_image.png
new file mode 100644
index 00000000..f37b6215
Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/launch_image.png differ
diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml
index 304732f8..856b6b20 100644
--- a/android/app/src/main/res/drawable/launch_background.xml
+++ b/android/app/src/main/res/drawable/launch_background.xml
@@ -3,10 +3,10 @@
-
-
+ -
-
-->
+ android:src="@drawable/launch_image" />
+
diff --git a/android/app/src/main/res/xml/device_filter.xml b/android/app/src/main/res/xml/device_filter.xml
new file mode 100644
index 00000000..2c7588ca
--- /dev/null
+++ b/android/app/src/main/res/xml/device_filter.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts
index ca7fe065..d36178c8 100644
--- a/android/settings.gradle.kts
+++ b/android/settings.gradle.kts
@@ -11,6 +11,8 @@ pluginManagement {
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
+ maven { url = uri("https://maven.aliyun.com/repository/google") }
+ maven { url = uri("https://maven.aliyun.com/repository/public") }
google()
mavenCentral()
gradlePluginPortal()
diff --git a/assets/languages/en-US.json b/assets/languages/en-US.json
index 626b3bfb..7abd423c 100644
--- a/assets/languages/en-US.json
+++ b/assets/languages/en-US.json
@@ -260,6 +260,130 @@
"emergency_stop": "E-Stop",
"emergency_stopping": "Stopping"
},
+
+ "pv_control": {
+ "manual_control": "Manual",
+ "pause_task": "Pause",
+ "screenshot": "Snapshot",
+ "view_map": "Map",
+ "emergency_stop": "E-Stop",
+ "emergency_stopping": "Stopping",
+ "sheet_mower_title": "Mower Lift",
+ "sheet_mower_hint": "Left: raise · Right: lower · Release: stop",
+ "sheet_chassis_title": "Chassis Lift",
+ "sheet_chassis_hint": "Left: up · Right: down · Release: stop",
+ "sheet_fire_title": "Ignition",
+ "sheet_fire_hint": "Left: ignite · Right: extinguish · Center: stop",
+ "seg_mower_up": "Raise",
+ "seg_mower_down": "Lower",
+ "seg_chassis_up": "Up",
+ "seg_chassis_down": "Down",
+ "seg_fire_up": "Ignite",
+ "seg_fire_down": "Extinguish",
+ "seg_stop": "Stop",
+ "scrub_hint": "Press & hold \"{label}\", then slide",
+ "toast_paused": "Task paused",
+ "toast_emergency_sent": "Emergency stop sent",
+ "toast_emergency_released": "Emergency stop released",
+ "toast_screenshot_failed": "Screenshot failed",
+ "toast_screenshot_saved": "Screenshot saved",
+ "toast_permission_released": "Remote control released",
+ "toast_permission_release_failed": "Failed to release control",
+ "remote_mode": "Remote Mode",
+ "online": "Online",
+ "offline": "Offline",
+ "unknown": "Unknown",
+ "nav_task": "Task",
+ "nav_map": "Map",
+ "nav_video": "Video",
+ "nav_settings": "Settings",
+ "mode_remote": "Remote",
+ "mode_local": "Local",
+ "qual_none": "No Fix",
+ "qual_single": "Single",
+ "qual_differential": "DGPS",
+ "qual_ins": "Dead Reckoning",
+ "qual_rtk_fixed": "RTK Fixed",
+ "qual_rtk_float": "RTK Float",
+ "qual_rtk_fixed_nh": "RTK Fixed (no heading)",
+ "qual_rtk_float_nh": "RTK Float (no heading)",
+ "obstacle_ahead": "Obstacle Ahead",
+ "view_left_pip": "Left View",
+ "view_right_pip": "Right View",
+ "rear_view": "Rear View",
+ "reverse_camera": "Reverse Cam",
+ "map_current_task": "Task: Auto Mowing - Zone 1",
+ "map_switch": "Switch ›",
+ "work_progress": "Progress",
+ "work_area_size": "Area",
+ "work_duration": "Duration",
+ "vehicle_status": "Vehicle",
+ "status_normal": "Normal",
+ "status_abnormal": "Abnormal",
+ "drive_speed": "Speed",
+ "gear": "Gear",
+ "gear_low": "Low",
+ "chassis_height": "Height",
+ "mower_status": "Mower",
+ "mower_running": "Running",
+ "battery_label": "Battery",
+ "current_speed": "Speed",
+ "patrol_mileage": "Mileage",
+ "network_status": "Network",
+ "device_temperature": "Temp",
+ "battery_unknown": "Battery N/A",
+ "battery_percent": "Battery {n}%",
+ "auto_driving": "Auto Driving",
+ "manual_controlling": "Manual Control",
+ "switch_mode": "Switch Mode",
+ "rtsp_searching": "Searching local camera…",
+ "rtsp_connecting": "Connecting stream…",
+ "rtsp_reconnecting": "Signal lost, reconnecting…",
+ "rtsp_not_found": "No camera found, retrying…",
+ "rtsp_failed": "Stream failed, retrying…",
+ "rtsp_no_ethernet": "No Ethernet, check the cable",
+ "map_waiting_push": "Waiting for location…",
+ "task_current": "Current Task",
+ "task_none_running": "No running task",
+ "task_field_no": "Task No.",
+ "task_field_device": "Device No.",
+ "task_field_status": "Status",
+ "task_field_route": "Route No.",
+ "task_field_site": "Plot No.",
+ "task_field_create_time": "Created",
+ "task_none_value": "None",
+ "task_new": "New",
+ "task_executing": "Running",
+ "task_paused": "Paused",
+ "set_video_source": "Source",
+ "set_source_rtsp": "Ethernet RTSP",
+ "set_source_hdmi": "HDMI",
+ "set_source_webrtc": "WebRTC",
+ "set_video_fit": "Display",
+ "set_fit_contain": "Fit",
+ "set_fit_cover": "Fill",
+ "set_fit_contain_hint": "No crop; letterbox if ratios differ",
+ "set_fit_cover_hint": "Fill with no bars; edges cropped",
+ "set_sim_obstacle": "Simulate Obstacle",
+ "set_sim_obstacle_sub": "Alert: banner + box + red flash",
+ "set_sim_reverse": "Reverse Camera",
+ "set_sim_reverse_sub": "Overlay reverse guide lines",
+ "perm_unknown_device": "Unknown device",
+ "perm_unknown_platform": "Unknown platform",
+ "perm_request_body": "{platform} is requesting control\n\nDevice ID: {deviceId}\n\nRelease control?",
+ "realtime_video": "Live Video",
+ "pv_robot_remote_control": "PV Robot Remote Control",
+ "screenshot_saved_path": "Saved: {path}",
+ "close": "Close",
+ "rail_return_home": "Return Home",
+ "rail_find_vehicle": "Find Vehicle",
+ "rail_pause_work": "Pause Work",
+ "rail_light": "Lights",
+ "rail_horn": "Horn",
+ "rail_record_shot": "Record/Snapshot",
+ "speed_high": "High",
+ "speed_low": "Low"
+ },
"route_planning": {
"title": "Route Planning",
diff --git a/assets/languages/zh-CN.json b/assets/languages/zh-CN.json
index 7ef16bb1..e1ba3914 100644
--- a/assets/languages/zh-CN.json
+++ b/assets/languages/zh-CN.json
@@ -274,6 +274,130 @@
"emergency_stop": "急停",
"emergency_stopping": "急停中"
},
+
+ "pv_control": {
+ "manual_control": "手动控制",
+ "pause_task": "暂停任务",
+ "screenshot": "截图",
+ "view_map": "查看地图",
+ "emergency_stop": "紧急停止",
+ "emergency_stopping": "急停中",
+ "sheet_mower_title": "割刀升降",
+ "sheet_mower_hint": "左滑抬刀 · 右滑降刀 · 松手停",
+ "sheet_chassis_title": "底盘升降",
+ "sheet_chassis_hint": "左滑升 · 右滑降 · 松手停",
+ "sheet_fire_title": "点火",
+ "sheet_fire_hint": "左滑点火 · 右滑熄火 · 中间停",
+ "seg_mower_up": "抬刀",
+ "seg_mower_down": "降刀",
+ "seg_chassis_up": "升高",
+ "seg_chassis_down": "降低",
+ "seg_fire_up": "点火",
+ "seg_fire_down": "熄火",
+ "seg_stop": "停止",
+ "scrub_hint": "长按「{label}」后左右滑动",
+ "toast_paused": "已暂停任务",
+ "toast_emergency_sent": "已发送紧急停止",
+ "toast_emergency_released": "已解除紧急停止",
+ "toast_screenshot_failed": "截图失败",
+ "toast_screenshot_saved": "截图已保存",
+ "toast_permission_released": "已释放远程控制权限",
+ "toast_permission_release_failed": "控制权限释放失败",
+ "remote_mode": "遥控模式",
+ "online": "在线",
+ "offline": "离线",
+ "unknown": "未知",
+ "nav_task": "任务",
+ "nav_map": "地图",
+ "nav_video": "视频",
+ "nav_settings": "设置",
+ "mode_remote": "远程",
+ "mode_local": "本地",
+ "qual_none": "无定位",
+ "qual_single": "单点定位",
+ "qual_differential": "差分定位",
+ "qual_ins": "惯性导航",
+ "qual_rtk_fixed": "RTK 固定解",
+ "qual_rtk_float": "RTK 浮点解",
+ "qual_rtk_fixed_nh": "RTK 固定(无航向)",
+ "qual_rtk_float_nh": "RTK 浮点(无航向)",
+ "obstacle_ahead": "前方有障碍物",
+ "view_left_pip": "左视角",
+ "view_right_pip": "右视角",
+ "rear_view": "后视图",
+ "reverse_camera": "倒车影像",
+ "map_current_task": "当前任务:自主割草-1区",
+ "map_switch": "切换 ›",
+ "work_progress": "作业进度",
+ "work_area_size": "作业面积",
+ "work_duration": "作业时长",
+ "vehicle_status": "车辆状态",
+ "status_normal": "正常",
+ "status_abnormal": "异常",
+ "drive_speed": "行驶速度",
+ "gear": "档位",
+ "gear_low": "低速",
+ "chassis_height": "底盘高度",
+ "mower_status": "割刀状态",
+ "mower_running": "运行中",
+ "battery_label": "电池电量",
+ "current_speed": "当前速度",
+ "patrol_mileage": "巡检里程",
+ "network_status": "网络状态",
+ "device_temperature": "设备温度",
+ "battery_unknown": "电量 未知",
+ "battery_percent": "电量 {n}%",
+ "auto_driving": "自动驾驶中",
+ "manual_controlling": "手动控制中",
+ "switch_mode": "切换模式",
+ "rtsp_searching": "正在搜索本地摄像头…",
+ "rtsp_connecting": "正在连接视频流…",
+ "rtsp_reconnecting": "信号中断,自动重连中…",
+ "rtsp_not_found": "未发现摄像头,自动重试中…",
+ "rtsp_failed": "拉流失败,自动重试中…",
+ "rtsp_no_ethernet": "未检测到以太网,请检查网线",
+ "map_waiting_push": "等待定位推送…",
+ "task_current": "当前任务",
+ "task_none_running": "暂无执行中的任务",
+ "task_field_no": "任务编号",
+ "task_field_device": "设备编号",
+ "task_field_status": "任务状态",
+ "task_field_route": "路线编号",
+ "task_field_site": "地块编号",
+ "task_field_create_time": "创建时间",
+ "task_none_value": "无",
+ "task_new": "新建",
+ "task_executing": "执行中",
+ "task_paused": "暂停中",
+ "set_video_source": "视频源",
+ "set_source_rtsp": "以太网 RTSP 源",
+ "set_source_hdmi": "HDMI",
+ "set_source_webrtc": "WebRTC",
+ "set_video_fit": "视频展示",
+ "set_fit_contain": "完整显示",
+ "set_fit_cover": "铺满屏幕",
+ "set_fit_contain_hint": "画面不裁,窗口比例不合时上下或左右留黑边",
+ "set_fit_cover_hint": "等比放大到无黑边,超出窗口的边缘被裁掉",
+ "set_sim_obstacle": "模拟障碍物信息",
+ "set_sim_obstacle_sub": "收到告警推送:横幅 + 障碍框 + 屏幕泛红同时出现",
+ "set_sim_reverse": "倒车影像",
+ "set_sim_reverse_sub": "后视卡叠加倒车引导线(模拟倒车挡)",
+ "perm_unknown_device": "未知设备",
+ "perm_unknown_platform": "未知平台",
+ "perm_request_body": "{platform} 端正在请求控制权\n\n设备ID: {deviceId}\n\n是否同意释放控制权?",
+ "realtime_video": "实时视频",
+ "pv_robot_remote_control": "光伏电站机器人远程控制",
+ "screenshot_saved_path": "已保存:{path}",
+ "close": "关闭",
+ "rail_return_home": "一键返航",
+ "rail_find_vehicle": "一键寻车",
+ "rail_pause_work": "暂停作业",
+ "rail_light": "灯光",
+ "rail_horn": "鸣笛",
+ "rail_record_shot": "录像截图",
+ "speed_high": "高速",
+ "speed_low": "低速"
+ },
"route_planning": {
"title": "路径规划",
diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift
index c30b367e..2a841826 100644
--- a/ios/Runner/AppDelegate.swift
+++ b/ios/Runner/AppDelegate.swift
@@ -1,10 +1,10 @@
import Flutter
import UIKit
-@main
+@mainlication(
+ _ application
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
- override func application(
- _ application: UIApplication,
+ override func app: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
diff --git a/lib/components/language_selection_dialog.dart b/lib/components/language_selection_dialog.dart
index e38767b5..a171e439 100644
--- a/lib/components/language_selection_dialog.dart
+++ b/lib/components/language_selection_dialog.dart
@@ -38,8 +38,8 @@ class AppLanguages {
}
/// 语言选择弹窗。
-/// [mandatory] = true:登录时强制选择,不可返回/点外部关闭;
-/// [mandatory] = false:设置页中可随时关闭。
+/// [mandatory] = true:登录时强制弹出,拦截系统返回键;可点某个语言即时切换,或点“继续”确认当前默认语言后关闭。
+/// [mandatory] = false:设置页中普通选择,可取消。
class LanguageSelectionDialog extends StatelessWidget {
final bool mandatory;
const LanguageSelectionDialog({super.key, this.mandatory = true});
@@ -49,6 +49,12 @@ class LanguageSelectionDialog extends StatelessWidget {
if (context.mounted) Navigator.of(context).pop();
}
+ /// “继续”:不改变当前(默认)语言,仅确认已选并关闭,后续启动不再弹。
+ Future _onContinue(BuildContext context, Locale current) async {
+ await context.read().setLocale(current);
+ if (context.mounted) Navigator.of(context).pop();
+ }
+
@override
Widget build(BuildContext context) {
final current = context.select(
@@ -79,14 +85,15 @@ class LanguageSelectionDialog extends StatelessWidget {
],
),
),
- actions: mandatory
- ? null
- : [
- TextButton(
- onPressed: () => Navigator.of(context).pop(),
- child: const Text('取消'),
- ),
- ],
+ actions: [
+ // 强制弹窗:提供“继续”出口,保持当前默认语言并确认后关闭;非强制:取消
+ TextButton(
+ onPressed: () => mandatory
+ ? _onContinue(context, current)
+ : Navigator.of(context).pop(),
+ child: Text(mandatory ? '继续 / Continue' : '取消'),
+ ),
+ ],
),
);
}
diff --git a/lib/core/network/mqtt/data/datasources/mower_realtime_datasource.dart b/lib/core/network/mqtt/data/datasources/mower_realtime_datasource.dart
index ffe8fb5c..9464518c 100644
--- a/lib/core/network/mqtt/data/datasources/mower_realtime_datasource.dart
+++ b/lib/core/network/mqtt/data/datasources/mower_realtime_datasource.dart
@@ -8,6 +8,7 @@ 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';
+import '../../domain/entities/host_realtime_message_entity.dart';
/// 割草机实时状态 MQTT 数据源(替代原 TCP 0x02 机器状态推送)
///
@@ -20,6 +21,9 @@ abstract class MowerRealtimeDataSource {
Stream get locationStream;
Stream get routeStatusStream;
+ /// 🔥 主机实时状态(device/{sn}/host/realTime/message,新增话题,纯附加)
+ Stream get hostRealtimeStream;
+
/// 按设备 SN 启动订阅:自动退订旧设备主题、同 SN 去重
Future startListening({required String deviceSn});
@@ -32,11 +36,14 @@ class MowerRealtimeDataSourceImpl implements MowerRealtimeDataSource {
static const String _realtimeSuffix = 'property/realtime/post';
static const String _locationSuffix = 'property/location/post';
static const String _routeSuffix = 'route/post';
+ static const String _hostRealtimeSuffix = 'host/realTime/message';
final _vehicleController =
StreamController.broadcast();
final _locationController = StreamController.broadcast();
final _routeController = StreamController.broadcast();
+ final _hostRealtimeController =
+ StreamController.broadcast();
StreamSubscription? _subscription;
@@ -47,6 +54,7 @@ class MowerRealtimeDataSourceImpl implements MowerRealtimeDataSource {
int _vehicleMsgCount = 0;
int _locationMsgCount = 0;
int _routeMsgCount = 0;
+ int _hostMsgCount = 0;
MowerRealtimeDataSourceImpl(this.mqttClient);
@@ -59,9 +67,14 @@ class MowerRealtimeDataSourceImpl implements MowerRealtimeDataSource {
@override
Stream get routeStatusStream => _routeController.stream;
+ @override
+ Stream get hostRealtimeStream =>
+ _hostRealtimeController.stream;
+
String _realtimeTopic(String sn) => 'mower/$sn/$_realtimeSuffix';
String _locationTopic(String sn) => 'mower/$sn/$_locationSuffix';
String _routeTopic(String sn) => 'device/$sn/$_routeSuffix';
+ String _hostRealtimeTopic(String sn) => 'device/$sn/$_hostRealtimeSuffix';
@override
Future startListening({required String deviceSn}) async {
@@ -81,6 +94,7 @@ class MowerRealtimeDataSourceImpl implements MowerRealtimeDataSource {
await mqttClient.unsubscribe(_realtimeTopic(_deviceSn!));
await mqttClient.unsubscribe(_locationTopic(_deviceSn!));
await mqttClient.unsubscribe(_routeTopic(_deviceSn!));
+ await mqttClient.unsubscribe(_hostRealtimeTopic(_deviceSn!));
debugPrint('${LogTime.now()} 🔕 [MowerRealtimeDataSource] 已退订旧设备: $_deviceSn');
}
@@ -88,10 +102,12 @@ class MowerRealtimeDataSourceImpl implements MowerRealtimeDataSource {
_vehicleMsgCount = 0;
_locationMsgCount = 0;
_routeMsgCount = 0;
+ _hostMsgCount = 0;
await mqttClient.subscribe(_realtimeTopic(deviceSn));
await mqttClient.subscribe(_locationTopic(deviceSn));
await mqttClient.subscribe(_routeTopic(deviceSn));
+ await mqttClient.subscribe(_hostRealtimeTopic(deviceSn));
// 消息流监听只建立一次,按 _deviceSn 过滤
_subscription ??= mqttClient.messageStream?.listen(_handleMessage);
@@ -100,6 +116,7 @@ class MowerRealtimeDataSourceImpl implements MowerRealtimeDataSource {
debugPrint(' 车辆实时: ${_realtimeTopic(deviceSn)}');
debugPrint(' 定位: ${_locationTopic(deviceSn)}');
debugPrint(' 路径规划: ${_routeTopic(deviceSn)}');
+ debugPrint(' 主机实时: ${_hostRealtimeTopic(deviceSn)}');
}
@override
@@ -109,6 +126,7 @@ class MowerRealtimeDataSourceImpl implements MowerRealtimeDataSource {
await mqttClient.unsubscribe(_realtimeTopic(_deviceSn!));
await mqttClient.unsubscribe(_locationTopic(_deviceSn!));
await mqttClient.unsubscribe(_routeTopic(_deviceSn!));
+ await mqttClient.unsubscribe(_hostRealtimeTopic(_deviceSn!));
debugPrint('${LogTime.now()} 🔕 [MowerRealtimeDataSource] 停止监听设备: $_deviceSn');
_deviceSn = null;
}
@@ -153,6 +171,16 @@ class MowerRealtimeDataSourceImpl implements MowerRealtimeDataSource {
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(_hostRealtimeSuffix)) {
+ // 🔥 主机实时(新增话题,纯附加):外层套信封,数据在 json.data,提取方式与定位一致
+ _hostMsgCount++;
+ if (_hostMsgCount == 1) {
+ debugPrint('${LogTime.now()} 🎉 [MQTT-主机实时] 首帧到达: $sn');
+ }
+ final dataObj = jsonData['data'] as Map? ?? jsonData;
+ final host = HostRealtimeMessageEntity.fromJson(dataObj);
+ debugPrint('${LogTime.now()} 🛰️ [MQTT-主机实时] 第$_hostMsgCount帧 电量=${host.batteryPercentage}% 电压=${host.batteryVoltage}V 控制模式=${host.controlMode} 急停=${host.emergencyStop} 运动=${host.motion}');
+ _hostRealtimeController.add(host);
} else if (message.topic.endsWith(_routeSuffix)) {
_routeMsgCount++;
if (_routeMsgCount == 1) {
@@ -174,5 +202,6 @@ class MowerRealtimeDataSourceImpl implements MowerRealtimeDataSource {
_vehicleController.close();
_locationController.close();
_routeController.close();
+ _hostRealtimeController.close();
}
}
diff --git a/lib/core/network/mqtt/data/repositories/mower_realtime_repository_impl.dart b/lib/core/network/mqtt/data/repositories/mower_realtime_repository_impl.dart
index 992237ac..5e659712 100644
--- a/lib/core/network/mqtt/data/repositories/mower_realtime_repository_impl.dart
+++ b/lib/core/network/mqtt/data/repositories/mower_realtime_repository_impl.dart
@@ -7,6 +7,7 @@ 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';
+import '../../domain/entities/host_realtime_message_entity.dart';
class MowerRealtimeRepositoryImpl implements MowerRealtimeRepository {
final MowerRealtimeDataSource dataSource;
@@ -23,6 +24,10 @@ class MowerRealtimeRepositoryImpl implements MowerRealtimeRepository {
Stream get routeStatusStream =>
dataSource.routeStatusStream;
+ @override
+ Stream get hostRealtimeStream =>
+ dataSource.hostRealtimeStream;
+
@override
Future> startListening({
required String deviceSn,
diff --git a/lib/core/network/mqtt/domain/entities/host_realtime_message_entity.dart b/lib/core/network/mqtt/domain/entities/host_realtime_message_entity.dart
new file mode 100644
index 00000000..5e46f4c0
--- /dev/null
+++ b/lib/core/network/mqtt/domain/entities/host_realtime_message_entity.dart
@@ -0,0 +1,132 @@
+import 'package:equatable/equatable.dart';
+
+/// 主机实时状态消息(MQTT 主题 device/{sn}/host/realTime/message)
+///
+/// 报文外层套一层信封,真正数据在 `data` 子对象(由 DataSource 提取后传入)。
+/// 字段与后端 Java 实体一一对应,采用防御式解析,缺字段用默认值兜底。
+class HostRealtimeMessageEntity extends Equatable {
+ final double batteryPercentage; // 实时电量百分比
+ final double batteryVoltage; // 实时电压
+ final double leftMotorCurrent; // 实时左电机电流
+ final double rightMotorCurrent; // 实时右电机电流
+ final double leftMotorTemperature; // 实时左电机温度
+ final double rightMotorTemperature; // 实时右电机温度
+ final double mainTemperature; // 实时主温度
+ final double yaw; // 实时航向角度
+ final double pitch; // 实时俯仰角度
+ final String headingStatus; // 实时航向状态
+ final double totalWorkArea; // 累计作业面积
+ final double totalWorkDuration; // 累计作业时长
+ final double totalWorkDistance; // 累计作业里程
+ final String motion; // moving|stopped|blocked 运动状态
+ final bool emergencyStop; // 急停状态
+ final double leftTargetRpm; // 实时左电机目标转速
+ final double rightTargetRpm; // 实时右电机目标转速
+ final double leftMeasuredRpm; // 实时左电机测量转速
+ final double rightMeasuredRpm; // 实时右电机测量转速
+ final double leftMotorRpm; // 实时左电机转速
+ final double rightMotorRpm; // 实时右电机转速
+ final double roll; // 实时翻滚角度
+ final String controlMode; // 当前控制模式(遥控器拨杆对应)
+
+ /// 原始 data 报文,便于后端补字段时兜底
+ final Map rawData;
+
+ const HostRealtimeMessageEntity({
+ this.batteryPercentage = 0,
+ this.batteryVoltage = 0,
+ this.leftMotorCurrent = 0,
+ this.rightMotorCurrent = 0,
+ this.leftMotorTemperature = 0,
+ this.rightMotorTemperature = 0,
+ this.mainTemperature = 0,
+ this.yaw = 0,
+ this.pitch = 0,
+ this.headingStatus = '',
+ this.totalWorkArea = 0,
+ this.totalWorkDuration = 0,
+ this.totalWorkDistance = 0,
+ this.motion = '',
+ this.emergencyStop = false,
+ this.leftTargetRpm = 0,
+ this.rightTargetRpm = 0,
+ this.leftMeasuredRpm = 0,
+ this.rightMeasuredRpm = 0,
+ this.leftMotorRpm = 0,
+ this.rightMotorRpm = 0,
+ this.roll = 0,
+ this.controlMode = '',
+ this.rawData = const {},
+ });
+
+ /// [data] 为报文 `data` 子对象(由 DataSource 提取,与 MowerLocationEntity 一致)
+ factory HostRealtimeMessageEntity.fromJson(Map data) {
+ return HostRealtimeMessageEntity(
+ batteryPercentage: _toDouble(data['battery_percentage']),
+ batteryVoltage: _toDouble(data['battery_voltage']),
+ leftMotorCurrent: _toDouble(data['left_motor_current']),
+ rightMotorCurrent: _toDouble(data['right_motor_current']),
+ leftMotorTemperature: _toDouble(data['left_motor_temperature']),
+ rightMotorTemperature: _toDouble(data['right_motor_temperature']),
+ mainTemperature: _toDouble(data['main_temperature']),
+ yaw: _toDouble(data['yaw']),
+ pitch: _toDouble(data['pitch']),
+ headingStatus: data['heading_status']?.toString() ?? '',
+ totalWorkArea: _toDouble(data['total_work_area']),
+ totalWorkDuration: _toDouble(data['total_work_duration']),
+ totalWorkDistance: _toDouble(data['total_work_distance']),
+ motion: data['motion']?.toString() ?? '',
+ emergencyStop: _toBool(data['emergency_stop']),
+ leftTargetRpm: _toDouble(data['left_target_rpm']),
+ rightTargetRpm: _toDouble(data['right_target_rpm']),
+ leftMeasuredRpm: _toDouble(data['left_measured_rpm']),
+ rightMeasuredRpm: _toDouble(data['right_measured_rpm']),
+ leftMotorRpm: _toDouble(data['left_motor_rpm']),
+ rightMotorRpm: _toDouble(data['right_motor_rpm']),
+ roll: _toDouble(data['roll']),
+ controlMode: data['control_mode']?.toString() ?? '',
+ rawData: data,
+ );
+ }
+
+ static double _toDouble(dynamic value) {
+ if (value == null) return 0;
+ if (value is num) return value.toDouble();
+ return double.tryParse(value.toString()) ?? 0;
+ }
+
+ static bool _toBool(dynamic value) {
+ if (value == null) return false;
+ if (value is bool) return value;
+ if (value is num) return value != 0;
+ final s = value.toString().toLowerCase();
+ return s == 'true' || s == '1';
+ }
+
+ @override
+ List