1,接收tcp推送消息和和接收tcp推送机器的状态推送消息。

2,集成选择作业列表中的一项接口。
This commit is contained in:
2026-02-27 15:33:59 +08:00
parent 5170eeb4e7
commit f6167d3d51
11 changed files with 340 additions and 45 deletions

View File

@@ -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<String, dynamic>;
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()}');
}
}

View File

@@ -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<List<DeviceAddPathPointModel>> generatePath({
@@ -115,6 +115,35 @@ class PathRepositoryImpl implements PathRepository {
throw Exception('Network error in deleteWorkRecord: $e');
}
}
/// 选择工作记录
@override
Future<List<Map<String, dynamic>>> 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<String, dynamic>;
if (data['code'] == 200 && data.containsKey('data')) {
final List<dynamic> records = data['data'];
return List<Map<String, dynamic>>.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');
}
}

View File

@@ -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<Object?> get props => [latitude, longitude];
@override
String toString() => 'GPSEntity(lat: $latitude, lng: $longitude)';
}

View File

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

View File

@@ -29,4 +29,8 @@ abstract class PathRepository {
required String workName,
});
/// 根据作业名查询路径记录(用于“选择一个路径”)
Future<List<Map<String, dynamic>>> selectWorkRecordByName({
required String workName,
});
}

View File

@@ -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<Either<DeviceFailure, List<Map<String, dynamic>>>> call(String workName) async {
try {
final data = await repository.selectWorkRecordByName(workName: workName);
return Right(data);
} catch (e) {
return Left(DeviceFailure.networkError(message: e.toString()));
}
}
}

View File

@@ -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<DeviceStatusEvent, DeviceStatusState> {
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<DeviceStatusState> mapEventToState(DeviceStatusEvent event) async* {
if (event is DeviceStatusLoaded) {
yield* _mapDeviceStatusLoaded(event);
} else if (event is PushMessageReceived) {
yield* _mapPushMessageReceived(event);
}
}
Stream<DeviceStatusState> _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<DeviceStatusState> _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');
}
}
}

View File

@@ -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<Object?> get props => [jsonString];
}
// 改为接收 Map(不是 RawPacket)
class PushMessageReceived extends DeviceStatusEvent {
final Map<String, dynamic> jsonData;
PushMessageReceived(this.jsonData);
@override
List<Object?> get props => [jsonData];
}

View File

@@ -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<Object?> get props => [];
}
class DeviceStatusUpdated extends DeviceStatusState {
final RunningStatusEntity status;
final GPSEntity gps;
DeviceStatusUpdated(this.status, this.gps);
@override
List<Object?> get props => [status, gps];
}
class DeviceStatusError extends DeviceStatusState {
final String message;
DeviceStatusError(this.message);
@override
List<Object?> get props => [message];
}

View File

@@ -9,6 +9,9 @@ class DevicesState extends Equatable {
final double? deviceLatitude; // 新增字段
final double? deviceLongitude; // 新增字段
final List<Map<String, dynamic>>? workRecords;
final List<Map<String, dynamic>>? pathData;
const DevicesState({
this.devices = const [],
this.selectedDevice,
@@ -16,7 +19,8 @@ class DevicesState extends Equatable {
this.errorMessage,
this.deviceLatitude,
this.deviceLongitude,
this.workRecords
this.workRecords,
this.pathData,
});
// 使用 copyWith 方便局部更新状态
@@ -28,6 +32,7 @@ class DevicesState extends Equatable {
double? deviceLatitude,
double? deviceLongitude,
List<Map<String, dynamic>>? workRecords,
List<Map<String, dynamic>>? pathData,
}) {
return DevicesState(
devices: devices ?? this.devices,
@@ -36,6 +41,7 @@ class DevicesState extends Equatable {
errorMessage: errorMessage, // 错误信息通常每次更新都要重新赋值或清空
deviceLatitude: deviceLatitude ?? this.deviceLatitude,
deviceLongitude: deviceLongitude ?? this.deviceLongitude,
pathData: pathData ?? this.pathData,
workRecords: workRecords ?? this.workRecords,
);
}
@@ -46,5 +52,6 @@ class DevicesState extends Equatable {
deviceLatitude,
deviceLongitude,
workRecords,
pathData,
];
}

View File

@@ -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<DeviceStatusBloc>(create: (_) => sl<DeviceStatusBloc>()),
// 其他 Cubit...
],
child: MaterialApp.router(
title: 'Maibu Satabot',