绑定设备+覆盖模式+控制显示bug+在线更新

This commit is contained in:
2026-09-01 08:59:20 +08:00
parent 0fa67a127b
commit 61bce2c1cf
49 changed files with 2424 additions and 720 deletions

View File

@@ -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";
}

View File

@@ -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()),
);

View File

@@ -14,14 +14,14 @@ class EnvConfig {
static bool get isProduction => environment == 'prod';
/// TCP 服务器配置
static String get tcpIp => '1.95.137.212';
static int get tcpPort {
// 测试服: 59016, 生产服: 9001(目前暂时都走测试服 59004,回退时改回 9001)
if (environment == 'prod') {
return 59004;
}
return 59004; // TCP 端口
}
// ─── 服务器 IP(测试/生产共用同一台机器) ───
static const String serverHost = '1.95.137.212';
// ─── 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';
}

View 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')}]';
}
}

View File

@@ -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();
}
}

View File

@@ -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');

View File

@@ -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();
}
}

View File

@@ -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];
}

View File

@@ -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];
}

View File

@@ -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,

View File

@@ -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();
}

View File

@@ -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 连接已断开');

View File

@@ -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 data = response.data['data'];
final respCode = response.data['code'];
final data = response.data['data'];
final message = response.data['message'] ?? '';
if (data['hasUpdate'] == false) {
_logger.i('✅ 已是最新版本');
return null;
}
final versionInfo = AppVersionInfo.fromJson(data);
// 🔥 关键修复:无论更新类型是什么,只要本地已应用此版本,就跳过
if (appliedPatchVersion != null && appliedPatchVersion == versionInfo.version) {
_logger.i('✅ 本地已应用版本 ${versionInfo.version},跳过更新(updateType: ${versionInfo.updateType})');
return null;
}
// 🔥 额外检查:如果本地补丁版本code >= 服务端返回的versionCode,也跳过
if (appliedPatchVersionCode != null && appliedPatchVersionCode >= versionInfo.versionCode) {
_logger.i('✅ 本地补丁版本code ($appliedPatchVersionCode) >= 服务端versionCode (${versionInfo.versionCode}),跳过更新');
return null;
}
_logger.i('✅ 发现新版本: ${versionInfo.version}');
_logger.i('🔄 更新类型: ${versionInfo.updateType}');
_logger.i('⚠️ 强制更新: ${versionInfo.forceUpdate}');
return versionInfo;
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) {
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) {
debugPrint('[RESULT] No update - already at version ${versionInfo.version}');
debugPrint('========================================');
debugPrint('');
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}');
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');
}
}