426 lines
18 KiB
Dart
426 lines
18 KiB
Dart
import 'dart:collection';
|
||
|
||
import 'package:flutter/rendering.dart';
|
||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||
import 'package:get_it/get_it.dart';
|
||
import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity.dart';
|
||
import 'package:maibu_satabot_v2/features/devices/domain/repositories/device_repository.dart';
|
||
import 'package:maibu_satabot_v2/features/devices/domain/usecases/generate_path_usecase.dart';
|
||
import 'package:maibu_satabot_v2/features/devices/domain/usecases/get_user_device_usecase.dart';
|
||
import 'package:maibu_satabot_v2/features/devices/domain/usecases/save_work_record_usecase.dart';
|
||
import 'package:maibu_satabot_v2/features/devices/domain/usecases/select_work_record_usecase.dart';
|
||
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 '../../../../core/consts/tcp_consts.dart';
|
||
import '../../../../core/network/tcp/tcp_client.dart';
|
||
import '../../data/models/device_add_path_point_model.dart';
|
||
import '../../data/models/device_work_area_param_model.dart';
|
||
import '../../domain/usecases/bind_device_usecase.dart';
|
||
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';
|
||
|
||
class DevicesCubit extends Cubit<DevicesState> {
|
||
final GetUserDeviceUseCase _getUserDeviceUseCase;
|
||
final GetDeviceLocationUseCase _getDeviceLocationUseCase;
|
||
final DeviceRepository repository;
|
||
final GetWorkRecordUseCase _getWorkRecordUseCase;
|
||
final DeleteWorkRecordUseCase _deleteWorkRecordUseCase;
|
||
final BindDeviceUseCase _bindDeviceUseCase;
|
||
final UnbindDeviceUseCase _unbindDeviceUseCase;
|
||
final UpdateDevicenameUsecase _updateDevicename;
|
||
final SelectWorkRecordUseCase _selectWorkRecordUseCase;
|
||
final SaveWorkRecordUseCase _saveWorkRecordUseCase;
|
||
final GeneratePathUseCase _generatePathUseCase;
|
||
final RoutePlanningUseCase _routePlanningUseCase; //
|
||
final DeviceStatusBloc _deviceStatusBloc; // 🔥 新增字段
|
||
final TcpClient _tcpClient;
|
||
// 🔥 关键:注入全局服务
|
||
final PathPlanningService _pathPlanningService;
|
||
|
||
DevicesCubit(
|
||
this.repository,
|
||
this._getUserDeviceUseCase,
|
||
this._getDeviceLocationUseCase,
|
||
this._getWorkRecordUseCase,
|
||
this._deleteWorkRecordUseCase,
|
||
this._unbindDeviceUseCase,
|
||
this._updateDevicename,
|
||
this._selectWorkRecordUseCase,
|
||
this._saveWorkRecordUseCase,
|
||
this._generatePathUseCase,
|
||
this._routePlanningUseCase,
|
||
this._bindDeviceUseCase,
|
||
this._deviceStatusBloc,
|
||
this._tcpClient, this._pathPlanningService,
|
||
) : super(const DevicesState());
|
||
|
||
Future<void> unbindDevice(String deviceId, String deviceName) async {
|
||
emit(state.copyWith(isLoading: true, errorMessage: '', operationType: DeviceOperationType.unbind));
|
||
|
||
try {
|
||
final params = UnbindDeviceParams(deviceId, deviceName);
|
||
final result = await _unbindDeviceUseCase.call(params);
|
||
|
||
result.fold(
|
||
(failure) => emit(
|
||
state.copyWith(
|
||
isLoading: false,
|
||
errorMessage: failure.message ?? '解绑设备失败',
|
||
operationType: DeviceOperationType.none, // 操作结束重置
|
||
),
|
||
),
|
||
(successCode) {
|
||
// 核心修复:int 转 bool 条件判断
|
||
final isSuccess = successCode == 1; // 显式转为 bool
|
||
if (isSuccess) {
|
||
final updatedDevices = state.devices?.where((device) {
|
||
return device.deviceName != deviceId;
|
||
}).toList();
|
||
|
||
emit(
|
||
state.copyWith(
|
||
isLoading: false,
|
||
devices: updatedDevices,
|
||
selectedDevice: state.selectedDevice?.deviceName == deviceId ? null : state.selectedDevice,
|
||
errorMessage: '',
|
||
operationType: DeviceOperationType.none,
|
||
),
|
||
);
|
||
} else {
|
||
emit(state.copyWith(isLoading: false, errorMessage: '解绑失败:状态码 $successCode', operationType: DeviceOperationType.none));
|
||
}
|
||
},
|
||
);
|
||
} catch (e) {
|
||
emit(state.copyWith(isLoading: false, errorMessage: '解绑异常:${e.toString()}', operationType: DeviceOperationType.none));
|
||
}
|
||
}
|
||
|
||
Future<void> updateDeviceName(String deviceId, String deviceName) async {
|
||
emit(state.copyWith(isLoading: true, errorMessage: '', operationType: DeviceOperationType.updateName));
|
||
|
||
try {
|
||
final params = UpdateDevicenameParams(deviceId, deviceName);
|
||
final result = await _updateDevicename.call(params);
|
||
|
||
result.fold((failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message ?? '更新设备名称失败', operationType: DeviceOperationType.none)), (
|
||
successCode,
|
||
) {
|
||
// 核心修复:int 转 bool 条件判断
|
||
print('更新设备名称结果代码: $successCode'); // 调试输出结果代码
|
||
final isSuccess = successCode == 1; // 显式转为 bool
|
||
if (isSuccess) {
|
||
//final updatedDevices = state.devices?.map((device) {
|
||
// return device.deviceName == deviceId ? device.copyWith(deviceName: deviceName) : device;
|
||
//}).toList();
|
||
|
||
emit(
|
||
state.copyWith(
|
||
isLoading: false,
|
||
//devices: updatedDevices,
|
||
//selectedDevice: state.selectedDevice?.deviceName == deviceId ? state.selectedDevice?.copyWith(deviceName: deviceName) : state.selectedDevice,
|
||
errorMessage: '',
|
||
operationType: DeviceOperationType.none,
|
||
),
|
||
);
|
||
} else {
|
||
emit(state.copyWith(isLoading: false, errorMessage: '更新设备名称失败:状态码 $successCode', operationType: DeviceOperationType.none));
|
||
}
|
||
});
|
||
} catch (e) {
|
||
emit(state.copyWith(isLoading: false, errorMessage: '更新设备名称异常:${e.toString()}', operationType: DeviceOperationType.none));
|
||
}
|
||
}
|
||
|
||
// 获取所有设备列表
|
||
// 获取所有设备列表
|
||
Future<void> fetchAllDevices(String username) async {
|
||
emit(state.copyWith(isLoading: true));
|
||
|
||
try {
|
||
// 🔥 关键步骤1:记录刷新前的选中设备标识(用 deviceName 作为唯一标识)
|
||
final String? oldSelectedDeviceName = state.selectedDevice?.deviceName;
|
||
|
||
// 网络请求获取新列表
|
||
var resultEither = await _getUserDeviceUseCase.call(GetUserDeviceParams(username));
|
||
|
||
resultEither.fold((failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message)), (deviceList) {
|
||
// 🔥 关键步骤2:匹配新列表中对应的旧选中设备
|
||
DeviceEntity? newSelectedDevice;
|
||
if (oldSelectedDeviceName != null && deviceList.isNotEmpty) {
|
||
// 在新列表中查找和旧选中设备名称一致的设备
|
||
newSelectedDevice = deviceList.firstWhere(
|
||
(device) => device.deviceName == oldSelectedDeviceName,
|
||
// 如果找不到(如设备已解绑),返回 null
|
||
orElse: () => deviceList.first, // 兜底:选中第一个
|
||
);
|
||
} else {
|
||
// 无旧选中设备,默认选中第一个
|
||
newSelectedDevice = deviceList.isNotEmpty ? deviceList.first : null;
|
||
}
|
||
|
||
// 🔥 关键步骤3:更新状态,使用匹配后的选中设备
|
||
emit(
|
||
state.copyWith(
|
||
devices: deviceList,
|
||
selectedDevice: newSelectedDevice, // 保留旧选中设备
|
||
isLoading: false,
|
||
),
|
||
);
|
||
|
||
});
|
||
} catch (e) {
|
||
emit(state.copyWith(isLoading: false, errorMessage: e.toString()));
|
||
}
|
||
}
|
||
|
||
// 切换当前选中的设备
|
||
void selectDevice(DeviceEntity device) {
|
||
emit(state.copyWith(selectedDevice: device));
|
||
}
|
||
|
||
// 更新单个设备的状态(例如从 Tcp 收到实时电量更新)
|
||
void updateDeviceStatus(DeviceEntity updatedDevice) {
|
||
final newList = state.devices.map((d) {
|
||
return d.deviceName == updatedDevice.deviceName ? updatedDevice : d;
|
||
}).toList();
|
||
|
||
// 如果更新的是当前选中的设备,也要同步更新 selectedDevice
|
||
final newSelected = state.selectedDevice?.deviceName == updatedDevice.deviceName ? updatedDevice : state.selectedDevice;
|
||
|
||
emit(state.copyWith(devices: newList, selectedDevice: newSelected));
|
||
}
|
||
|
||
/// 切换设备
|
||
Future<void> switchDevice(DeviceEntity device) async {
|
||
// 保持现有列表,只改 loading
|
||
emit(state.copyWith(isLoading: true));
|
||
|
||
final result = await repository.switchDevice("app", device.deviceName);
|
||
|
||
result.fold(
|
||
// 失败处理
|
||
(l) {
|
||
emit(state.copyWith(isLoading: false, errorMessage: l.message));
|
||
selectDevice(device);
|
||
},
|
||
// 成功处理:改为 async 函数块
|
||
(r) async {
|
||
// 3. 最后更新 UI 状态
|
||
try {
|
||
// 🔥 关键修复 1:先强制断开旧连接!
|
||
// 这一步会销毁旧 Socket,清除旧 Listener,防止旧数据继续推送
|
||
if (_tcpClient.isConnected) {
|
||
debugPrint('🛑 检测到已连接,先断开旧 TCP 连接...');
|
||
_tcpClient.disconnects(forSwitch: true);
|
||
// 稍微等待一下,确保底层 Socket 资源释放 (可选,但推荐)
|
||
// await Future.delayed(const Duration(milliseconds: 100));
|
||
}
|
||
|
||
// 🔥 关键修复 2:发起新连接
|
||
// connect 方法内部会自动调用 _sendAuthPacket -> 获取设备列表 -> 自动订阅当前选中的设备
|
||
debugPrint('🔌 开始重新新连接 TCP,将自动订阅新设备:${device.deviceName}');
|
||
await _tcpClient.connectBySwitch(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT, deviceName: device.deviceName);
|
||
// 🔥 关键修复 3:重置设备状态 Bloc,清除旧设备图表数据
|
||
_deviceStatusBloc.add(DeviceStatusReset());
|
||
|
||
debugPrint('✅ 新设备切换流程完成');
|
||
} catch (e) {
|
||
debugPrint('⚠️ 设备切换成功,但 TCP 重连或状态重置失败:$e');
|
||
// 即使 TCP 失败,也更新 UI 选中状态,让用户知道切换了,只是没数据
|
||
}
|
||
|
||
emit(state.copyWith(isLoading: false, selectedDevice: device));
|
||
},
|
||
);
|
||
}
|
||
|
||
/// 绑定设备
|
||
Future<void> bindDevice(String deviceId, String deviceAlias) async {
|
||
|
||
emit(state.copyWith(isLoading: true, errorMessage: ''));
|
||
try {
|
||
final params = BindDeviceParams(deviceId, deviceAlias);
|
||
final result = await _bindDeviceUseCase.call(params);
|
||
result.fold(
|
||
// 失败处理
|
||
(failure) {
|
||
emit(state.copyWith(
|
||
isLoading: false,
|
||
errorMessage: failure.message ?? '绑定设备失败',
|
||
));
|
||
// 抛出异常,携带后端返回的错误消息(如“设备不存在”)
|
||
throw Exception(failure.message ?? '绑定设备失败');
|
||
},
|
||
// 成功处理(返回 int 状态码)
|
||
(successCode) {
|
||
if (successCode == 1) {
|
||
emit(state.copyWith(isLoading: false, errorMessage: ''));
|
||
// 绑定成功后刷新设备列表
|
||
//fetchAllDevices(state.?.username ?? '');
|
||
} else {
|
||
emit(state.copyWith(isLoading: false, errorMessage: '绑定失败:状态码 $successCode'));
|
||
}
|
||
},
|
||
);
|
||
} catch (e) {
|
||
// emit(state.copyWith(
|
||
// isLoading: false,
|
||
// errorMessage: '绑定异常:${e.toString()}',
|
||
// ));
|
||
// 🔥 重新抛出,传递给 UI
|
||
rethrow;
|
||
}
|
||
}
|
||
|
||
/// 获取设备位置
|
||
Future<void> getDeviceLocation(DeviceEntity device) async {
|
||
emit(state.copyWith(isLoading: true));
|
||
final result = await _getDeviceLocationUseCase.call(device.deviceName);
|
||
result.fold(
|
||
(failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message)),
|
||
(location) => emit(state.copyWith(isLoading: false, deviceLatitude: location.latitude, deviceLongitude: location.longitude)),
|
||
);
|
||
}
|
||
|
||
///获取记录
|
||
Future<void> loadWorkRecords(String userId) async {
|
||
emit(state.copyWith(isLoading: true));
|
||
final result = await _getWorkRecordUseCase(userId);
|
||
result.fold(
|
||
(failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message)),
|
||
(records) => emit(state.copyWith(isLoading: false, workRecords: records)),
|
||
);
|
||
}
|
||
|
||
/// 删除工作记录
|
||
Future<void> deleteWorkRecord(String workName) async {
|
||
emit(state.copyWith(isLoading: true));
|
||
final result = await _deleteWorkRecordUseCase(workName);
|
||
result.fold(
|
||
(failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message)),
|
||
(_) => emit(state.copyWith(isLoading: false, workRecords: state.workRecords?.where((record) => record != workName).toList())),
|
||
);
|
||
}
|
||
|
||
/// 加载指定作业名的路径数据
|
||
Future<void> loadSelectedPath(String workName) async {
|
||
emit(state.copyWith(isLoading: true));
|
||
try {
|
||
final result = await _selectWorkRecordUseCase.call(workName);
|
||
result.fold(
|
||
(failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message)),
|
||
(data) => emit(state.copyWith(isLoading: false, pathData: data)),
|
||
);
|
||
} catch (e) {
|
||
emit(state.copyWith(isLoading: false, errorMessage: e.toString()));
|
||
}
|
||
}
|
||
|
||
/// 保存路径数据
|
||
Future<void> saveWorkRecord(String workName, String userId, String jsonData) async {
|
||
emit(state.copyWith(isLoading: true));
|
||
final result = await _saveWorkRecordUseCase.call(workName: workName, userId: userId, jsonData: jsonData);
|
||
result.fold((failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message)), (data) => emit(state.copyWith(isLoading: false)));
|
||
}
|
||
|
||
/// generatePath
|
||
Future<void> generatePath({
|
||
required ReferencePoint reference,
|
||
required int heading,
|
||
required OuterBoundary outer,
|
||
required Map<String, HoleBoundary> holes,
|
||
required int workType,
|
||
}) async {
|
||
//holes.asMap().forEach((index, hole) {
|
||
// final pointsStr = hole.position.map((p) => "(${p.lat.toStringAsFixed(6)}, ${p.lon.toStringAsFixed(6)})").join(', ');
|
||
// debugPrint('第${index + 1}组holes:$pointsStr');
|
||
//});
|
||
emit(state.copyWith(isLoading: true));
|
||
final result = await _generatePathUseCase.execute(reference: reference, heading: heading, outer: outer, holes: holes, workType: workType);
|
||
|
||
result.fold((failure) => emit(state.copyWith(errorMessage: failure.message)), (pathData) => emit(state.copyWith(generatedPath: pathData)));
|
||
}
|
||
|
||
// 开始路径规划
|
||
Future<void> startRoutePlanning(Queue<DeviceAddPathPointModel> locationQueue) async {
|
||
print("cubit层开始路径规划");
|
||
// 清空全局 Service 中的队列
|
||
// _routePlanningUseCase.clearLocationQueue();
|
||
final List<DeviceAddPathPointModel> pathList = locationQueue.toList();
|
||
print('✅ 队列转换为 List,长度:${pathList.length}');
|
||
|
||
_pathPlanningService.updateQueue(pathList);
|
||
print('✅ 队列更新成功');
|
||
|
||
// 开始路径规划
|
||
emit(state.copyWith(isLoading: true));
|
||
//传入全局的Service 中路径规划队列
|
||
Queue<DeviceAddPathPointModel> queue = _pathPlanningService.getQueue() ;
|
||
print('📦 从 Service 获取的队列长度:${queue.length}');
|
||
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));
|
||
final result = await _routePlanningUseCase.pauseRPWork();
|
||
result.fold((failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message ?? '暂停失败')), (_) => emit(state.copyWith(isLoading: false)));
|
||
}
|
||
|
||
// 恢复
|
||
Future<void> resumeRoutePlanning() async {
|
||
emit(state.copyWith(isLoading: true));
|
||
final result = await _routePlanningUseCase.resumeRPWork();
|
||
result.fold((failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message ?? '恢复失败')), (_) => emit(state.copyWith(isLoading: false)));
|
||
}
|
||
|
||
// 停止
|
||
Future<void> stopRoutePlanning() async {
|
||
emit(state.copyWith(isLoading: true));
|
||
final result = await _routePlanningUseCase.stopRoutePlanning();
|
||
result.fold((failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message ?? '停止失败')), (_) => emit(state.copyWith(isLoading: false)));
|
||
}
|
||
|
||
void updateAppState(AppState appState) {
|
||
emit(state.copyWith(appState: appState));
|
||
(_routePlanningUseCase as dynamic).repository.setAppState(appState);
|
||
}
|
||
|
||
void finishWork() {
|
||
emit(state.copyWith(isFinshWork: true));
|
||
}
|
||
|
||
/// 重置完成状态
|
||
void resetWorkStatus() {
|
||
emit(state.copyWith(isFinshWork: false));
|
||
}
|
||
void setArrivedLocation(double latitude, double longitude) {
|
||
emit(state.copyWith(arriLatitude: latitude, arriLongitude: longitude));
|
||
print('✅ [DevicesCubit] 更新已到达位置:Lat=$latitude, Lng=$longitude');
|
||
}
|
||
|
||
//获取已到达的点的经纬度
|
||
(double, double) getArrivedLocation() {
|
||
return (state.arriLatitude ?? 0.0, state.arriLongitude ?? 0.0);
|
||
}
|
||
|
||
// 重置已到达记录
|
||
void resetArrivedLocation() {
|
||
emit(state.copyWith(arriLatitude: null, arriLongitude: null));
|
||
print('🔄 [DevicesCubit] 已重置到达位置');
|
||
}
|
||
}
|