集成双 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:
2026-06-18 16:41:02 +08:00
parent a6143f2ba7
commit 06718ed957
27 changed files with 1645 additions and 27 deletions

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View 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;
}

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

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

View File

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

View File

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

View 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;
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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