Merge branch 'feature/my' of http://1.95.137.212:57001/APP/FlutterApp into feature/my
This commit is contained in:
@@ -11,6 +11,7 @@ import 'package:maibu_satabot_v2/features/devices/domain/usecases/select_work_re
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/usecases/unbind_device_usecase.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/usecases/update_devicename_usecase.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/services/path_planning_service.dart';
|
||||
import 'package:maibu_satabot_v2/features/my/presentation/bloc/my_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/my/repository/my_repository.dart';
|
||||
import 'package:maibu_satabot_v2/features/my/repository/my_repository_impl.dart';
|
||||
@@ -71,9 +72,14 @@ Future<void> init() async {
|
||||
sl.registerLazySingleton(
|
||||
() => TcpClient(sl<UserStorage>(), getUserDeviceUseCase: sl<GetUserDeviceUseCase>(), switchDeviceUseCase: sl<SwitchDeviceUseCase>()),
|
||||
);
|
||||
sl.registerLazySingleton(() => PathPlanningService());
|
||||
|
||||
/// 1.1.3 NetMessageDispatcher:消息调度器,并将 TcpClient 注入给它
|
||||
sl.registerLazySingleton(() => NetMessageDispatcher(sl()));
|
||||
sl.registerLazySingleton(() => NetMessageDispatcher(
|
||||
sl<TcpClient>(),
|
||||
sl<RoutePlanningRepository>(),
|
||||
sl<PathPlanningService>(),
|
||||
));
|
||||
|
||||
/// 1.2 --- 本地存储 (LocalStorage) ---
|
||||
/// 1.2.1 SharedPreferences:本地存储库
|
||||
@@ -164,7 +170,7 @@ Future<void> init() async {
|
||||
/// 5. 状态管理 (Cubit/Bloc)
|
||||
sl.registerLazySingleton(() => AppUserCubit()); // AuthCubit 依赖它,必须先注册
|
||||
sl.registerLazySingleton(() => GetDeviceLocationUseCase(sl()));
|
||||
sl.registerLazySingleton(() => DevicesCubit(sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(),sl(),sl()));
|
||||
sl.registerLazySingleton(() => DevicesCubit(sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(),sl(),sl(),sl()));
|
||||
sl.registerFactory(() => RemoteControlCubit(sl()));
|
||||
/* // 工厂模式(留存,每次获取新实例)
|
||||
|
||||
|
||||
4
lib/core/env/env_config.dart
vendored
4
lib/core/env/env_config.dart
vendored
@@ -7,9 +7,9 @@ class EnvConfig {
|
||||
static String get sentryDsn {
|
||||
// 根据环境切换 DSN(去 Sentry 后台创建两个项目:Test 和 Prod)
|
||||
if (environment == 'prod') {
|
||||
return 'https://your_prod_dsn@sentry.io/123';
|
||||
return 'https://d5fdda78335793935593efa3f4ed11bb@o4510984107261952.ingest.de.sentry.io/4510984527937616';
|
||||
}
|
||||
return 'https://your_test_dsn@sentry.io/456';
|
||||
return 'https://1a72051f8ac6c21cde96ded18a318217@o4510984107261952.ingest.de.sentry.io/4510984468824144';
|
||||
}
|
||||
|
||||
static bool get isProduction => environment == 'prod';
|
||||
|
||||
@@ -10,17 +10,26 @@ import 'package:intl/intl.dart';
|
||||
|
||||
class SentryLoggerImpl implements ILoggerService {
|
||||
File? _localLogFile;
|
||||
//调试模式开关
|
||||
bool _isDebugModeEnabled = false;
|
||||
|
||||
@override
|
||||
Future<void> init() async {
|
||||
// await SentryFlutter.init((options) {
|
||||
// options.dsn = EnvConfig.sentryDsn;
|
||||
// options.environment = EnvConfig.environment;
|
||||
// // 生产环境只采样 20% 的性能数据,测试环境全开
|
||||
// options.tracesSampleRate = EnvConfig.isProduction ? 0.2 : 1.0;
|
||||
// });
|
||||
await _collectDeviceInfo();
|
||||
_isDebugModeEnabled = false;
|
||||
await SentryFlutter.init((options) {
|
||||
options.dsn = EnvConfig.sentryDsn;
|
||||
options.environment = EnvConfig.environment;
|
||||
// 生产环境只采样 20% 的性能数据,测试环境全开
|
||||
options.tracesSampleRate = EnvConfig.isProduction ? 0.2 : 1.0;
|
||||
|
||||
// 动态设置 Sentry 接收的日志级别
|
||||
// 如果开启调试模式,接收 DEBUG;否则生产环境只接收 INFO 以上
|
||||
|
||||
options.debug = _isDebugModeEnabled; // 开启 SDK 内部调试
|
||||
|
||||
});
|
||||
|
||||
await _collectDeviceInfo();
|
||||
await _prepareLocalFile();
|
||||
}
|
||||
|
||||
@@ -93,6 +102,54 @@ class SentryLoggerImpl implements ILoggerService {
|
||||
// 本地写入失败不应崩溃
|
||||
}
|
||||
}
|
||||
@override
|
||||
void logWithLevel(
|
||||
String message, {
|
||||
String level = 'INFO', // 'DEBUG', 'INFO', 'WARNING', 'ERROR'
|
||||
Map<String, dynamic>? data,
|
||||
}) {
|
||||
final timestamp = DateTime.now().toIso8601String();
|
||||
final logEntry = "[$timestamp] [$level] $message ${data ?? ''}\n";
|
||||
|
||||
// --- 策略判断 ---
|
||||
// 1. 开发环境:全部允许
|
||||
// 2. 生产环境:
|
||||
// - 如果是 DEBUG 级别:只有 _isDebugModeEnabled 为 true 时才允许
|
||||
// - 其他级别:始终允许
|
||||
bool shouldProcess = !EnvConfig.isProduction ||
|
||||
level != 'DEBUG' ||
|
||||
_isDebugModeEnabled;
|
||||
|
||||
if (shouldProcess) {
|
||||
// 写入本地文件
|
||||
_writeToLocalFile(logEntry);
|
||||
|
||||
// 上报 Sentry
|
||||
SentryLevel sentryLevel;
|
||||
switch (level) {
|
||||
case 'DEBUG': sentryLevel = SentryLevel.debug; break;
|
||||
case 'WARNING': sentryLevel = SentryLevel.warning; break;
|
||||
case 'ERROR': sentryLevel = SentryLevel.error; break;
|
||||
default: sentryLevel = SentryLevel.info;
|
||||
}
|
||||
|
||||
// 使用 addBreadcrumb 记录轨迹
|
||||
Sentry.addBreadcrumb(Breadcrumb(
|
||||
message: message,
|
||||
level: sentryLevel,
|
||||
data: data,
|
||||
));
|
||||
|
||||
// 如果是 ERROR,额外捕获为一个事件
|
||||
if (level == 'ERROR') {
|
||||
Sentry.captureMessage(message, level: sentryLevel);
|
||||
}
|
||||
}
|
||||
|
||||
if (kDebugMode) {
|
||||
debugPrint(logEntry);
|
||||
}
|
||||
}
|
||||
|
||||
// 增加一个获取本地日志文件路径的方法,方便你以后做“导出日志”功能
|
||||
Future<String?> getLocalLogPath() async {
|
||||
|
||||
@@ -6,6 +6,11 @@ abstract class ILoggerService {
|
||||
bool isWarning = false,
|
||||
Map<String, dynamic>? data,
|
||||
});
|
||||
void logWithLevel(
|
||||
String message, {
|
||||
String level = 'INFO', // 'DEBUG', 'INFO', 'WARNING', 'ERROR'
|
||||
Map<String, dynamic>? data,
|
||||
});
|
||||
void addBreadcrumb(String message, {String? category});
|
||||
void setUser(String userId);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ class DioClient {
|
||||
final dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: HttpApiConsts.baseUrl,
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 10),
|
||||
connectTimeout: const Duration(seconds: 12),
|
||||
receiveTimeout: const Duration(seconds: 12),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/data/models/device_add_path_point_model.dart';
|
||||
|
||||
import '../../features/devices/domain/repositories/route_planning_repository.dart';
|
||||
import '../../features/devices/services/path_planning_service.dart';
|
||||
import 'protocol_decoder.dart';
|
||||
import 'tcp/tcp_client.dart';
|
||||
// import '../../features/auth/data/models/user_model.dart';
|
||||
|
||||
class NetMessageDispatcher {
|
||||
final TcpClient tcpClient;
|
||||
NetMessageDispatcher(this.tcpClient);
|
||||
final PathPlanningService _pathPlanningService;
|
||||
final RoutePlanningRepository routePlanningRepository;
|
||||
|
||||
NetMessageDispatcher(this.tcpClient, this.routePlanningRepository, this._pathPlanningService);
|
||||
|
||||
|
||||
/// 过滤特定指令的流
|
||||
@@ -36,6 +44,85 @@ class NetMessageDispatcher {
|
||||
return onCommand(cmd).map((p) => jsonDecode(utf8.decode(p.payload)));
|
||||
}
|
||||
|
||||
//监听路径规划的开始作业的指令的应答
|
||||
|
||||
// if (data[0] == (byte) 0xAB && data[1] == (byte) 0xAA && data[2] == (byte) 0x01) {
|
||||
// System.out.println("=====下位机回复成功=====" + data[0] + " " + data[1] + " " + data[2] + " " + data[3] + " " +
|
||||
// data[4] + " " + data[5] + " " + data[6] + " " + data[7] + " " + data[8] + " " + data[9]);
|
||||
// if (data[5] == (byte) 0x01) {
|
||||
// Point point = new Point();
|
||||
// //发送下一个指令
|
||||
// }
|
||||
|
||||
/// 监听路径规划的开始作业指令的应答 (0x01 指令)
|
||||
/// 协议格式:AB AA 01 [DATA] CRC(2) AA AB
|
||||
/// 验证头部:data[0]=0xAB, data[1]=0xAA, data[2]=0x01
|
||||
/// 检查状态位:data[5] == 0x01 表示回复成功
|
||||
Stream<RawPacket> onPathPlanningResponse() {
|
||||
debugPrint('[Dispatcher] 开始监听路径规划指令应答 (CMD: 0x01)');
|
||||
|
||||
return onCommand(0x01).where((packet) {
|
||||
// 构造完整数据包用于验证
|
||||
final fullData = <int>[
|
||||
0xAB, 0xAA, 0x01,
|
||||
...packet.payload,
|
||||
0x00, 0x00, // CRC 占位
|
||||
0xAA, 0xAB
|
||||
];
|
||||
|
||||
// 验证头部协议
|
||||
if (fullData[0] == 0xAB &&
|
||||
fullData[1] == 0xAA &&
|
||||
fullData[2] == 0x01) {
|
||||
|
||||
debugPrint('[Dispatcher] 下位机回复成功 - 完整数据包:${fullData.join(" ")}');
|
||||
|
||||
// 检查状态位 (索引 5 对应 payload 的第 2 个字节)
|
||||
if (fullData.length > 5 && fullData[5] == 0x01) {
|
||||
debugPrint('✅ [Dispatcher] 状态位验证通过:0x01 - 可以发送下一个指令');
|
||||
///取全局的路径规划发送实体 queue
|
||||
/// 发送下一个指令 移除上一条数据
|
||||
var queue = _pathPlanningService.getQueue();
|
||||
//去除第一条数据
|
||||
queue.removeFirst();
|
||||
_pathPlanningService.updateQueue(queue as List<DeviceAddPathPointModel>);
|
||||
routePlanningRepository.startRoutePlanning(queue);
|
||||
return true;
|
||||
} else {
|
||||
debugPrint('⚠️ [Dispatcher] 状态位异常:${fullData.length > 5 ? fullData[5].toRadixString(16) : "N/A"}');
|
||||
}
|
||||
} else {
|
||||
debugPrint('❌ [Dispatcher] 协议头验证失败');
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/// 监听特定状态的回复(可复用)
|
||||
/// [commandCode] 指令码
|
||||
/// [statusIndex] 状态字节在 payload 中的索引
|
||||
/// [expectedStatus] 期望的状态值
|
||||
Stream<RawPacket> onStatusResponse({
|
||||
required int commandCode,
|
||||
required int statusIndex,
|
||||
required int expectedStatus,
|
||||
}) {
|
||||
debugPrint('🔍 [Dispatcher] 监听指令应答 CMD: 0x${commandCode.toRadixString(16)}, 期望状态:0x${expectedStatus.toRadixString(16)}');
|
||||
|
||||
return onCommand(commandCode).where((packet) {
|
||||
if (packet.payload.length > statusIndex) {
|
||||
final status = packet.payload[statusIndex];
|
||||
if (status == expectedStatus) {
|
||||
debugPrint('✅ [Dispatcher] 状态验证通过 - CMD: 0x${commandCode.toRadixString(16)}, 状态:0x${status.toRadixString(16)}');
|
||||
return true;
|
||||
} else {
|
||||
debugPrint('⚠️ [Dispatcher] 状态不匹配 - 期望:0x${expectedStatus.toRadixString(16)}, 实际:0x${status.toRadixString(16)}');
|
||||
}
|
||||
} else {
|
||||
debugPrint('❌ [Dispatcher] Payload 长度不足 - 索引:$statusIndex, 实际长度:${packet.payload.length}');
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ class AuthTcpDatasourceImpl implements AuthTcpDatasource {
|
||||
@override
|
||||
Future<void> sendAuthPacket() async {
|
||||
if (!_tcpClient.isConnected) {
|
||||
throw Exception('TCP 未连接,无法发送认证包');
|
||||
throw Exception('TCP 未连接,无法发送认证包sendAuthPacket');
|
||||
}
|
||||
|
||||
final user = await _userStorage.getUser();
|
||||
@@ -40,7 +40,7 @@ class AuthTcpDatasourceImpl implements AuthTcpDatasource {
|
||||
final token = user.token!;
|
||||
final authString = '$username:app:$token';
|
||||
|
||||
debugPrint('🔑 [AuthTcp] 准备发送认证包:$authString');
|
||||
debugPrint('🔑 sendAuthPacket[AuthTcp] 准备发送认证包:$authString');
|
||||
|
||||
final authBytes = utf8.encode(authString);
|
||||
|
||||
@@ -57,7 +57,7 @@ class AuthTcpDatasourceImpl implements AuthTcpDatasource {
|
||||
..addByte(0xAB);
|
||||
|
||||
_tcpClient.sendRawBytes(builder.takeBytes()); // 假设 TcpClient 增加了此方法,或用 socket.add
|
||||
debugPrint('✅ [AuthTcp] 认证包已发送');
|
||||
debugPrint('✅ sendAuthPacket[AuthTcp] 认证包已发送');
|
||||
|
||||
// 获取用户设备列表
|
||||
try {
|
||||
@@ -70,36 +70,36 @@ class AuthTcpDatasourceImpl implements AuthTcpDatasource {
|
||||
await eitherResult.fold(
|
||||
(failure) {
|
||||
// 处理失败:打印日志或抛出异常
|
||||
debugPrint('❌ [AuthTcp] 获取设备列表失败:$failure');
|
||||
debugPrint('❌ sendAuthPacket [AuthTcp] 获取设备列表失败:$failure');
|
||||
throw Exception('获取设备列表失败:$failure');
|
||||
},
|
||||
(devices) async {
|
||||
// 处理成功:devices 现在是真正的 List<DeviceEntity>
|
||||
if (devices.isEmpty) {
|
||||
debugPrint('⚠️ [AuthTcp] 当前用户无可用设备,跳过切换步骤');
|
||||
debugPrint('⚠️ sendAuthPacket[AuthTcp] 当前用户无可用设备,跳过切换步骤');
|
||||
return;
|
||||
}
|
||||
|
||||
// 取第一个设备
|
||||
final DeviceEntity targetDevice = devices.first;
|
||||
debugPrint('📱 [AuthTcp] 准备切换至默认设备:${targetDevice.deviceName}');
|
||||
debugPrint('📱 sendAuthPacket[AuthTcp] 准备切换至默认设备:${targetDevice.deviceName}');
|
||||
|
||||
// 切换设备 (同样,如果 switchDeviceUseCase 也返回 Either,也需要 fold 处理)
|
||||
final switchResult = await switchDeviceUseCase.deviceRepository.switchDevice("app",targetDevice.deviceName);
|
||||
|
||||
await switchResult.fold(
|
||||
(failure) {
|
||||
debugPrint('❌ [AuthTcp] 切换设备失败:$failure');
|
||||
debugPrint('❌ sendAuthPacket[AuthTcp] 切换设备失败:$failure');
|
||||
throw Exception('切换设备失败:$failure');
|
||||
},
|
||||
(success) {
|
||||
debugPrint('✅ [AuthTcp] 设备切换成功,服务端应开始推送数据');
|
||||
debugPrint('✅ sendAuthPacket[AuthTcp] 设备切换成功,服务端应开始推送数据');
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ [AuthTcp] 设备订阅流程异常:$e');
|
||||
debugPrint('❌ sendAuthPacket[AuthTcp] 设备订阅流程异常:$e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:maibu_satabot_v2/core/consts/tcp_consts.dart';
|
||||
import 'package:maibu_satabot_v2/core/domain/entities/user_entity.dart';
|
||||
import 'package:maibu_satabot_v2/core/network/net_message_dispatcher.dart';
|
||||
|
||||
import '../../../../core/app/app_user_cubit.dart';
|
||||
import '../../../../core/infrastructure/logging/sentry_logger_impl.dart';
|
||||
import '../../../../core/logging/i_logger_service.dart';
|
||||
import '../../../../core/network/tcp/tcp_client.dart';
|
||||
import '../../../../core/storage/user_storage.dart';
|
||||
import '../../../devices/domain/usecases/get_user_device_usecase.dart';
|
||||
@@ -34,16 +37,26 @@ class AuthCubit extends Cubit<AuthState> {
|
||||
|
||||
/// App 启动时检查本地缓存
|
||||
Future<void> appStarted() async {
|
||||
final logger = GetIt.I<ILoggerService>() as SentryLoggerImpl;
|
||||
// print("App 启动时检查本地缓存");
|
||||
final user = await storage.getUser();
|
||||
logger.logWithLevel(
|
||||
'启动时检查本地缓存',
|
||||
level: 'INFO',
|
||||
data: {'user': user, 'data': user}
|
||||
);
|
||||
if (user != null) {
|
||||
// 1. 建立 TCP 连接 (远程控制)
|
||||
// print("App 启动时检查本地缓存user != null) ");
|
||||
// 1. print("App 启动时检查本地缓存user != null) ");
|
||||
// print("App 启动时检查本地缓存 tcp.connect) ");
|
||||
tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT);
|
||||
//await _authTcpDatasource.sendAuthPacket();//包括发送认证包和获取列表和切换函数
|
||||
// await _authTcpDatasource.sendAuthPacket();//包括发送认证包和获取列表和切换函数
|
||||
// 2. 同步全局 App 状态
|
||||
appCubit.setAuth(user);
|
||||
// 3. 进入已登录状态
|
||||
emit(AuthAuthenticated(user));
|
||||
} else {
|
||||
// print("App 启动时检查本地缓存user=null) ");
|
||||
emit(AuthUnauthenticated());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:maibu_satabot_v2/core/consts/http_api_consts.dart';
|
||||
import 'package:maibu_satabot_v2/core/network/tcp/tcp_client.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/data/datasources/device_http_datasource.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity.dart';
|
||||
|
||||
import '../../../../../core/infrastructure/logging/sentry_logger_impl.dart';
|
||||
import '../../../../../core/logging/i_logger_service.dart';
|
||||
import '../../../../../core/storage/user_storage.dart';
|
||||
import '../../../domain/entities/device_location_entity.dart';
|
||||
|
||||
@@ -24,14 +26,43 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
|
||||
|
||||
@override
|
||||
Future<int> bindDevice(String deviceId, String deviceAlias) async {
|
||||
var response = await dio.post(HttpApiConsts.bindDevice, data: {'deviceId': deviceId, 'deviceAlias': deviceAlias});
|
||||
final logger = GetIt.I<ILoggerService>() as SentryLoggerImpl;
|
||||
logger.logWithLevel('用户点击bindDevice方法开始触发', level: 'INFO');
|
||||
logger.logWithLevel(
|
||||
'用户点击bindDevice方法API 请求开始',
|
||||
level: 'INFO',
|
||||
data: {'url': 'https://serviceri.satabot.com/iot/device/bindDevice'}
|
||||
);
|
||||
var response = await dio.post(
|
||||
HttpApiConsts.bindDevice,
|
||||
data: {'deviceId': deviceId, 'deviceAlias': deviceAlias},
|
||||
);
|
||||
logger.logWithLevel(
|
||||
'请求详细参数',
|
||||
level: 'DEBUG',
|
||||
data: {
|
||||
'url': 'https://serviceri.satabot.com/iot/device/bindDevice',
|
||||
'deviceId': deviceId,
|
||||
'deviceAlias': deviceAlias
|
||||
}
|
||||
);
|
||||
if (response.statusCode != 200) {
|
||||
logger.logWithLevel(
|
||||
'API 响应失败',
|
||||
level: 'ERROR',
|
||||
data: {'statusCode': response.statusCode, 'data': response.data}
|
||||
);
|
||||
throw Exception('网络请求失败:${response.statusCode}');
|
||||
}
|
||||
|
||||
final responseData = response.data;
|
||||
|
||||
if (responseData['code'] != 200 || responseData['data'] != true) {
|
||||
logger.logWithLevel(
|
||||
'API 响应失败',
|
||||
level: 'ERROR',
|
||||
data: {'statusCode': response.statusCode, 'data': response.data}
|
||||
);
|
||||
throw Exception(responseData['msg'] ?? '业务异常');
|
||||
}
|
||||
|
||||
@@ -76,7 +107,15 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
|
||||
|
||||
@override
|
||||
Future<int> switchDevice(String platform, String deviceId) async {
|
||||
final logger = GetIt.I<ILoggerService>() as SentryLoggerImpl;
|
||||
final token = await _getToken();
|
||||
print('用户点击switchDevice方法开始触发: $token');
|
||||
logger.logWithLevel('用户点击switchDevice方法开始触发', level: 'DEBUG');
|
||||
logger.logWithLevel(
|
||||
'用户点击switchDevice方法API 请求开始',
|
||||
level: 'INFO',
|
||||
data: {'url': 'https://serviceri.satabot.com/iot/device/switchDevice'}
|
||||
);
|
||||
var response = await dio.post(
|
||||
HttpApiConsts.switchDevice,
|
||||
data: {'platform': platform, 'deviceId': deviceId},
|
||||
@@ -87,16 +126,38 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
|
||||
},
|
||||
),
|
||||
);
|
||||
logger.logWithLevel(
|
||||
'请求详细参数',
|
||||
level: 'DEBUG',
|
||||
data: {
|
||||
'url': 'https://serviceri.satabot.com/iot/device/switchDevice',
|
||||
'platform': platform,
|
||||
'deviceId': deviceId
|
||||
}
|
||||
);
|
||||
if (response.statusCode != 200) {
|
||||
logger.logWithLevel(
|
||||
'API 请求失败',
|
||||
level: 'ERROR',
|
||||
data: {'statusCode': response.statusCode, 'data': response.data}
|
||||
);
|
||||
throw Exception('网络请求失败:${response.statusCode}');
|
||||
}
|
||||
|
||||
final responseData = response.data;
|
||||
|
||||
logger.logWithLevel(
|
||||
'API 响应成功',
|
||||
level: 'INFO',
|
||||
data: {'statusCode': response.statusCode, 'data': response.data}
|
||||
);
|
||||
if (responseData['code'] != 200 || responseData['data'] != true) {
|
||||
logger.logWithLevel(
|
||||
'API 响应失败',
|
||||
level: 'ERROR',
|
||||
data: {'statusCode': response.statusCode, 'data': response.data}
|
||||
);
|
||||
throw Exception(responseData['msg'] ?? '业务异常');
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -135,9 +196,19 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
|
||||
|
||||
@override
|
||||
Future<dynamic> getDeviceLocation(String deviceame) async {
|
||||
final logger = GetIt.I<ILoggerService>() as SentryLoggerImpl;
|
||||
// TODO: implement getDeviceLocation
|
||||
try {
|
||||
// 发起 GET 请求
|
||||
logger.logWithLevel('用户点击getDeviceLocation方法开始触发', level: 'INFO');
|
||||
logger.logWithLevel(
|
||||
'请求详细参数',
|
||||
level: 'DEBUG',
|
||||
data: {
|
||||
'url': 'https://serviceri.satabot.com/iot/device/userDevice',
|
||||
'tenantName': deviceame
|
||||
}
|
||||
);
|
||||
final response = await dio.get(
|
||||
'https://serviceri.satabot.com/iot/device/userDevice',
|
||||
queryParameters: {'tenantName': deviceame},
|
||||
@@ -152,17 +223,35 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
|
||||
// 检查响应状态码
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
logger.logWithLevel(
|
||||
'API 响应接收',
|
||||
level: 'DEBUG',
|
||||
data: {'statusCode': response.statusCode, 'data': response.data}
|
||||
);
|
||||
if (data['code'] == 200) {
|
||||
//return data['data']; // 返回设备数据
|
||||
final locationData = data['data']; // 假设数据结构,测试时候如有不妥就修改结果的实例化
|
||||
return DeviceLocationEntity(deviceName: locationData['deviceName'], latitude: locationData['latitude'], longitude: locationData['longitude']);
|
||||
logger.logWithLevel(
|
||||
'成功获取设备位置',
|
||||
level: 'INFO',
|
||||
data: {'lat': locationData['latitude'], 'lng': locationData['longitude']}
|
||||
);
|
||||
return DeviceLocationEntity(
|
||||
deviceName: locationData['deviceName'],
|
||||
latitude: locationData['latitude'],
|
||||
longitude: locationData['longitude'],
|
||||
);
|
||||
} else {
|
||||
logger.logWithLevel(data['msg'] ?? '业务异常', level: 'ERROR');
|
||||
throw Exception(data['msg'] ?? '业务异常');
|
||||
}
|
||||
} else {
|
||||
logger.logWithLevel('网络请求失败: ${response.statusCode}', level: 'ERROR');
|
||||
throw Exception('网络请求失败: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
logger.logWithLevel('获取设备位置失败: $e', level: 'ERROR');
|
||||
logger.captureException(e, stackTrace: e);
|
||||
throw Exception('获取设备位置失败: $e');
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import '../../domain/usecases/delete_work_record_usecase.dart';
|
||||
import '../../domain/usecases/get_device_location_usecase.dart';
|
||||
import '../../domain/usecases/get_work_record_usecase.dart';
|
||||
import '../../domain/usecases/route_planning_usecase.dart';
|
||||
import '../../services/path_planning_service.dart';
|
||||
import 'device_status_bloc.dart';
|
||||
import 'device_status_event.dart';
|
||||
import 'devices_state.dart';
|
||||
@@ -39,6 +40,9 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
final RoutePlanningUseCase _routePlanningUseCase; //
|
||||
final DeviceStatusBloc _deviceStatusBloc; // 🔥 新增字段
|
||||
final TcpClient _tcpClient;
|
||||
// 🔥 关键:注入全局服务
|
||||
final PathPlanningService _pathPlanningService;
|
||||
|
||||
DevicesCubit(
|
||||
this.repository,
|
||||
this._getUserDeviceUseCase,
|
||||
@@ -52,9 +56,8 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
this._generatePathUseCase,
|
||||
this._routePlanningUseCase,
|
||||
this._bindDeviceUseCase,
|
||||
this._deviceStatusBloc, this._tcpClient,
|
||||
|
||||
|
||||
this._deviceStatusBloc,
|
||||
this._tcpClient, this._pathPlanningService,
|
||||
) : super(const DevicesState());
|
||||
|
||||
Future<void> unbindDevice(String deviceId, String deviceName) async {
|
||||
@@ -346,14 +349,21 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
|
||||
// 开始路径规划
|
||||
Future<void> startRoutePlanning(Queue<DeviceAddPathPointModel> locationQueue) async {
|
||||
// 清空全局 Service 中的队列
|
||||
// _routePlanningUseCase.clearLocationQueue();
|
||||
_pathPlanningService.updateQueue(locationQueue as List<DeviceAddPathPointModel>);
|
||||
// 开始路径规划
|
||||
emit(state.copyWith(isLoading: true));
|
||||
final result = await _routePlanningUseCase.startRoutePlanning(locationQueue);
|
||||
//传入全局的Service 中路径规划队列
|
||||
Queue<DeviceAddPathPointModel> queue = _pathPlanningService.getQueue() ;
|
||||
final result = await _routePlanningUseCase.startRoutePlanning(queue);
|
||||
result.fold(
|
||||
(failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message ?? '路径规划启动失败')),
|
||||
(_) => emit(state.copyWith(isLoading: false, errorMessage: '')),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// 暂停
|
||||
Future<void> pauseRoutePlanning() async {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
|
||||
75
lib/features/devices/services/path_planning_service.dart
Normal file
75
lib/features/devices/services/path_planning_service.dart
Normal file
@@ -0,0 +1,75 @@
|
||||
// lib/features/devices/domain/services/path_planning_service.dart
|
||||
import 'dart:collection';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import '../data/models/device_add_path_point_model.dart';
|
||||
|
||||
class PathPlanningService {
|
||||
static final PathPlanningService _instance = PathPlanningService._internal();
|
||||
factory PathPlanningService() => _instance;
|
||||
PathPlanningService._internal();
|
||||
|
||||
// 🔥 核心:全局唯一的可变队列
|
||||
final Queue<DeviceAddPathPointModel> locationQueue = Queue();
|
||||
|
||||
bool isRunning = false;
|
||||
int currentIndex = 0;
|
||||
int totalCount = 0;
|
||||
|
||||
/// 🔥 存:生成路径后立即存储
|
||||
void updateQueue(List<DeviceAddPathPointModel> locations) {
|
||||
if (locations.isEmpty) {
|
||||
debugPrint('⚠️ [PathPlanningService] 尝试更新空队列');
|
||||
return;
|
||||
}
|
||||
|
||||
locationQueue.clear();
|
||||
locationQueue.addAll(locations);
|
||||
currentIndex = 0;
|
||||
totalCount = locations.length;
|
||||
|
||||
debugPrint('✅ [PathPlanningService] 队列已更新,共 $totalCount 个点');
|
||||
}
|
||||
|
||||
/// 🔥 取:发送时取出第一个点
|
||||
DeviceAddPathPointModel? removeFirst() {
|
||||
if (locationQueue.isEmpty) {
|
||||
debugPrint('⚠️ [PathPlanningService] 队列为空');
|
||||
return null;
|
||||
}
|
||||
|
||||
final point = locationQueue.removeFirst();
|
||||
currentIndex++;
|
||||
|
||||
debugPrint(
|
||||
'📍 [PathPlanningService] 取出第 $currentIndex/$totalCount 个点:'
|
||||
'${point.latitude}, ${point.longitude}',
|
||||
);
|
||||
|
||||
return point;
|
||||
}
|
||||
|
||||
/// 🔥 获取进度
|
||||
int get remainingCount => locationQueue.length;
|
||||
int get completedCount => currentIndex;
|
||||
double get progress => totalCount == 0 ? 0.0 : currentIndex / totalCount;
|
||||
|
||||
bool get isEmpty => locationQueue.isEmpty;
|
||||
bool get isNotEmpty => locationQueue.isNotEmpty;
|
||||
|
||||
/// 🔥 清空(作业完成或停止时)
|
||||
void clear() {
|
||||
locationQueue.clear();
|
||||
isRunning = false;
|
||||
currentIndex = 0;
|
||||
totalCount = 0;
|
||||
debugPrint('🧹 [PathPlanningService] 队列已清空');
|
||||
}
|
||||
|
||||
|
||||
int get length => locationQueue.length;
|
||||
|
||||
/// 获取当前队列(供 Cubit 使用)
|
||||
Queue<DeviceAddPathPointModel> getQueue() {
|
||||
return Queue.of(locationQueue);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user