集成双 MQTT 客户端实现设备实时数据监控(未测试)
1.集成两个独立的 MQTT 客户端,支持 WebSocket 和 TCP 协议 2.实现无人机/机场 OSD 实时监控(ws://1.95.137.212:8083/mqtt) 3.实现任务状态消息订阅(tcp://1.95.137.212:59020) 4.采用清洁架构设计,分层清晰(Domain → Data → Presentation)
This commit is contained in:
@@ -91,6 +91,7 @@ import '../../features/v2/device_list/domain/usecases/get_uav_video_stream_useca
|
||||
import '../../features/v2/device_list/domain/usecases/update_flight_task_status_usecase.dart';
|
||||
import '../../features/v2/device_list/presentation/bloc/drone_station_bloc.dart';
|
||||
import '../../features/v2/device_list/presentation/bloc/robot_list_bloc.dart';
|
||||
import '../../features/v2/device_list/presentation/bloc/device_realtime_bloc.dart';
|
||||
import '../../features/v2/waring_center/data/datasources/alarm_remote_datasource.dart';
|
||||
import '../../features/v2/waring_center/data/datasources/impl/alarm_remote_datasource_impl.dart';
|
||||
import '../../features/v2/waring_center/data/repositories/alarm_repository_impl.dart';
|
||||
@@ -126,6 +127,14 @@ import '../network/dio_client.dart';
|
||||
import '../network/net_message_dispatcher.dart';
|
||||
import '../network/tcp/tcp_client.dart';
|
||||
import '../network/tcp/tcp_status_cubit.dart';
|
||||
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/domain/repositories/drone_osd_repository.dart';
|
||||
import '../network/mqtt/domain/repositories/task_message_repository.dart';
|
||||
import '../network/mqtt/data/repositories/drone_osd_repository_impl.dart';
|
||||
import '../network/mqtt/data/repositories/task_message_repository_impl.dart';
|
||||
import '../router/app_router.dart';
|
||||
import '../storage/impl/user_storage_impl.dart';
|
||||
import '../storage/user_storage.dart';
|
||||
@@ -148,7 +157,17 @@ Future<void> init() async {
|
||||
);
|
||||
sl.registerLazySingleton(() => PathPlanningService());
|
||||
|
||||
/// 1.1.3 NetMessageDispatcher:消息调度器,并将 TcpClient 注入给它
|
||||
/// 1.1.3 MQTT Clients - 两个独立的 MQTT 客户端实例
|
||||
sl.registerLazySingleton<MqttClient>(
|
||||
() => MqttClientImpl(),
|
||||
instanceName: 'droneOsdClient',
|
||||
);
|
||||
sl.registerLazySingleton<MqttClient>(
|
||||
() => MqttClientImpl(),
|
||||
instanceName: 'taskMessageClient',
|
||||
);
|
||||
|
||||
/// 1.1.4 NetMessageDispatcher:消息调度器,并将 TcpClient 注入给它
|
||||
sl.registerLazySingleton(
|
||||
() => NetMessageDispatcher(
|
||||
sl<TcpClient>(),
|
||||
@@ -172,6 +191,22 @@ Future<void> init() async {
|
||||
/// 1.3 Log工具Sentry
|
||||
sl.registerLazySingleton<ILoggerService>(() => SentryLoggerImpl());
|
||||
|
||||
/// 1.4 --- MQTT Data Sources ---
|
||||
sl.registerLazySingleton<DroneOsdDataSource>(
|
||||
() => DroneOsdDataSourceImpl(sl<MqttClient>(instanceName: 'droneOsdClient')),
|
||||
);
|
||||
sl.registerLazySingleton<TaskMessageDataSource>(
|
||||
() => TaskMessageDataSourceImpl(sl<MqttClient>(instanceName: 'taskMessageClient')),
|
||||
);
|
||||
|
||||
/// 1.5 --- MQTT Repositories ---
|
||||
sl.registerLazySingleton<DroneOsdRepository>(
|
||||
() => DroneOsdRepositoryImpl(sl<DroneOsdDataSource>()),
|
||||
);
|
||||
sl.registerLazySingleton<TaskMessageRepository>(
|
||||
() => TaskMessageRepositoryImpl(sl<TaskMessageDataSource>()),
|
||||
);
|
||||
|
||||
/// 2. 数据源 (DataSource)
|
||||
sl.registerLazySingleton<AuthHttpDataSource>(
|
||||
() => AuthHttpDataSourceImpl(sl()),
|
||||
@@ -326,6 +361,11 @@ Future<void> init() async {
|
||||
/// Robot List V2
|
||||
sl.registerFactory<RobotListBloc>(() => RobotListBloc(sl<Dio>()));
|
||||
|
||||
/// Device Realtime V2 (MQTT)
|
||||
sl.registerFactory<DeviceRealtimeBloc>(
|
||||
() => DeviceRealtimeBloc(sl<TaskMessageRepository>()),
|
||||
);
|
||||
|
||||
/// Alarm Center V2
|
||||
sl.registerLazySingleton<AlarmRemoteDataSource>(
|
||||
() => AlarmRemoteDataSourceImpl(),
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../domain/interfaces/mqtt_client.dart';
|
||||
import '../../domain/models/mqtt_message.dart';
|
||||
import '../../domain/entities/drone_osd_entity.dart';
|
||||
|
||||
abstract class DroneOsdDataSource {
|
||||
Stream<DroneOsdEntity> get droneOsdStream;
|
||||
Stream<DroneOsdEntity> get stationOsdStream;
|
||||
|
||||
Future<void> startListening({
|
||||
required String deviceSn,
|
||||
required String gatewaySn,
|
||||
});
|
||||
|
||||
Future<void> stopListening();
|
||||
}
|
||||
|
||||
class DroneOsdDataSourceImpl implements DroneOsdDataSource {
|
||||
final MqttClient mqttClient;
|
||||
final _droneOsdController = StreamController<DroneOsdEntity>.broadcast();
|
||||
final _stationOsdController = StreamController<DroneOsdEntity>.broadcast();
|
||||
|
||||
StreamSubscription<MqttMessage>? _subscription;
|
||||
String? _deviceSn;
|
||||
String? _gatewaySn;
|
||||
|
||||
DroneOsdDataSourceImpl(this.mqttClient);
|
||||
|
||||
@override
|
||||
Stream<DroneOsdEntity> get droneOsdStream => _droneOsdController.stream;
|
||||
|
||||
@override
|
||||
Stream<DroneOsdEntity> get stationOsdStream => _stationOsdController.stream;
|
||||
|
||||
@override
|
||||
Future<void> startListening({
|
||||
required String deviceSn,
|
||||
required String gatewaySn,
|
||||
}) async {
|
||||
_deviceSn = deviceSn;
|
||||
_gatewaySn = gatewaySn;
|
||||
|
||||
final droneTopic = 'thing/product/$deviceSn/osd';
|
||||
final stationTopic = 'thing/product/$gatewaySn/osd';
|
||||
|
||||
debugPrint('🛸 [DroneOsdDataSource] 开始监听:');
|
||||
debugPrint(' 无人机: $droneTopic');
|
||||
debugPrint(' 机场: $stationTopic');
|
||||
|
||||
await mqttClient.subscribe(droneTopic);
|
||||
await mqttClient.subscribe(stationTopic);
|
||||
|
||||
_subscription = mqttClient.messageStream?.listen((message) {
|
||||
_handleMessage(message);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopListening() async {
|
||||
await _subscription?.cancel();
|
||||
_subscription = null;
|
||||
|
||||
if (_deviceSn != null) {
|
||||
await mqttClient.unsubscribe('thing/product/$_deviceSn/osd');
|
||||
}
|
||||
if (_gatewaySn != null) {
|
||||
await mqttClient.unsubscribe('thing/product/$_gatewaySn/osd');
|
||||
}
|
||||
|
||||
_deviceSn = null;
|
||||
_gatewaySn = null;
|
||||
}
|
||||
|
||||
void _handleMessage(MqttMessage message) {
|
||||
try {
|
||||
final jsonData = jsonDecode(message.payload) as Map<String, dynamic>;
|
||||
final osdData = DroneOsdEntity.fromJson(jsonData);
|
||||
|
||||
if (message.topic.contains(_deviceSn ?? '')) {
|
||||
debugPrint('🛸 [DroneOsdDataSource] 无人机 OSD 更新');
|
||||
_droneOsdController.add(osdData);
|
||||
} else if (message.topic.contains(_gatewaySn ?? '')) {
|
||||
debugPrint('🏢 [DroneOsdDataSource] 机场 OSD 更新');
|
||||
_stationOsdController.add(osdData);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ [DroneOsdDataSource] 解析 OSD 数据失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
stopListening();
|
||||
_droneOsdController.close();
|
||||
_stationOsdController.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../domain/interfaces/mqtt_client.dart';
|
||||
import '../../domain/models/mqtt_message.dart';
|
||||
import '../../domain/entities/task_arrive_entity.dart';
|
||||
import '../../domain/entities/task_status_entity.dart';
|
||||
import '../../domain/entities/real_time_message_entity.dart';
|
||||
|
||||
abstract class TaskMessageDataSource {
|
||||
Stream<TaskStatusEntity> get taskStatusStream;
|
||||
Stream<TaskArriveEntity> get taskArriveStream;
|
||||
Stream<RealTimeMessageEntity> get realTimeMessageStream;
|
||||
|
||||
Future<void> startListening({required String deviceId});
|
||||
Future<void> stopListening();
|
||||
}
|
||||
|
||||
class TaskMessageDataSourceImpl implements TaskMessageDataSource {
|
||||
final MqttClient mqttClient;
|
||||
final _taskStatusController = StreamController<TaskStatusEntity>.broadcast();
|
||||
final _taskArriveController = StreamController<TaskArriveEntity>.broadcast();
|
||||
final _realTimeMessageController = StreamController<RealTimeMessageEntity>.broadcast();
|
||||
|
||||
StreamSubscription<MqttMessage>? _subscription;
|
||||
String? _deviceId;
|
||||
|
||||
TaskMessageDataSourceImpl(this.mqttClient);
|
||||
|
||||
@override
|
||||
Stream<TaskStatusEntity> get taskStatusStream => _taskStatusController.stream;
|
||||
|
||||
@override
|
||||
Stream<TaskArriveEntity> get taskArriveStream => _taskArriveController.stream;
|
||||
|
||||
@override
|
||||
Stream<RealTimeMessageEntity> get realTimeMessageStream =>
|
||||
_realTimeMessageController.stream;
|
||||
|
||||
@override
|
||||
Future<void> startListening({required String deviceId}) async {
|
||||
_deviceId = deviceId;
|
||||
|
||||
final taskStatusTopic = 'task/$deviceId/status';
|
||||
final taskArriveTopic = 'task/$deviceId/arrive';
|
||||
final realTimeTopic = 'device/$deviceId/realTimeMessage';
|
||||
|
||||
debugPrint('📋 [TaskMessageDataSource] 开始监听:');
|
||||
debugPrint(' 任务状态: $taskStatusTopic');
|
||||
debugPrint(' 到达通知: $taskArriveTopic');
|
||||
debugPrint(' 实时消息: $realTimeTopic');
|
||||
|
||||
await mqttClient.subscribe(taskStatusTopic);
|
||||
await mqttClient.subscribe(taskArriveTopic);
|
||||
await mqttClient.subscribe(realTimeTopic);
|
||||
|
||||
_subscription = mqttClient.messageStream?.listen((message) {
|
||||
_handleMessage(message);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopListening() async {
|
||||
await _subscription?.cancel();
|
||||
_subscription = null;
|
||||
|
||||
if (_deviceId != null) {
|
||||
await mqttClient.unsubscribe('task/$_deviceId/status');
|
||||
await mqttClient.unsubscribe('task/$_deviceId/arrive');
|
||||
await mqttClient.unsubscribe('device/$_deviceId/realTimeMessage');
|
||||
}
|
||||
|
||||
_deviceId = null;
|
||||
}
|
||||
|
||||
void _handleMessage(MqttMessage message) {
|
||||
try {
|
||||
final jsonData = jsonDecode(message.payload) as Map<String, dynamic>;
|
||||
|
||||
if (message.topic.contains('/status')) {
|
||||
final taskStatus = TaskStatusEntity.fromJson(jsonData);
|
||||
debugPrint('📋 [TaskMessageDataSource] 任务状态更新: ${taskStatus.status}');
|
||||
_taskStatusController.add(taskStatus);
|
||||
} else if (message.topic.contains('/arrive')) {
|
||||
final arriveInfo = TaskArriveEntity.fromJson(jsonData);
|
||||
debugPrint('📍 [TaskMessageDataSource] 到达通知: 任务${arriveInfo.taskId}');
|
||||
_taskArriveController.add(arriveInfo);
|
||||
} else if (message.topic.contains('/realTimeMessage')) {
|
||||
final realTimeMsg = RealTimeMessageEntity.fromJson(jsonData);
|
||||
debugPrint('💬 [TaskMessageDataSource] 实时消息 - 类型: ${realTimeMsg.type}, 数据点数: ${realTimeMsg.data.length}');
|
||||
_realTimeMessageController.add(realTimeMsg);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ [TaskMessageDataSource] 解析任务消息失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
stopListening();
|
||||
_taskStatusController.close();
|
||||
_taskArriveController.close();
|
||||
_realTimeMessageController.close();
|
||||
}
|
||||
}
|
||||
168
lib/core/network/mqtt/data/infrastructure/mqtt_client_impl.dart
Normal file
168
lib/core/network/mqtt/data/infrastructure/mqtt_client_impl.dart
Normal file
@@ -0,0 +1,168 @@
|
||||
import 'dart:async';
|
||||
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 '../../domain/interfaces/mqtt_client.dart';
|
||||
import '../../domain/models/mqtt_config.dart';
|
||||
import '../../domain/models/mqtt_message.dart';
|
||||
|
||||
class MqttClientImpl implements MqttClient {
|
||||
mqtt_server.MqttServerClient? _client;
|
||||
final _messageController = StreamController<MqttMessage>.broadcast();
|
||||
MqttConfig? _currentConfig;
|
||||
bool _isConnected = false;
|
||||
|
||||
@override
|
||||
Stream<MqttMessage>? get messageStream => _messageController.stream;
|
||||
|
||||
@override
|
||||
bool get isConnected => _isConnected;
|
||||
|
||||
@override
|
||||
Future<void> connect(MqttConfig config) async {
|
||||
if (_isConnected) {
|
||||
debugPrint('⚠️ [MqttClient] 已连接,先断开');
|
||||
await disconnect();
|
||||
}
|
||||
|
||||
try {
|
||||
_currentConfig = config;
|
||||
final clientId = '${config.clientId}_${DateTime.now().millisecondsSinceEpoch}';
|
||||
|
||||
switch (config.protocol) {
|
||||
case MqttProtocol.websocket:
|
||||
_client = mqtt_server.MqttServerClient.withPort(
|
||||
config.host,
|
||||
clientId,
|
||||
config.port,
|
||||
);
|
||||
_client!.useWebSocket = true;
|
||||
break;
|
||||
case MqttProtocol.wss:
|
||||
_client = mqtt_server.MqttServerClient.withPort(
|
||||
config.host,
|
||||
clientId,
|
||||
config.port,
|
||||
);
|
||||
_client!.useWebSocket = true;
|
||||
_client!.secure = true;
|
||||
break;
|
||||
case MqttProtocol.tcp:
|
||||
default:
|
||||
_client = mqtt_server.MqttServerClient.withPort(
|
||||
config.host,
|
||||
clientId,
|
||||
config.port,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
_client!.logging(on: false);
|
||||
_client!.keepAlivePeriod = config.keepAlivePeriod;
|
||||
_client!.autoReconnect = true;
|
||||
_client!.resubscribeOnAutoReconnect = true;
|
||||
_client!.onDisconnected = _onDisconnected;
|
||||
_client!.onConnected = _onConnected;
|
||||
_client!.onSubscribed = _onSubscribed;
|
||||
|
||||
final connMessage = mqtt.MqttConnectMessage()
|
||||
.withClientIdentifier(clientId)
|
||||
.startClean()
|
||||
.withWillQos(mqtt.MqttQos.atLeastOnce);
|
||||
|
||||
_client!.connectionMessage = connMessage;
|
||||
|
||||
debugPrint('🔌 [MqttClient] 开始连接到 ${config.host}:${config.port}');
|
||||
|
||||
await _client!.connect(config.username, config.password);
|
||||
|
||||
if (_client!.connectionStatus?.state == mqtt.MqttConnectionState.connected) {
|
||||
_isConnected = true;
|
||||
debugPrint('✅ [MqttClient] 连接成功');
|
||||
_listenToMessages();
|
||||
} else {
|
||||
throw Exception('连接失败: ${_client!.connectionStatus?.returnCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ [MqttClient] 连接异常: $e');
|
||||
_isConnected = false;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> disconnect() async {
|
||||
debugPrint('🔌 [MqttClient] 断开连接');
|
||||
_client?.disconnect();
|
||||
_isConnected = false;
|
||||
_currentConfig = null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> subscribe(String topic) async {
|
||||
if (!_isConnected || _client == null) {
|
||||
throw Exception('MQTT 未连接');
|
||||
}
|
||||
|
||||
debugPrint('📡 [MqttClient] 订阅主题: $topic');
|
||||
_client!.subscribe(topic, mqtt.MqttQos.atLeastOnce);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> unsubscribe(String topic) async {
|
||||
if (!_isConnected || _client == null) {
|
||||
throw Exception('MQTT 未连接');
|
||||
}
|
||||
|
||||
debugPrint('🔕 [MqttClient] 取消订阅: $topic');
|
||||
_client!.unsubscribe(topic);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> publish(String topic, String message) async {
|
||||
if (!_isConnected || _client == null) {
|
||||
throw Exception('MQTT 未连接');
|
||||
}
|
||||
|
||||
debugPrint('📤 [MqttClient] 发布消息到 $topic');
|
||||
final builder = mqtt.MqttClientPayloadBuilder();
|
||||
builder.addString(message);
|
||||
_client!.publishMessage(topic, mqtt.MqttQos.atLeastOnce, builder.payload!);
|
||||
}
|
||||
|
||||
void _listenToMessages() {
|
||||
_client!.updates!.listen((List<mqtt.MqttReceivedMessage<mqtt.MqttMessage>> messages) {
|
||||
for (final msg in messages) {
|
||||
final topic = msg.topic;
|
||||
final payload = mqtt.MqttPublishPayload.bytesToStringAsString((msg.payload as mqtt.MqttPublishMessage).payload.message);
|
||||
|
||||
debugPrint('📥 [MqttClient] 收到消息 [$topic]: $payload');
|
||||
|
||||
_messageController.add(MqttMessage(
|
||||
topic: topic,
|
||||
payload: payload,
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _onConnected() {
|
||||
debugPrint('✅ [MqttClient] 已连接');
|
||||
_isConnected = true;
|
||||
}
|
||||
|
||||
void _onDisconnected() {
|
||||
debugPrint('❌ [MqttClient] 已断开');
|
||||
_isConnected = false;
|
||||
}
|
||||
|
||||
void _onSubscribed(String topic) {
|
||||
debugPrint('✅ [MqttClient] 订阅成功: $topic');
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_messageController.close();
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'dart:async';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
|
||||
import '../../../../../core/error/failure.dart';
|
||||
import '../datasources/drone_osd_datasource.dart';
|
||||
import '../../domain/repositories/drone_osd_repository.dart';
|
||||
import '../../domain/entities/drone_osd_entity.dart';
|
||||
|
||||
class DroneOsdRepositoryImpl implements DroneOsdRepository {
|
||||
final DroneOsdDataSource dataSource;
|
||||
|
||||
DroneOsdRepositoryImpl(this.dataSource);
|
||||
|
||||
@override
|
||||
Stream<DroneOsdEntity> get droneOsdStream => dataSource.droneOsdStream;
|
||||
|
||||
@override
|
||||
Stream<DroneOsdEntity> get stationOsdStream => dataSource.stationOsdStream;
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> startListening({
|
||||
required String deviceSn,
|
||||
required String gatewaySn,
|
||||
}) async {
|
||||
try {
|
||||
await dataSource.startListening(
|
||||
deviceSn: deviceSn,
|
||||
gatewaySn: gatewaySn,
|
||||
);
|
||||
return right(null);
|
||||
} catch (e) {
|
||||
return left(Failure(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopListening() async {
|
||||
await dataSource.stopListening();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'dart:async';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
|
||||
import '../../../../../core/error/failure.dart';
|
||||
import '../datasources/task_message_datasource.dart';
|
||||
import '../../domain/repositories/task_message_repository.dart';
|
||||
import '../../domain/entities/task_arrive_entity.dart';
|
||||
import '../../domain/entities/task_status_entity.dart';
|
||||
import '../../domain/entities/real_time_message_entity.dart';
|
||||
|
||||
class TaskMessageRepositoryImpl implements TaskMessageRepository {
|
||||
final TaskMessageDataSource dataSource;
|
||||
|
||||
TaskMessageRepositoryImpl(this.dataSource);
|
||||
|
||||
@override
|
||||
Stream<RealTimeMessageEntity> get realTimeMessageStream =>
|
||||
dataSource.realTimeMessageStream;
|
||||
|
||||
@override
|
||||
Stream<TaskArriveEntity> get taskArriveStream => dataSource.taskArriveStream;
|
||||
|
||||
@override
|
||||
Stream<TaskStatusEntity> get taskStatusStream => dataSource.taskStatusStream;
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> startListening({required String deviceId}) async {
|
||||
try {
|
||||
await dataSource.startListening(deviceId: deviceId);
|
||||
return right(null);
|
||||
} catch (e) {
|
||||
return left(Failure(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopListening() async {
|
||||
await dataSource.stopListening();
|
||||
}
|
||||
}
|
||||
18
lib/core/network/mqtt/domain/entities/drone_osd_entity.dart
Normal file
18
lib/core/network/mqtt/domain/entities/drone_osd_entity.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class DroneOsdEntity extends Equatable {
|
||||
final Map<String, dynamic> rawData;
|
||||
|
||||
const DroneOsdEntity({required this.rawData});
|
||||
|
||||
factory DroneOsdEntity.fromJson(Map<String, dynamic> json) {
|
||||
return DroneOsdEntity(rawData: json);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return rawData;
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [rawData];
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class RealTimeMessageEntity extends Equatable {
|
||||
final List<DeviceDataPoint> data;
|
||||
final String type;
|
||||
|
||||
const RealTimeMessageEntity({
|
||||
required this.data,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
factory RealTimeMessageEntity.fromJson(Map<String, dynamic> json) {
|
||||
final dataList = json['data'] as List<dynamic>? ?? [];
|
||||
return RealTimeMessageEntity(
|
||||
data: dataList.map((item) => DeviceDataPoint.fromJson(item)).toList(),
|
||||
type: json['type'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
double? getValueByName(String name) {
|
||||
final point = data.firstWhere(
|
||||
(p) => p.name == name,
|
||||
orElse: () => DeviceDataPoint(name: '', value: '', unit: ''),
|
||||
);
|
||||
if (point.value.isEmpty) return null;
|
||||
return double.tryParse(point.value);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'data': data.map((e) => e.toJson()).toList(),
|
||||
'type': type,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [data, type];
|
||||
}
|
||||
|
||||
class DeviceDataPoint extends Equatable {
|
||||
final String name;
|
||||
final String value;
|
||||
final String unit;
|
||||
|
||||
const DeviceDataPoint({
|
||||
required this.name,
|
||||
required this.value,
|
||||
required this.unit,
|
||||
});
|
||||
|
||||
factory DeviceDataPoint.fromJson(Map<String, dynamic> json) {
|
||||
return DeviceDataPoint(
|
||||
name: json['name'] as String? ?? '',
|
||||
value: json['value'] as String? ?? '',
|
||||
unit: json['unit'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'value': value,
|
||||
'unit': unit,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [name, value, unit];
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class TaskArriveEntity extends Equatable {
|
||||
final String type;
|
||||
final String deviceId;
|
||||
final int taskId;
|
||||
final dynamic status;
|
||||
final ArriveLocation entity;
|
||||
|
||||
const TaskArriveEntity({
|
||||
required this.type,
|
||||
required this.deviceId,
|
||||
required this.taskId,
|
||||
this.status,
|
||||
required this.entity,
|
||||
});
|
||||
|
||||
factory TaskArriveEntity.fromJson(Map<String, dynamic> json) {
|
||||
return TaskArriveEntity(
|
||||
type: json['type'] as String? ?? '',
|
||||
deviceId: json['deviceId'] as String? ?? '',
|
||||
taskId: json['taskId'] as int? ?? 0,
|
||||
status: json['status'],
|
||||
entity: ArriveLocation.fromJson(json['entity'] as Map<String, dynamic>? ?? {}),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'type': type,
|
||||
'deviceId': deviceId,
|
||||
'taskId': taskId,
|
||||
'status': status,
|
||||
'entity': entity.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [type, deviceId, taskId, status, entity];
|
||||
}
|
||||
|
||||
class ArriveLocation extends Equatable {
|
||||
final double lat;
|
||||
final double lng;
|
||||
|
||||
const ArriveLocation({
|
||||
required this.lat,
|
||||
required this.lng,
|
||||
});
|
||||
|
||||
factory ArriveLocation.fromJson(Map<String, dynamic> json) {
|
||||
return ArriveLocation(
|
||||
lat: (json['lat'] as num?)?.toDouble() ?? 0.0,
|
||||
lng: (json['lng'] as num?)?.toDouble() ?? 0.0,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'lat': lat,
|
||||
'lng': lng,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [lat, lng];
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class TaskStatusEntity extends Equatable {
|
||||
final String type;
|
||||
final String deviceId;
|
||||
final int taskId;
|
||||
final String? status;
|
||||
final Map<String, dynamic>? extraData;
|
||||
|
||||
const TaskStatusEntity({
|
||||
required this.type,
|
||||
required this.deviceId,
|
||||
required this.taskId,
|
||||
this.status,
|
||||
this.extraData,
|
||||
});
|
||||
|
||||
factory TaskStatusEntity.fromJson(Map<String, dynamic> json) {
|
||||
return TaskStatusEntity(
|
||||
type: json['type'] as String? ?? '',
|
||||
deviceId: json['deviceId'] as String? ?? '',
|
||||
taskId: json['taskId'] as int? ?? 0,
|
||||
status: json['status'] as String?,
|
||||
extraData: json,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'type': type,
|
||||
'deviceId': deviceId,
|
||||
'taskId': taskId,
|
||||
'status': status,
|
||||
if (extraData != null) ...extraData!,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [type, deviceId, taskId, status, extraData];
|
||||
}
|
||||
14
lib/core/network/mqtt/domain/interfaces/mqtt_client.dart
Normal file
14
lib/core/network/mqtt/domain/interfaces/mqtt_client.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'dart:async';
|
||||
import '../models/mqtt_config.dart';
|
||||
import '../models/mqtt_message.dart';
|
||||
|
||||
abstract class MqttClient {
|
||||
Stream<MqttMessage>? get messageStream;
|
||||
|
||||
Future<void> connect(MqttConfig config);
|
||||
Future<void> disconnect();
|
||||
Future<void> subscribe(String topic);
|
||||
Future<void> unsubscribe(String topic);
|
||||
Future<void> publish(String topic, String message);
|
||||
bool get isConnected;
|
||||
}
|
||||
90
lib/core/network/mqtt/domain/models/mqtt_config.dart
Normal file
90
lib/core/network/mqtt/domain/models/mqtt_config.dart
Normal file
@@ -0,0 +1,90 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
enum MqttProtocol { tcp, websocket, wss }
|
||||
|
||||
class MqttConfig extends Equatable {
|
||||
final String host;
|
||||
final int port;
|
||||
final String username;
|
||||
final String password;
|
||||
final MqttProtocol protocol;
|
||||
final String clientId;
|
||||
final bool cleanSession;
|
||||
final int keepAlivePeriod;
|
||||
final int reconnectDelayMs;
|
||||
|
||||
const MqttConfig({
|
||||
required this.host,
|
||||
required this.port,
|
||||
required this.username,
|
||||
required this.password,
|
||||
required this.protocol,
|
||||
required this.clientId,
|
||||
this.cleanSession = true,
|
||||
this.keepAlivePeriod = 60,
|
||||
this.reconnectDelayMs = 3000,
|
||||
});
|
||||
|
||||
factory MqttConfig.droneOsd() {
|
||||
return const MqttConfig(
|
||||
host: '1.95.137.212',
|
||||
port: 8083,
|
||||
username: 'maibu',
|
||||
password: 'jsmbzn520',
|
||||
protocol: MqttProtocol.websocket,
|
||||
clientId: 'drone_osd_client',
|
||||
cleanSession: true,
|
||||
reconnectDelayMs: 3000,
|
||||
);
|
||||
}
|
||||
|
||||
factory MqttConfig.taskMessage() {
|
||||
return const MqttConfig(
|
||||
host: '1.95.137.212',
|
||||
port: 59020,
|
||||
username: 'maibu',
|
||||
password: 'jsmbzn520',
|
||||
protocol: MqttProtocol.tcp,
|
||||
clientId: 'task_message_client',
|
||||
cleanSession: true,
|
||||
reconnectDelayMs: 3000,
|
||||
);
|
||||
}
|
||||
|
||||
MqttConfig copyWith({
|
||||
String? host,
|
||||
int? port,
|
||||
String? username,
|
||||
String? password,
|
||||
MqttProtocol? protocol,
|
||||
String? clientId,
|
||||
bool? cleanSession,
|
||||
int? keepAlivePeriod,
|
||||
int? reconnectDelayMs,
|
||||
}) {
|
||||
return MqttConfig(
|
||||
host: host ?? this.host,
|
||||
port: port ?? this.port,
|
||||
username: username ?? this.username,
|
||||
password: password ?? this.password,
|
||||
protocol: protocol ?? this.protocol,
|
||||
clientId: clientId ?? this.clientId,
|
||||
cleanSession: cleanSession ?? this.cleanSession,
|
||||
keepAlivePeriod: keepAlivePeriod ?? this.keepAlivePeriod,
|
||||
reconnectDelayMs: reconnectDelayMs ?? this.reconnectDelayMs,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
host,
|
||||
port,
|
||||
username,
|
||||
password,
|
||||
protocol,
|
||||
clientId,
|
||||
cleanSession,
|
||||
keepAlivePeriod,
|
||||
reconnectDelayMs,
|
||||
];
|
||||
}
|
||||
24
lib/core/network/mqtt/domain/models/mqtt_message.dart
Normal file
24
lib/core/network/mqtt/domain/models/mqtt_message.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class MqttMessage extends Equatable {
|
||||
final String topic;
|
||||
final String payload;
|
||||
final DateTime timestamp;
|
||||
|
||||
MqttMessage({
|
||||
required this.topic,
|
||||
required this.payload,
|
||||
DateTime? timestamp,
|
||||
}) : timestamp = timestamp ?? DateTime.now();
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'topic': topic,
|
||||
'payload': payload,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [topic, payload, timestamp];
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'dart:async';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
|
||||
import '../../../../../core/error/failure.dart';
|
||||
import '../entities/drone_osd_entity.dart';
|
||||
|
||||
abstract class DroneOsdRepository {
|
||||
Stream<DroneOsdEntity> get droneOsdStream;
|
||||
Stream<DroneOsdEntity> get stationOsdStream;
|
||||
|
||||
Future<Either<Failure, void>> startListening({
|
||||
required String deviceSn,
|
||||
required String gatewaySn,
|
||||
});
|
||||
|
||||
Future<void> stopListening();
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'dart:async';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
|
||||
import '../../../../../core/error/failure.dart';
|
||||
import '../entities/task_arrive_entity.dart';
|
||||
import '../entities/task_status_entity.dart';
|
||||
import '../entities/real_time_message_entity.dart';
|
||||
|
||||
abstract class TaskMessageRepository {
|
||||
Stream<RealTimeMessageEntity> get realTimeMessageStream;
|
||||
Stream<TaskArriveEntity> get taskArriveStream;
|
||||
Stream<TaskStatusEntity> get taskStatusStream;
|
||||
|
||||
Future<Either<Failure, void>> startListening({required String deviceId});
|
||||
Future<void> stopListening();
|
||||
}
|
||||
54
lib/core/network/mqtt/mqtt_manager.dart
Normal file
54
lib/core/network/mqtt/mqtt_manager.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
import '../../di/injection.dart';
|
||||
import 'data/datasources/drone_osd_datasource.dart';
|
||||
import 'data/datasources/task_message_datasource.dart';
|
||||
import 'domain/interfaces/mqtt_client.dart';
|
||||
import 'domain/models/mqtt_config.dart';
|
||||
|
||||
class MqttManager {
|
||||
static final MqttManager _instance = MqttManager._internal();
|
||||
factory MqttManager() => _instance;
|
||||
MqttManager._internal();
|
||||
|
||||
bool _isInitialized = false;
|
||||
|
||||
Future<void> initialize() async {
|
||||
if (_isInitialized) {
|
||||
debugPrint('⚠️ [MqttManager] 已初始化,跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
debugPrint('🔧 [MqttManager] 开始初始化 MQTT 连接...');
|
||||
|
||||
final droneOsdClient = sl<MqttClient>(instanceName: 'droneOsdClient');
|
||||
final taskMessageClient = sl<MqttClient>(instanceName: 'taskMessageClient');
|
||||
|
||||
await droneOsdClient.connect(MqttConfig.droneOsd());
|
||||
await taskMessageClient.connect(MqttConfig.taskMessage());
|
||||
|
||||
_isInitialized = true;
|
||||
debugPrint('✅ [MqttManager] MQTT 初始化完成');
|
||||
} catch (e) {
|
||||
debugPrint('❌ [MqttManager] MQTT 初始化失败: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
debugPrint('🔌 [MqttManager] 断开所有 MQTT 连接...');
|
||||
|
||||
final droneOsdClient = sl<MqttClient>(instanceName: 'droneOsdClient');
|
||||
final taskMessageClient = sl<MqttClient>(instanceName: 'taskMessageClient');
|
||||
|
||||
await droneOsdClient.disconnect();
|
||||
await taskMessageClient.disconnect();
|
||||
|
||||
_isInitialized = false;
|
||||
debugPrint('✅ [MqttManager] 所有 MQTT 连接已断开');
|
||||
}
|
||||
|
||||
bool get isInitialized => _isInitialized;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import '../../../../core/app/app_user_cubit.dart';
|
||||
import '../../../../core/logging/i_logger_service.dart';
|
||||
import '../../../../core/network/error_handler.dart';
|
||||
import '../../../../features/v2/site/presentation/cubit/site_cubit.dart';
|
||||
import '../../domain/entities/device_task_entity.dart'; // 🔥 新增
|
||||
import '../../domain/usecases/cancel_task_usecase.dart';
|
||||
import '../../domain/usecases/get_device_task_pool_usecase.dart';
|
||||
import '../../domain/usecases/pause_task_usecase.dart';
|
||||
@@ -70,23 +71,32 @@ class DeviceTaskCubit extends Cubit<DeviceTaskState> {
|
||||
));
|
||||
},
|
||||
(taskList) {
|
||||
// 过滤出当前设备的任务
|
||||
final deviceTasks = taskList
|
||||
.where((task) => task.deviceId == deviceId)
|
||||
.toList();
|
||||
|
||||
// 取第一个任务(或根据业务逻辑选择)
|
||||
final currentTask = deviceTasks.isNotEmpty ? deviceTasks.first : null;
|
||||
// 🔥 过滤出当前设备 + 活跃状态的任务(NEW, EXECUTING, PAUSE)
|
||||
final activeTasks = taskList.where((task) {
|
||||
// 1. 设备号匹配
|
||||
if (task.deviceId != deviceId) return false;
|
||||
|
||||
// 2. 状态过滤:只保留新建、执行中、暂停中的任务
|
||||
final status = task.taskStatus;
|
||||
return status == 'NEW' || // 新建
|
||||
status == 'EXECUTING' || // 执行中
|
||||
status == 'PAUSE'; // 暂停中
|
||||
}).toList();
|
||||
|
||||
_logger.logWithLevel(
|
||||
'✅ 找到 ${deviceTasks.length} 个任务,当前任务ID: ${currentTask?.id}',
|
||||
'✅ 找到 ${activeTasks.length} 个活跃任务',
|
||||
);
|
||||
|
||||
// 🔥 如果有多个任务,保存所有选项供用户选择
|
||||
// 如果只有1个,直接选中
|
||||
final currentTask = activeTasks.isNotEmpty ? activeTasks.first : null;
|
||||
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
taskPool: taskList,
|
||||
currentTask: currentTask,
|
||||
currentTaskId: currentTask?.id,
|
||||
activeTasks: activeTasks, // 🔥 保存所有活跃任务列表
|
||||
));
|
||||
},
|
||||
);
|
||||
@@ -304,6 +314,15 @@ class DeviceTaskCubit extends Cubit<DeviceTaskState> {
|
||||
_logger.logWithLevel('🔄 更新当前任务ID: $taskId');
|
||||
}
|
||||
|
||||
/// 🔥 手动选择任务(用户从弹窗中选择)
|
||||
void selectTask(DeviceTaskEntity task) {
|
||||
_logger.logWithLevel('✅ 用户选择任务ID: ${task.id}, 状态: ${task.taskStatus}');
|
||||
emit(state.copyWith(
|
||||
currentTask: task,
|
||||
currentTaskId: task.id,
|
||||
));
|
||||
}
|
||||
|
||||
/// 清除当前任务
|
||||
void clearCurrentTask() {
|
||||
emit(state.copyWith(
|
||||
|
||||
@@ -12,6 +12,7 @@ class DeviceTaskState extends Equatable {
|
||||
final List<DeviceTaskEntity> taskPool;
|
||||
final DeviceTaskEntity? currentTask;
|
||||
final int? currentTaskId;
|
||||
final List<DeviceTaskEntity> activeTasks; // 🔥 活跃任务列表(供用户选择)
|
||||
final bool isLoading;
|
||||
final String? errorMessage;
|
||||
final DeviceTaskOperationType operationType;
|
||||
@@ -21,6 +22,7 @@ class DeviceTaskState extends Equatable {
|
||||
this.taskPool = const [],
|
||||
this.currentTask,
|
||||
this.currentTaskId,
|
||||
this.activeTasks = const [], // 🔥 默认空列表
|
||||
this.isLoading = false,
|
||||
this.errorMessage,
|
||||
this.operationType = DeviceTaskOperationType.none,
|
||||
@@ -31,6 +33,7 @@ class DeviceTaskState extends Equatable {
|
||||
List<DeviceTaskEntity>? taskPool,
|
||||
DeviceTaskEntity? currentTask,
|
||||
int? currentTaskId,
|
||||
List<DeviceTaskEntity>? activeTasks, // 🔥 新增
|
||||
bool? isLoading,
|
||||
String? errorMessage,
|
||||
DeviceTaskOperationType? operationType,
|
||||
@@ -40,6 +43,7 @@ class DeviceTaskState extends Equatable {
|
||||
taskPool: taskPool ?? this.taskPool,
|
||||
currentTask: currentTask ?? this.currentTask,
|
||||
currentTaskId: currentTaskId ?? this.currentTaskId,
|
||||
activeTasks: activeTasks ?? this.activeTasks, // 🔥 新增
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
errorMessage: errorMessage,
|
||||
operationType: operationType ?? this.operationType,
|
||||
@@ -52,6 +56,7 @@ class DeviceTaskState extends Equatable {
|
||||
taskPool,
|
||||
currentTask,
|
||||
currentTaskId,
|
||||
activeTasks, // 🔥 新增
|
||||
isLoading,
|
||||
errorMessage,
|
||||
operationType,
|
||||
|
||||
@@ -13,6 +13,7 @@ import '../../../../core/app/app_user_cubit.dart';
|
||||
import '../../../../core/router/route_paths.dart';
|
||||
import '../../../devices/presentation/bloc/devices_cubit.dart';
|
||||
import '../../../devices/presentation/bloc/devices_state.dart';
|
||||
import '../../../remote_control/presentation/bloc/remote_control_cubit.dart'; // 🔥 添加 RemoteControlCubit
|
||||
// 导入你的主题文件以获取 offWhite
|
||||
// import 'package:maibu_satabot_v2/core/theme/app_theme.dart';
|
||||
|
||||
@@ -126,11 +127,18 @@ class ImmersionHeader extends StatelessWidget {
|
||||
children: [
|
||||
_buildCircleIcon(Icons.sync, () {
|
||||
// 1. 触发 Cubit 请求最新设备列表
|
||||
// 假设你的 username 存储在 AuthCubit 或类似的全局状态中
|
||||
final username =
|
||||
context.read<AppUserCubit>().state.user?.username ??
|
||||
"";
|
||||
context.read<DevicesCubit>().fetchAllDevices(username);
|
||||
|
||||
// 🔥 修复:检查是否已有 targetDevice
|
||||
final remoteControlState = context.read<RemoteControlCubit>().state;
|
||||
if (remoteControlState.targetDevice == null) {
|
||||
debugPrint('🔄 [ImmersionHeader] 无 targetDevice,开始加载设备列表');
|
||||
context.read<DevicesCubit>().fetchAllDevices(username);
|
||||
} else {
|
||||
debugPrint('✅ [ImmersionHeader] 已有 targetDevice: ${remoteControlState.targetDevice!.deviceName},跳过加载');
|
||||
}
|
||||
|
||||
// 2. 弹出窗口(窗口内部会根据状态显示转圈或列表)
|
||||
_showDeviceSwitcher(context);
|
||||
|
||||
@@ -36,6 +36,9 @@ import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_statu
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_state.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_state.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/entities/device_task_entity.dart'; // 🔥 新增
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_task_cubit.dart'; // 🔥 新增
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_task_state.dart'; // 🔥 新增
|
||||
import 'package:maibu_satabot_v2/features/v2/site/presentation/cubit/site_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/widgets/BottomDirectionLine.dart';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/widgets/common/commonFn.dart';
|
||||
@@ -1978,6 +1981,102 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 🔥 显示任务选择弹窗
|
||||
Future<DeviceTaskEntity?> _showTaskSelectionDialog(
|
||||
List<DeviceTaskEntity> tasks,
|
||||
) async {
|
||||
return showDialog<DeviceTaskEntity>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('选择任务'),
|
||||
content: SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: tasks.length,
|
||||
itemBuilder: (context, index) {
|
||||
final task = tasks[index];
|
||||
// 状态标签颜色
|
||||
Color statusColor;
|
||||
String statusText;
|
||||
switch (task.taskStatus) {
|
||||
case 'NEW':
|
||||
statusColor = Colors.blue;
|
||||
statusText = '新建';
|
||||
break;
|
||||
case 'EXECUTING':
|
||||
statusColor = Colors.green;
|
||||
statusText = '执行中';
|
||||
break;
|
||||
case 'PAUSE':
|
||||
statusColor = Colors.orange;
|
||||
statusText = '暂停中';
|
||||
break;
|
||||
default:
|
||||
statusColor = Colors.grey;
|
||||
statusText = task.taskStatusTranslate;
|
||||
}
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: ListTile(
|
||||
title: Text('任务 #${task.id}'),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('设备号: ${task.deviceId}'),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
color: statusColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (task.createTime != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
task.createTime!,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(dialogContext, task);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext, null),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 开始作业
|
||||
void _startWork() async {
|
||||
// 🔥 V2 适配:使用接口方式执行作业,不再使用 TCP
|
||||
@@ -2010,13 +2109,53 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. 打印请求参数日志
|
||||
// 🔥 5. 先过滤出活跃任务,让用户选择
|
||||
debugPrint('🔍 [开始作业] 正在查询活跃任务...');
|
||||
final taskCubit = sl<DeviceTaskCubit>();
|
||||
await taskCubit.fetchAndFilterTask(deviceId);
|
||||
|
||||
// 等待一下让状态更新
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
final activeTasks = taskCubit.state.activeTasks;
|
||||
debugPrint('✅ [开始作业] 找到 ${activeTasks.length} 个活跃任务');
|
||||
|
||||
if (activeTasks.isEmpty) {
|
||||
_showPageToast(
|
||||
message: "当前设备没有活跃任务(新建/执行中/暂停中)",
|
||||
type: ToastType.warn,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔥 6. 如果有多条任务,显示选择弹窗
|
||||
DeviceTaskEntity? selectedTask;
|
||||
if (activeTasks.length > 1) {
|
||||
debugPrint('⚠️ [开始作业] 有多个活跃任务,显示选择弹窗');
|
||||
selectedTask = await _showTaskSelectionDialog(activeTasks);
|
||||
if (selectedTask == null) {
|
||||
// 用户取消选择
|
||||
debugPrint('❌ [开始作业] 用户取消选择任务');
|
||||
return;
|
||||
}
|
||||
// 用户选择了任务,更新 cubit
|
||||
taskCubit.selectTask(selectedTask);
|
||||
} else {
|
||||
// 只有1条任务,直接使用
|
||||
selectedTask = activeTasks.first;
|
||||
debugPrint('✅ [开始作业] 自动选择唯一任务 #${selectedTask.id}');
|
||||
}
|
||||
|
||||
debugPrint('📋 [开始作业] 最终选择的任务ID: ${selectedTask.id}, 状态: ${selectedTask.taskStatus}');
|
||||
|
||||
// 7. 打印请求参数日志
|
||||
debugPrint('🚀 [开始作业] 请求参数:');
|
||||
debugPrint(' ├─ deviceId: $deviceId');
|
||||
debugPrint(' ├─ routeId: $routeId');
|
||||
debugPrint(' ├─ taskId: ${selectedTask.id}');
|
||||
debugPrint(' └─ siteId: $siteId');
|
||||
|
||||
// 6. 更新UI状态
|
||||
// 8. 更新UI状态
|
||||
setState(() {
|
||||
isStopWork = false;
|
||||
isStartWork = true;
|
||||
@@ -2026,11 +2165,11 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
gctracePoint?.clear();
|
||||
});
|
||||
|
||||
// 7. 更新应用状态
|
||||
// 9. 更新应用状态
|
||||
context.read<DevicesCubit>().updateAppState(AppState.routePlanning);
|
||||
|
||||
try {
|
||||
// 8. 调用接口创建设备任务
|
||||
// 10. 调用接口创建设备任务
|
||||
final result = await sl<CreateDeviceTaskUseCase>().execute(
|
||||
deviceId: deviceId,
|
||||
routeId: routeId,
|
||||
@@ -2107,18 +2246,63 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
|
||||
/// 暂停作业
|
||||
void _pauseWork() async {
|
||||
// 🔥 获取设备ID和taskId
|
||||
final targetDevice = context.read<RemoteControlCubit>().state.targetDevice;
|
||||
final deviceId = targetDevice?.deviceName;
|
||||
if (deviceId == null || deviceId.isEmpty) {
|
||||
_showPageToast(message: "请先选择一个设备", type: ToastType.info);
|
||||
return;
|
||||
}
|
||||
|
||||
final taskCubit = sl<DeviceTaskCubit>();
|
||||
final taskId = taskCubit.state.currentTaskId;
|
||||
if (taskId == null) {
|
||||
_showPageToast(message: "无可用任务", type: ToastType.warn);
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('⏸️ [暂停作业] deviceId: $deviceId, taskId: $taskId');
|
||||
|
||||
setState(() {
|
||||
isStartWork = false; // 🔥 关键:停止作业标志
|
||||
_workStatus = WorkStatus.paused;
|
||||
});
|
||||
_saveDataToLocal(); // 🔥 保存暂停状态
|
||||
|
||||
// 🔥 调用接口暂停任务(注释掉原有的 TCP 方式)
|
||||
try {
|
||||
await taskCubit.pauseTask(deviceId);
|
||||
_showPageToast(message: "作业已暂停", type: ToastType.success);
|
||||
} catch (e) {
|
||||
debugPrint('❌ [暂停作业] 异常: $e');
|
||||
_showPageToast(message: "暂停失败: $e", type: ToastType.error);
|
||||
}
|
||||
|
||||
// 原有的 TCP 方式(已注释)
|
||||
//context.read<DevicesCubit>().updateAppState(AppState.none);
|
||||
await context.read<DevicesCubit>().pauseRoutePlanning();
|
||||
//await context.read<DevicesCubit>().pauseRoutePlanning();
|
||||
}
|
||||
|
||||
void _stopWork() async {
|
||||
_logger.log('按下停止按钮');
|
||||
|
||||
// 🔥 获取设备ID和taskId
|
||||
final targetDevice = context.read<RemoteControlCubit>().state.targetDevice;
|
||||
final deviceId = targetDevice?.deviceName;
|
||||
if (deviceId == null || deviceId.isEmpty) {
|
||||
_showPageToast(message: "请先选择一个设备", type: ToastType.info);
|
||||
return;
|
||||
}
|
||||
|
||||
final taskCubit = sl<DeviceTaskCubit>();
|
||||
final taskId = taskCubit.state.currentTaskId;
|
||||
if (taskId == null) {
|
||||
_showPageToast(message: "无可用任务", type: ToastType.warn);
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('⏹️ [停止作业] deviceId: $deviceId, taskId: $taskId');
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isStartWork = false;
|
||||
@@ -2133,7 +2317,18 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
});
|
||||
}
|
||||
|
||||
await context.read<DevicesCubit>().stopRoutePlanning();
|
||||
// 🔥 调用接口取消任务(注释掉原有的 TCP 方式)
|
||||
try {
|
||||
await taskCubit.cancelTask(deviceId);
|
||||
_showPageToast(message: "作业已停止", type: ToastType.success);
|
||||
} catch (e) {
|
||||
debugPrint('❌ [停止作业] 异常: $e');
|
||||
_showPageToast(message: "停止失败: $e", type: ToastType.error);
|
||||
}
|
||||
|
||||
// 原有的 TCP 方式(已注释)
|
||||
//await context.read<DevicesCubit>().stopRoutePlanning();
|
||||
|
||||
_saveDataToLocal();
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
if (mounted) {
|
||||
@@ -2152,12 +2347,40 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
|
||||
/// 继续作业
|
||||
void _resumeWork() async {
|
||||
// 🔥 获取设备ID和taskId
|
||||
final targetDevice = context.read<RemoteControlCubit>().state.targetDevice;
|
||||
final deviceId = targetDevice?.deviceName;
|
||||
if (deviceId == null || deviceId.isEmpty) {
|
||||
_showPageToast(message: "请先选择一个设备", type: ToastType.info);
|
||||
return;
|
||||
}
|
||||
|
||||
final taskCubit = sl<DeviceTaskCubit>();
|
||||
final taskId = taskCubit.state.currentTaskId;
|
||||
if (taskId == null) {
|
||||
_showPageToast(message: "无可用任务", type: ToastType.warn);
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('▶️ [继续作业] deviceId: $deviceId, taskId: $taskId');
|
||||
|
||||
setState(() {
|
||||
_workStatus = WorkStatus.working;
|
||||
});
|
||||
_saveDataToLocal(); // 🔥 保存暂停状态
|
||||
context.read<DevicesCubit>().updateAppState(AppState.routePlanning);
|
||||
await context.read<DevicesCubit>().resumeRoutePlanning();
|
||||
|
||||
// 🔥 调用接口恢复任务(注释掉原有的 TCP 方式)
|
||||
try {
|
||||
await taskCubit.recoveryTask(deviceId);
|
||||
_showPageToast(message: "作业已恢复", type: ToastType.success);
|
||||
} catch (e) {
|
||||
debugPrint('❌ [继续作业] 异常: $e');
|
||||
_showPageToast(message: "恢复失败: $e", type: ToastType.error);
|
||||
}
|
||||
|
||||
// 原有的 TCP 方式(已注释)
|
||||
//context.read<DevicesCubit>().updateAppState(AppState.routePlanning);
|
||||
//await context.read<DevicesCubit>().resumeRoutePlanning();
|
||||
}
|
||||
|
||||
void _showDeleteConfirmDialog(PlotData plot, Function(PlotData) onDelete) {
|
||||
@@ -2284,7 +2507,147 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 2. 错误提示(如果加载失败)
|
||||
// 🔥 3. 当前任务信息显示(可点击重新选择)
|
||||
BlocBuilder<DeviceTaskCubit, DeviceTaskState>(
|
||||
builder: (context, taskState) {
|
||||
final currentTask = taskState.currentTask;
|
||||
if (currentTask == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
// 状态标签颜色
|
||||
Color statusColor;
|
||||
String statusText;
|
||||
switch (currentTask.taskStatus) {
|
||||
case 'NEW':
|
||||
statusColor = Colors.blue;
|
||||
statusText = '新建';
|
||||
break;
|
||||
case 'EXECUTING':
|
||||
statusColor = Colors.green;
|
||||
statusText = '执行中';
|
||||
break;
|
||||
case 'PAUSE':
|
||||
statusColor = Colors.orange;
|
||||
statusText = '暂停中';
|
||||
break;
|
||||
default:
|
||||
statusColor = Colors.grey;
|
||||
statusText = currentTask.taskStatusTranslate;
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
// 🔥 点击后显示所有活跃任务供用户选择
|
||||
final activeTasks = taskState.activeTasks;
|
||||
if (activeTasks.isEmpty) {
|
||||
_showPageToast(
|
||||
message: "没有可用的任务",
|
||||
type: ToastType.warn,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final selectedTask = await _showTaskSelectionDialog(
|
||||
activeTasks,
|
||||
);
|
||||
if (selectedTask != null) {
|
||||
// 用户选择了新任务,更新 cubit
|
||||
sl<DeviceTaskCubit>().selectTask(selectedTask);
|
||||
_showPageToast(
|
||||
message: "已切换到任务 #${selectedTask.id}",
|
||||
type: ToastType.success,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F7FA),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: const Color(0xFF165DFF).withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.assignment_outlined,
|
||||
color: const Color(0xFF165DFF),
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'当前任务: #${currentTask.id}',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'设备: ${currentTask.deviceId}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF4E5969),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(
|
||||
4,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
color: statusColor,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.swap_horiz,
|
||||
color: const Color(0xFF165DFF),
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Text(
|
||||
'切换',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF165DFF),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 4. 错误提示(如果加载失败)
|
||||
if (startWorkList.isEmpty || headingStatus == 0)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_cubi
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_state.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_bloc.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_state.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/presentation/bloc/remote_control_cubit.dart'; // 🔥 添加 RemoteControlCubit
|
||||
|
||||
class MachineDetailsPage extends StatefulWidget {
|
||||
final DeviceEntity? device;
|
||||
@@ -157,7 +158,15 @@ class _MachineDetailsPageState extends State<MachineDetailsPage> {
|
||||
Navigator.pop(context);
|
||||
if (context.mounted) {
|
||||
final username = context.read<AppUserCubit>().state.user?.username ?? "";
|
||||
context.read<DevicesCubit>().fetchAllDevices(username);
|
||||
|
||||
// 🔥 修复:检查是否已有 targetDevice
|
||||
final remoteControlState = context.read<RemoteControlCubit>().state;
|
||||
if (remoteControlState.targetDevice == null) {
|
||||
debugPrint('🔄 [MachineDetails] 无 targetDevice,开始加载设备列表');
|
||||
context.read<DevicesCubit>().fetchAllDevices(username);
|
||||
} else {
|
||||
debugPrint('✅ [MachineDetails] 已有 targetDevice: ${remoteControlState.targetDevice!.deviceName},跳过加载');
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -187,7 +196,14 @@ class _MachineDetailsPageState extends State<MachineDetailsPage> {
|
||||
Navigator.pop(context);
|
||||
if (context.mounted) {
|
||||
final username = context.read<AppUserCubit>().state.user?.username ?? "";
|
||||
context.read<DevicesCubit>().fetchAllDevices(username);
|
||||
// 🔥 修复:检查是否已有 targetDevice
|
||||
final remoteControlState = context.read<RemoteControlCubit>().state;
|
||||
if (remoteControlState.targetDevice == null) {
|
||||
debugPrint('🔄 [MachineDetails AppBar] 无 targetDevice,开始加载设备列表');
|
||||
context.read<DevicesCubit>().fetchAllDevices(username);
|
||||
} else {
|
||||
debugPrint('✅ [MachineDetails AppBar] 已有 targetDevice: ${remoteControlState.targetDevice!.deviceName},跳过加载');
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -612,7 +628,15 @@ class _MachineDetailsPageState extends State<MachineDetailsPage> {
|
||||
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context).translate('machine_details.unbind_success').replaceAll('%s', _deviceName)), backgroundColor: Colors.green, duration: const Duration(seconds: 2)));
|
||||
|
||||
final username = context.read<AppUserCubit>().state.user?.username ?? "";
|
||||
context.read<DevicesCubit>().fetchAllDevices(username);
|
||||
|
||||
// 🔥 修复:检查是否已有 targetDevice
|
||||
final remoteControlState = context.read<RemoteControlCubit>().state;
|
||||
if (remoteControlState.targetDevice == null) {
|
||||
debugPrint('🔄 [MachineDetails Unbind] 无 targetDevice,开始加载设备列表');
|
||||
context.read<DevicesCubit>().fetchAllDevices(username);
|
||||
} else {
|
||||
debugPrint('✅ [MachineDetails Unbind] 已有 targetDevice: ${remoteControlState.targetDevice!.deviceName},跳过加载');
|
||||
}
|
||||
}
|
||||
} else if (state.errorMessage?.isNotEmpty == true && !state.isLoading) {
|
||||
if (context.mounted) {
|
||||
@@ -636,9 +660,9 @@ class _MachineDetailsPageState extends State<MachineDetailsPage> {
|
||||
}
|
||||
|
||||
// 处理结果:放宽条件,只要无错误信息就认为成功(不管loading)
|
||||
print(
|
||||
"处理修改名称结果,operationType: ${state.operationType}, isLoading: ${state.isLoading}, errorMessage: ${state.errorMessage},state.errorMessage?.isEmpty: ${state.errorMessage?.isEmpty}",
|
||||
);
|
||||
// print(
|
||||
// "处理修改名称结果,operationType: ${state.operationType}, isLoading: ${state.isLoading}, errorMessage: ${state.errorMessage},state.errorMessage?.isEmpty: ${state.errorMessage?.isEmpty}",
|
||||
// );
|
||||
|
||||
// 核心修改:去掉!state.isLoading的判断
|
||||
if (state.errorMessage?.isEmpty == true) {
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../../../../../core/network/mqtt/domain/repositories/task_message_repository.dart';
|
||||
import 'device_realtime_event.dart';
|
||||
import 'device_realtime_state.dart';
|
||||
|
||||
class DeviceRealtimeBloc extends Bloc<DeviceRealtimeEvent, DeviceRealtimeState> {
|
||||
final TaskMessageRepository _taskMessageRepository;
|
||||
StreamSubscription? _realtimeSubscription;
|
||||
String? _currentDeviceId;
|
||||
|
||||
DeviceRealtimeBloc(this._taskMessageRepository) : super(const DeviceRealtimeInitial()) {
|
||||
on<DeviceRealtimeStartListen>(_onStartListen);
|
||||
on<DeviceRealtimeStopListen>(_onStopListen);
|
||||
on<DeviceRealtimeDataUpdated>(_onDataUpdated);
|
||||
}
|
||||
|
||||
Future<void> _onStartListen(
|
||||
DeviceRealtimeStartListen event,
|
||||
Emitter<DeviceRealtimeState> emit,
|
||||
) async {
|
||||
if (_currentDeviceId == event.deviceId && state is DeviceRealtimeLoaded) {
|
||||
debugPrint('⚠️ [DeviceRealtimeBloc] 已在监听该设备,跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
emit(const DeviceRealtimeLoading());
|
||||
|
||||
final result = await _taskMessageRepository.startListening(
|
||||
deviceId: event.deviceId,
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
emit(DeviceRealtimeError(failure.message));
|
||||
},
|
||||
(_) {
|
||||
_currentDeviceId = event.deviceId;
|
||||
|
||||
_realtimeSubscription?.cancel();
|
||||
_realtimeSubscription = _taskMessageRepository.realTimeMessageStream.listen(
|
||||
(message) {
|
||||
add(DeviceRealtimeDataUpdated(message.toJson()));
|
||||
},
|
||||
onError: (error) {
|
||||
debugPrint('❌ [DeviceRealtimeBloc] 实时数据流错误: $error');
|
||||
},
|
||||
);
|
||||
|
||||
debugPrint('✅ [DeviceRealtimeBloc] 开始监听设备: ${event.deviceId}');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _onStopListen(
|
||||
DeviceRealtimeStopListen event,
|
||||
Emitter<DeviceRealtimeState> emit,
|
||||
) {
|
||||
_realtimeSubscription?.cancel();
|
||||
_realtimeSubscription = null;
|
||||
_taskMessageRepository.stopListening();
|
||||
_currentDeviceId = null;
|
||||
|
||||
debugPrint('🔕 [DeviceRealtimeBloc] 停止监听');
|
||||
emit(const DeviceRealtimeInitial());
|
||||
}
|
||||
|
||||
void _onDataUpdated(
|
||||
DeviceRealtimeDataUpdated event,
|
||||
Emitter<DeviceRealtimeState> emit,
|
||||
) {
|
||||
final newState = DeviceRealtimeLoaded.fromRealtimeMessage(event.realtimeData);
|
||||
emit(newState);
|
||||
|
||||
debugPrint('📊 [DeviceRealtimeBloc] 数据更新 - 电压: ${newState.voltage}V, 电量: ${newState.battery}%');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_realtimeSubscription?.cancel();
|
||||
_taskMessageRepository.stopListening();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
abstract class DeviceRealtimeEvent extends Equatable {
|
||||
const DeviceRealtimeEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class DeviceRealtimeStartListen extends DeviceRealtimeEvent {
|
||||
final String deviceId;
|
||||
|
||||
const DeviceRealtimeStartListen(this.deviceId);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [deviceId];
|
||||
}
|
||||
|
||||
class DeviceRealtimeStopListen extends DeviceRealtimeEvent {
|
||||
const DeviceRealtimeStopListen();
|
||||
}
|
||||
|
||||
class DeviceRealtimeDataUpdated extends DeviceRealtimeEvent {
|
||||
final Map<String, dynamic> realtimeData;
|
||||
|
||||
const DeviceRealtimeDataUpdated(this.realtimeData);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [realtimeData];
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
abstract class DeviceRealtimeState extends Equatable {
|
||||
const DeviceRealtimeState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class DeviceRealtimeInitial extends DeviceRealtimeState {
|
||||
const DeviceRealtimeInitial();
|
||||
}
|
||||
|
||||
class DeviceRealtimeLoading extends DeviceRealtimeState {
|
||||
const DeviceRealtimeLoading();
|
||||
}
|
||||
|
||||
class DeviceRealtimeLoaded extends DeviceRealtimeState {
|
||||
final Map<String, dynamic> realtimeData;
|
||||
final double? voltage;
|
||||
final double? battery;
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
final double? yaw;
|
||||
final double? pitch;
|
||||
final double? roll;
|
||||
final double? leftMotorTemp;
|
||||
final double? rightMotorTemp;
|
||||
final double? chipTemp;
|
||||
final double? cuttingSpeed;
|
||||
final int? satelliteCnt;
|
||||
final int? controlMode;
|
||||
|
||||
const DeviceRealtimeLoaded({
|
||||
required this.realtimeData,
|
||||
this.voltage,
|
||||
this.battery,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
this.yaw,
|
||||
this.pitch,
|
||||
this.roll,
|
||||
this.leftMotorTemp,
|
||||
this.rightMotorTemp,
|
||||
this.chipTemp,
|
||||
this.cuttingSpeed,
|
||||
this.satelliteCnt,
|
||||
this.controlMode,
|
||||
});
|
||||
|
||||
factory DeviceRealtimeLoaded.fromRealtimeMessage(Map<String, dynamic> rawData) {
|
||||
final data = rawData['data'] as List<dynamic>? ?? [];
|
||||
|
||||
double? getValueByName(String name) {
|
||||
final item = data.firstWhere(
|
||||
(item) => item['name'] == name,
|
||||
orElse: () => null,
|
||||
);
|
||||
if (item == null) return null;
|
||||
return double.tryParse(item['value'].toString());
|
||||
}
|
||||
|
||||
int? getIntValueByName(String name) {
|
||||
final value = getValueByName(name);
|
||||
return value?.toInt();
|
||||
}
|
||||
|
||||
return DeviceRealtimeLoaded(
|
||||
realtimeData: rawData,
|
||||
voltage: getValueByName('voltage'),
|
||||
battery: getValueByName('battery'),
|
||||
latitude: getValueByName('latitude'),
|
||||
longitude: getValueByName('longitude'),
|
||||
yaw: getValueByName('yaw'),
|
||||
pitch: getValueByName('pitch'),
|
||||
roll: getValueByName('roll'),
|
||||
leftMotorTemp: getValueByName('leftMotorTemp'),
|
||||
rightMotorTemp: getValueByName('rightMotorTemp'),
|
||||
chipTemp: getValueByName('chipTemp'),
|
||||
cuttingSpeed: getValueByName('cuttingSpeed'),
|
||||
satelliteCnt: getIntValueByName('satelliteCnt'),
|
||||
controlMode: getIntValueByName('controlMode'),
|
||||
);
|
||||
}
|
||||
|
||||
DeviceRealtimeLoaded copyWith({
|
||||
Map<String, dynamic>? realtimeData,
|
||||
double? voltage,
|
||||
double? battery,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
double? yaw,
|
||||
double? pitch,
|
||||
double? roll,
|
||||
double? leftMotorTemp,
|
||||
double? rightMotorTemp,
|
||||
double? chipTemp,
|
||||
double? cuttingSpeed,
|
||||
int? satelliteCnt,
|
||||
int? controlMode,
|
||||
}) {
|
||||
return DeviceRealtimeLoaded(
|
||||
realtimeData: realtimeData ?? this.realtimeData,
|
||||
voltage: voltage ?? this.voltage,
|
||||
battery: battery ?? this.battery,
|
||||
latitude: latitude ?? this.latitude,
|
||||
longitude: longitude ?? this.longitude,
|
||||
yaw: yaw ?? this.yaw,
|
||||
pitch: pitch ?? this.pitch,
|
||||
roll: roll ?? this.roll,
|
||||
leftMotorTemp: leftMotorTemp ?? this.leftMotorTemp,
|
||||
rightMotorTemp: rightMotorTemp ?? this.rightMotorTemp,
|
||||
chipTemp: chipTemp ?? this.chipTemp,
|
||||
cuttingSpeed: cuttingSpeed ?? this.cuttingSpeed,
|
||||
satelliteCnt: satelliteCnt ?? this.satelliteCnt,
|
||||
controlMode: controlMode ?? this.controlMode,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
realtimeData,
|
||||
voltage,
|
||||
battery,
|
||||
latitude,
|
||||
longitude,
|
||||
yaw,
|
||||
pitch,
|
||||
roll,
|
||||
leftMotorTemp,
|
||||
rightMotorTemp,
|
||||
chipTemp,
|
||||
cuttingSpeed,
|
||||
satelliteCnt,
|
||||
controlMode,
|
||||
];
|
||||
}
|
||||
|
||||
class DeviceRealtimeError extends DeviceRealtimeState {
|
||||
final String message;
|
||||
|
||||
const DeviceRealtimeError(this.message);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import 'package:maibu_satabot_v2/core/update/update_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/core/update/version_check_service.dart';
|
||||
import 'package:maibu_satabot_v2/core/update/update_dialog.dart';
|
||||
import 'package:maibu_satabot_v2/core/update/update_state.dart';
|
||||
import 'package:maibu_satabot_v2/core/network/mqtt/mqtt_manager.dart';
|
||||
import 'package:maibu_satabot_v2/features/auth/presentation/bloc/auth_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/main_container/presentation/cubit/tab_config_cubit.dart';
|
||||
@@ -26,6 +27,7 @@ import 'core/localization/locale_cubit.dart';
|
||||
import 'features/auth/presentation/bloc/auth_state.dart';
|
||||
import 'features/auth/presentation/bloc/login_cubit.dart';
|
||||
import 'features/devices/presentation/bloc/device_status_bloc.dart';
|
||||
import 'features/devices/presentation/bloc/device_task_cubit.dart'; // 🔥 添加 DeviceTaskCubit
|
||||
import 'features/home/presentation/bloc/permission_request_bloc.dart';
|
||||
import 'features/remote_control/presentation/bloc/remote_control_cubit.dart';
|
||||
|
||||
@@ -43,6 +45,11 @@ void main() async {
|
||||
await logger.init();
|
||||
Bloc.observer = AppBlocObserver(logger);
|
||||
|
||||
// 🔥 初始化 MQTT 连接(异步,不阻塞启动)
|
||||
MqttManager().initialize().catchError((e) {
|
||||
debugPrint('⚠️ [Main] MQTT 初始化失败,但不影响应用启动: $e');
|
||||
});
|
||||
|
||||
// ⚠️ 注意:不要在启动时清除补丁版本记录!
|
||||
// 补丁版本记录只在整包更新成功后才清除
|
||||
// 如果在启动时清除,会导致差量更新后划掉App再进入时循环更新
|
||||
@@ -73,7 +80,16 @@ class MyApp extends StatelessWidget {
|
||||
create: (_) {
|
||||
final user = sl<AppUserCubit>().state.user;
|
||||
final devicesCubit = sl<DevicesCubit>();
|
||||
if (user != null) devicesCubit.fetchAllDevices(user.username);
|
||||
|
||||
// 🔥 修复:检查是否已有 targetDevice,如果有则不需要加载设备列表
|
||||
final remoteControlCubit = sl<RemoteControlCubit>();
|
||||
if (user != null && remoteControlCubit.state.targetDevice == null) {
|
||||
debugPrint('📱 [Main] 无 targetDevice,开始加载设备列表');
|
||||
devicesCubit.fetchAllDevices(user.username);
|
||||
} else if (user != null) {
|
||||
debugPrint('✅ [Main] 已有 targetDevice: ${remoteControlCubit.state.targetDevice!.deviceName},跳过设备列表加载');
|
||||
}
|
||||
|
||||
return devicesCubit;
|
||||
},
|
||||
),
|
||||
@@ -89,6 +105,9 @@ class MyApp extends StatelessWidget {
|
||||
BlocProvider<RemoteControlCubit>(
|
||||
create: (_) => sl<RemoteControlCubit>(),
|
||||
),
|
||||
BlocProvider<DeviceTaskCubit>(
|
||||
create: (_) => sl<DeviceTaskCubit>(),
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<LocaleCubit, Locale>(
|
||||
bloc: localeCubit,
|
||||
|
||||
16
pubspec.lock
16
pubspec.lock
@@ -312,6 +312,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.8"
|
||||
event_bus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: event_bus
|
||||
sha256: "1a55e97923769c286d295240048fc180e7b0768902c3c2e869fe059aafa15304"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -916,6 +924,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.5.7"
|
||||
mqtt_client:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: mqtt_client
|
||||
sha256: "41c8edd3bc8efc80c1c8ebfb40081c24d12d13085faca96b9280a624eca2d893"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "10.11.11"
|
||||
native_toolchain_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -140,6 +140,7 @@ dependencies:
|
||||
vibration: ^3.1.8
|
||||
flutter_patcher: ^0.1.2 # Add flutter_patcher here
|
||||
open_file: ^3.3.2 # 打开文件(安装 APK)
|
||||
mqtt_client: ^10.11.11
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user