1,接收tcp推送消息和和接收tcp推送机器的状态推送消息。
2,集成选择作业列表中的一项接口。
This commit is contained in:
@@ -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()}');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
14
lib/features/devices/domain/entities/gps_entity.dart
Normal file
14
lib/features/devices/domain/entities/gps_entity.dart
Normal 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)';
|
||||
}
|
||||
113
lib/features/devices/domain/entities/running_status_entity.dart
Normal file
113
lib/features/devices/domain/entities/running_status_entity.dart
Normal 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,
|
||||
];
|
||||
}
|
||||
@@ -29,4 +29,8 @@ abstract class PathRepository {
|
||||
required String workName,
|
||||
});
|
||||
|
||||
/// 根据作业名查询路径记录(用于“选择一个路径”)
|
||||
Future<List<Map<String, dynamic>>> selectWorkRecordByName({
|
||||
required String workName,
|
||||
});
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user