集成双 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

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