diff --git a/lib/core/network/tcp/tcp_client.dart b/lib/core/network/tcp/tcp_client.dart index e24c165b..9b8f669b 100644 --- a/lib/core/network/tcp/tcp_client.dart +++ b/lib/core/network/tcp/tcp_client.dart @@ -4,7 +4,11 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/cupertino.dart'; -import 'package:maibu_satabot_v2/core/network/protocol_decoder.dart'; + +import '../../../features/devices/domain/entities/gps_entity.dart'; +import '../../../features/devices/domain/entities/running_status_entity.dart'; +import '../protocol_decoder.dart'; + class TcpClient { Socket? _socket; @@ -30,18 +34,11 @@ class TcpClient { _socket!.listen((data) { var packets = _decoder.decode(data); for (var packet in packets) { - // 🔥 关键修改:若收到服务端心跳(cmd == 0xFF),立即回复一个心跳包 + // 🔥若收到服务端心跳(cmd == 0xFF),立即回复一个心跳包 if (packet.command == 0xFF) { debugPrint('收到服务端心跳,自动回复...'); sendHeartbeat(); // 回复 AB AA FF AA AB } - - // 🔥 处理推送消息 - if (isPushMsg(packet)) { - debugPrint('收到推送消息,开始处理...'); - handlePushMessage(packet); // 自定义处理逻辑 - } - _controller.add(packet); } }, @@ -115,41 +112,7 @@ class TcpClient { } -/// 判断是否是推送消息 - bool isPushMsg(RawPacket packet) { - return packet.command == 0x12; - } - /// 处理推送消息 - /// 处理推送消息 - void handlePushMessage(RawPacket packet) { - try { - // 打印详细信息 - debugPrint('收到推送消息:命令码=${packet.command}, 时间=${DateTime.now()}'); - // 解码 payload 为字符串 - final jsonString = utf8.decode(packet.payload); - debugPrint('推送内容:$jsonString'); - - // 如果是 JSON 格式,进一步解析 - final jsonData = jsonDecode(jsonString) as Map; - if (jsonData.containsKey('event')) { - final event = jsonData['event']; - final deviceId = jsonData['deviceId'] ?? '未知设备'; - final status = jsonData['status'] ?? '未知状态'; - - debugPrint('事件类型:$event, 设备ID:$deviceId, 状态:$status'); - - // 根据事件类型处理逻辑 - if (event == 'device_status_changed') { - // 示例:更新设备状态 - debugPrint('设备状态变更:$deviceId -> $status'); - } - } - } catch (e) { - // 异常处理 - debugPrint('推送消息解析失败:${e.toString()}'); - } - } diff --git a/lib/features/devices/data/repositories/generate_path_repository_Impl.dart b/lib/features/devices/data/repositories/generate_path_repository_Impl.dart index 62c1414a..f5b8dbae 100644 --- a/lib/features/devices/data/repositories/generate_path_repository_Impl.dart +++ b/lib/features/devices/data/repositories/generate_path_repository_Impl.dart @@ -6,7 +6,7 @@ import '../models/device_add_path_point_model.dart'; import '../models/device_work_area_param_model.dart'; class PathRepositoryImpl implements PathRepository { - final String baseUrl = 'https://your-backend-api.com'; // 后端接口地址 + final String baseUrl = 'https://serviceri.satabot.com'; // 后端接口地址 // 生成路径 @override Future> generatePath({ @@ -115,6 +115,35 @@ class PathRepositoryImpl implements PathRepository { throw Exception('Network error in deleteWorkRecord: $e'); } } + /// 选择工作记录 + @override + Future>> selectWorkRecordByName({required String workName}) async { + final timestamp = DateTime.now().millisecondsSinceEpoch; + final url = Uri.parse('https://serviceri.satabot.com/iot/workRecord/selectByWorkname') + .replace(queryParameters: { + 'workName': workName, + '_t': timestamp.toString(), // 防缓存 + }); + + try { + final response = await http.get(url); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body) as Map; + + if (data['code'] == 200 && data.containsKey('data')) { + final List records = data['data']; + return List>.from(records); + } else { + throw Exception('API error: ${data['msg'] ?? 'Unknown'}'); + } + } else { + throw Exception('HTTP ${response.statusCode}: ${response.reasonPhrase}'); + } + } catch (e) { + throw Exception('Network error in selectWorkRecordByName: $e'); + } + } diff --git a/lib/features/devices/domain/entities/gps_entity.dart b/lib/features/devices/domain/entities/gps_entity.dart new file mode 100644 index 00000000..7e03b7aa --- /dev/null +++ b/lib/features/devices/domain/entities/gps_entity.dart @@ -0,0 +1,14 @@ +import 'package:equatable/equatable.dart'; + +class GPSEntity extends Equatable { + final double latitude; + final double longitude; + + const GPSEntity(this.latitude, this.longitude); + + @override + List get props => [latitude, longitude]; + + @override + String toString() => 'GPSEntity(lat: $latitude, lng: $longitude)'; +} diff --git a/lib/features/devices/domain/entities/running_status_entity.dart b/lib/features/devices/domain/entities/running_status_entity.dart new file mode 100644 index 00000000..97347197 --- /dev/null +++ b/lib/features/devices/domain/entities/running_status_entity.dart @@ -0,0 +1,113 @@ +import 'package:equatable/equatable.dart'; + +class RunningStatusEntity extends Equatable { + final double voltage; + final double leftTargetSpeed; + final double rightTargetSpeed; + final double leftMeasureSpeed; + final double rightMeasureSpeed; + final double leftCurrent; + final double rightCurrent; + final double leftMotorTemp; + final double rightMotorTemp; + final double chipTemp; + final double yaw; + final double pitch; + final double roll; + final int satelliteCnt; + final int qual; + final int headingStatus; + final double latitude; + final double longitude; + final String knifeCuttingSpeed; + final String controlMode; + final String battery; + final String workingArea; + final String obstacleFlag; + + const RunningStatusEntity({ + this.voltage = 0.0, + this.leftTargetSpeed = 0.0, + this.rightTargetSpeed = 0.0, + this.leftMeasureSpeed = 0.0, + this.rightMeasureSpeed = 0.0, + this.leftCurrent = 0.0, + this.rightCurrent = 0.0, + this.leftMotorTemp = 0.0, + this.rightMotorTemp = 0.0, + this.chipTemp = 0.0, + this.yaw = 0.0, + this.pitch = 0.0, + this.roll = 0.0, + this.satelliteCnt = 0, + this.qual = 0, + this.headingStatus = 0, + this.latitude = 0.0, + this.longitude = 0.0, + this.knifeCuttingSpeed = "0", + this.controlMode = "0", + this.battery = "0", + this.workingArea = "0", + this.obstacleFlag = "0", + }); + + RunningStatusEntity copyWith({ + double? voltage, + double? leftTargetSpeed, + double? rightTargetSpeed, + double? leftMeasureSpeed, + double? rightMeasureSpeed, + double? leftCurrent, + double? rightCurrent, + double? leftMotorTemp, + double? rightMotorTemp, + double? chipTemp, + double? yaw, + double? pitch, + double? roll, + int? satelliteCnt, + int? qual, + int? headingStatus, + double? latitude, + double? longitude, + String? knifeCuttingSpeed, + String? controlMode, + String? battery, + String? workingArea, + String? obstacleFlag, + }) { + return RunningStatusEntity( + voltage: voltage ?? this.voltage, + leftTargetSpeed: leftTargetSpeed ?? this.leftTargetSpeed, + rightTargetSpeed: rightTargetSpeed ?? this.rightTargetSpeed, + leftMeasureSpeed: leftMeasureSpeed ?? this.leftMeasureSpeed, + rightMeasureSpeed: rightMeasureSpeed ?? this.rightMeasureSpeed, + leftCurrent: leftCurrent ?? this.leftCurrent, + rightCurrent: rightCurrent ?? this.rightCurrent, + leftMotorTemp: leftMotorTemp ?? this.leftMotorTemp, + rightMotorTemp: rightMotorTemp ?? this.rightMotorTemp, + chipTemp: chipTemp ?? this.chipTemp, + yaw: yaw ?? this.yaw, + pitch: pitch ?? this.pitch, + roll: roll ?? this.roll, + satelliteCnt: satelliteCnt ?? this.satelliteCnt, + qual: qual ?? this.qual, + headingStatus: headingStatus ?? this.headingStatus, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + knifeCuttingSpeed: knifeCuttingSpeed ?? this.knifeCuttingSpeed, + controlMode: controlMode ?? this.controlMode, + battery: battery ?? this.battery, + workingArea: workingArea ?? this.workingArea, + obstacleFlag: obstacleFlag ?? this.obstacleFlag, + ); + } + + @override + List get props => [ + voltage, leftTargetSpeed, rightTargetSpeed, leftMeasureSpeed, rightMeasureSpeed, + leftCurrent, rightCurrent, leftMotorTemp, rightMotorTemp, chipTemp, + yaw, pitch, roll, satelliteCnt, qual, headingStatus, + latitude, longitude, knifeCuttingSpeed, controlMode, battery, workingArea, obstacleFlag, + ]; +} diff --git a/lib/features/devices/domain/repositories/path_repository.dart b/lib/features/devices/domain/repositories/path_repository.dart index aaa1c673..e1f8048a 100644 --- a/lib/features/devices/domain/repositories/path_repository.dart +++ b/lib/features/devices/domain/repositories/path_repository.dart @@ -29,4 +29,8 @@ abstract class PathRepository { required String workName, }); + /// 根据作业名查询路径记录(用于“选择一个路径”) + Future>> selectWorkRecordByName({ + required String workName, + }); } \ No newline at end of file diff --git a/lib/features/devices/domain/usecases/select_work_record_usecase.dart b/lib/features/devices/domain/usecases/select_work_record_usecase.dart new file mode 100644 index 00000000..109834db --- /dev/null +++ b/lib/features/devices/domain/usecases/select_work_record_usecase.dart @@ -0,0 +1,19 @@ +// lib/features/devices/domain/usecases/select_work_record_usecase.dart +import 'package:fpdart/fpdart.dart'; +import '../../domain/errors/device_failure.dart'; +import '../../domain/repositories/path_repository.dart'; + +class SelectWorkRecordUseCase { + final PathRepository repository; + + SelectWorkRecordUseCase(this.repository); + + Future>>> call(String workName) async { + try { + final data = await repository.selectWorkRecordByName(workName: workName); + return Right(data); + } catch (e) { + return Left(DeviceFailure.networkError(message: e.toString())); + } + } +} diff --git a/lib/features/devices/presentation/bloc/device_status_bloc.dart b/lib/features/devices/presentation/bloc/device_status_bloc.dart new file mode 100644 index 00000000..6e33d4dd --- /dev/null +++ b/lib/features/devices/presentation/bloc/device_status_bloc.dart @@ -0,0 +1,90 @@ + +import 'dart:convert'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:maibu_satabot_v2/features/devices/domain/entities/running_status_entity.dart'; +import 'package:maibu_satabot_v2/features/devices/domain/entities/gps_entity.dart'; +import 'package:maibu_satabot_v2/core/network/protocol_decoder.dart'; // 确保能访问 RawPacket + +import '../../../../core/network/net_message_dispatcher.dart'; +import 'device_status_event.dart'; +import 'device_status_state.dart'; + +class DeviceStatusBloc extends Bloc { + final NetMessageDispatcher _dispatcher; + + DeviceStatusBloc(this._dispatcher) : super(DeviceStatusInitial()) { + // 直接订阅 0x02 指令的字符串流 + _dispatcher.onStringMessage().listen((jsonString) { + add(DeviceStatusLoaded(jsonString)); + }); + + // 订阅 0x12 的 JSON 流() + _dispatcher.onJsonMessage(0x12).listen((jsonData) { + add(PushMessageReceived(jsonData)); + }); + } + + @override + Stream mapEventToState(DeviceStatusEvent event) async* { + if (event is DeviceStatusLoaded) { + yield* _mapDeviceStatusLoaded(event); + } else if (event is PushMessageReceived) { + yield* _mapPushMessageReceived(event); + } + } + + Stream _mapDeviceStatusLoaded(DeviceStatusLoaded event) async* { + try { + final fields = event.jsonString.trim().split(' '); + if (fields.length < 18) { + yield DeviceStatusError('字段不足,期望 ≥18,实际: ${fields.length}'); + return; + } + ///解析设备状态 + final status = RunningStatusEntity().copyWith( + voltage: double.tryParse(fields[0]) ?? 0.0, + leftTargetSpeed: double.tryParse(fields[1]) ?? 0.0, + rightTargetSpeed: double.tryParse(fields[2]) ?? 0.0, + leftMeasureSpeed: double.tryParse(fields[3]) ?? 0.0, + rightMeasureSpeed: double.tryParse(fields[4]) ?? 0.0, + leftCurrent: double.tryParse(fields[5]) ?? 0.0, + rightCurrent: double.tryParse(fields[6]) ?? 0.0, + leftMotorTemp: double.tryParse(fields[7]) ?? 0.0, + rightMotorTemp: double.tryParse(fields[8]) ?? 0.0, + chipTemp: double.tryParse(fields[9]) ?? 0.0, + yaw: double.tryParse(fields[10]) ?? 0.0, + pitch: double.tryParse(fields[11]) ?? 0.0, + roll: double.tryParse(fields[12]) ?? 0.0, + satelliteCnt: int.tryParse(fields[13]) ?? 0, + qual: int.tryParse(fields[14]) ?? 0, + headingStatus: int.tryParse(fields[15]) ?? 0, + latitude: double.tryParse(fields[16]) ?? 0.0, + longitude: double.tryParse(fields[17]) ?? 0.0, + knifeCuttingSpeed: fields.length > 19 && fields[19].isNotEmpty ? fields[19] : "0", + controlMode: fields.length > 20 && fields[20].isNotEmpty ? fields[20] : "0", + battery: fields.length > 21 && fields[21].isNotEmpty ? fields[21] : "0", + workingArea: fields.length > 22 && fields[22].isNotEmpty ? fields[22] : "0", + obstacleFlag: fields.length > 23 && fields[23].isNotEmpty ? fields[23] : "0", + ); + ///解析GPS + final gps = GPSEntity(status.latitude, status.longitude); + yield DeviceStatusUpdated(status, gps); + } catch (e) { + yield DeviceStatusError('解析设备状态失败: $e'); + } + } +/// 解析 推送消息 + Stream _mapPushMessageReceived(PushMessageReceived event) async* { + try { + final eventStr = event.jsonData['event'] ?? ''; + final deviceId = event.jsonData['deviceId'] ?? '未知'; + debugPrint('收到推送事件: $eventStr, 设备: $deviceId'); + + // 可扩展:yield PushMessageState(event.jsonData) + } catch (e) { + yield DeviceStatusError('解析推送消息失败: $e'); + } + } + +} diff --git a/lib/features/devices/presentation/bloc/device_status_event.dart b/lib/features/devices/presentation/bloc/device_status_event.dart new file mode 100644 index 00000000..2ac964c9 --- /dev/null +++ b/lib/features/devices/presentation/bloc/device_status_event.dart @@ -0,0 +1,23 @@ + +// lib/features/devices/presentation/bloc/device_status_event.dart +import 'package:equatable/equatable.dart'; + +abstract class DeviceStatusEvent extends Equatable { + const DeviceStatusEvent(); +} + +// 改为接收 String(不是 RawPacket) +class DeviceStatusLoaded extends DeviceStatusEvent { + final String jsonString; // ← 关键:改为 String + DeviceStatusLoaded(this.jsonString); + @override + List get props => [jsonString]; +} + +// 改为接收 Map(不是 RawPacket) +class PushMessageReceived extends DeviceStatusEvent { + final Map jsonData; + PushMessageReceived(this.jsonData); + @override + List get props => [jsonData]; +} diff --git a/lib/features/devices/presentation/bloc/device_status_state.dart b/lib/features/devices/presentation/bloc/device_status_state.dart new file mode 100644 index 00000000..90fed225 --- /dev/null +++ b/lib/features/devices/presentation/bloc/device_status_state.dart @@ -0,0 +1,30 @@ + +import 'package:equatable/equatable.dart'; + +import '../../domain/entities/gps_entity.dart'; +import '../../domain/entities/running_status_entity.dart'; + +abstract class DeviceStatusState extends Equatable { + const DeviceStatusState(); +} + +class DeviceStatusInitial extends DeviceStatusState { + @override + List get props => []; +} + +class DeviceStatusUpdated extends DeviceStatusState { + final RunningStatusEntity status; + final GPSEntity gps; + + DeviceStatusUpdated(this.status, this.gps); + @override + List get props => [status, gps]; +} + +class DeviceStatusError extends DeviceStatusState { + final String message; + DeviceStatusError(this.message); + @override + List get props => [message]; +} diff --git a/lib/features/devices/presentation/bloc/devices_state.dart b/lib/features/devices/presentation/bloc/devices_state.dart index 0e4d4795..1fdb9a48 100644 --- a/lib/features/devices/presentation/bloc/devices_state.dart +++ b/lib/features/devices/presentation/bloc/devices_state.dart @@ -16,6 +16,9 @@ class DevicesState extends Equatable { final double? deviceLongitude; // 新增字段 final List>? workRecords; final DeviceOperationType operationType; // 标记当前操作类型 + final List>? pathData; + + const DevicesState({ this.devices = const [], this.selectedDevice, @@ -25,6 +28,7 @@ class DevicesState extends Equatable { this.deviceLongitude, this.workRecords, this.operationType = DeviceOperationType.none, // 默认无操作 + this.pathData, }); // 使用 copyWith 方便局部更新状态 @@ -37,6 +41,7 @@ class DevicesState extends Equatable { double? deviceLongitude, List>? workRecords, DeviceOperationType? operationType, + List>? pathData, }) { return DevicesState( devices: devices ?? this.devices, @@ -45,6 +50,7 @@ class DevicesState extends Equatable { errorMessage: errorMessage, // 错误信息通常每次更新都要重新赋值或清空 deviceLatitude: deviceLatitude ?? this.deviceLatitude, deviceLongitude: deviceLongitude ?? this.deviceLongitude, + pathData: pathData ?? this.pathData, workRecords: workRecords ?? this.workRecords, operationType: operationType ?? this.operationType, // 更新操作类型 ); @@ -56,5 +62,6 @@ class DevicesState extends Equatable { deviceLatitude, deviceLongitude, workRecords, + pathData, ]; } diff --git a/lib/main.dart b/lib/main.dart index c23fe95b..2f130e47 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,6 +13,7 @@ import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_cubi import 'core/di/injection.dart'; import 'features/auth/presentation/bloc/login_cubit.dart'; +import 'features/devices/presentation/bloc/device_status_bloc.dart'; void main() async { // 1. 使用 runZonedGuarded 捕获所有未处理的异步错误 @@ -65,6 +66,8 @@ class MyApp extends StatelessWidget { return devicesCubit; }, ), + BlocProvider(create: (_) => sl()), + // 其他 Cubit... ], child: MaterialApp.router( title: 'Maibu Satabot',