完成页面对接tcp推送的设备状态数据

完成页面测试解析tcp推送的设备状态数据的UI展示
This commit is contained in:
2026-03-03 19:34:17 +08:00
parent 0a3a062c04
commit 34b4c2bcbc
12 changed files with 676 additions and 230 deletions

View File

@@ -17,7 +17,9 @@ import 'package:maibu_satabot_v2/features/remote_control/domain/usecase/diff_ste
import 'package:shared_preferences/shared_preferences.dart';
import '../../features/auth/data/datasources/auth_http_datasource.dart';
import '../../features/auth/data/datasources/auth_tcp_datasource.dart';
import '../../features/auth/data/datasources/impl/auth_http_datasource_impl.dart';
import '../../features/auth/data/datasources/impl/auth_tcp_datasource_impl.dart';
import '../../features/auth/data/repositories/auth_repository_impl.dart';
import '../../features/auth/domain/repositories/auth_repository.dart';
import '../../features/auth/domain/usecases/login_usecase.dart';
@@ -41,6 +43,8 @@ import '../../features/devices/domain/usecases/get_work_record_usecase.dart';
import '../../features/devices/domain/usecases/route_planning_usecase.dart';
import '../../features/devices/domain/usecases/save_work_record_usecase.dart';
import '../../features/devices/domain/usecases/select_work_record_usecase.dart';
import '../../features/devices/domain/usecases/switch_device_usecase.dart';
import '../../features/devices/presentation/bloc/device_status_bloc.dart';
import '../../features/remote_control/presentation/bloc/remote_control_cubit.dart';
import '../app/app_user_cubit.dart';
import '../network/dio_client.dart';
@@ -59,7 +63,9 @@ Future<void> init() async {
sl.registerLazySingleton<Dio>(() => DioClient.create());
/// 1.1.2 TcpClient:TCP客户端
sl.registerLazySingleton(() => TcpClient());
sl.registerLazySingleton(() => TcpClient( sl<UserStorage>(),
getUserDeviceUseCase: sl<GetUserDeviceUseCase>(),
switchDeviceUseCase: sl<SwitchDeviceUseCase>(),));
/// 1.1.3 NetMessageDispatcher:消息调度器,并将 TcpClient 注入给它
sl.registerLazySingleton(() => NetMessageDispatcher(sl()));
@@ -83,7 +89,7 @@ Future<void> init() async {
() => AuthHttpDataSourceImpl(sl()),
);
sl.registerLazySingleton<DeviceHttpDatasource>(
() => DeviceHttpDatasourceImpl(sl()),
() => DeviceHttpDatasourceImpl(sl(), sl<UserStorage>()), //
);
/// 3. 仓库 (Repository)
@@ -126,7 +132,16 @@ Future<void> init() async {
sl.registerLazySingleton(() => UnbindDeviceUseCase(sl()));
sl.registerLazySingleton(() => UpdateDevicenameUsecase(sl()));
sl.registerLazySingleton<SaveWorkRecordUseCase>(() => SaveWorkRecordUseCase(sl<PathRepository>()));
sl.registerLazySingleton<SwitchDeviceUseCase>(() => SwitchDeviceUseCase(sl()));
sl.registerLazySingleton<AuthTcpDatasource>(
() => AuthTcpDatasourceImpl(
sl<TcpClient>(),
sl<UserStorage>(),
getUserDeviceUseCase: sl<GetUserDeviceUseCase>(),
switchDeviceUseCase: sl<SwitchDeviceUseCase>(),
),
);
/// 5. 状态管理 (Cubit/Bloc)
@@ -136,6 +151,11 @@ Future<void> init() async {
() => DevicesCubit(sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(),sl(),sl()),
);
sl.registerFactory(() => RemoteControlCubit(sl()));
/* // 工厂模式(留存,每次获取新实例)
sl.registerFactory(() => DeviceStatusBloc(sl<NetMessageDispatcher>()));
*/
//单例模式(全局共享一个实例)
sl.registerLazySingleton(() => DeviceStatusBloc(sl<NetMessageDispatcher>()));
/// . 路径生成
sl.registerLazySingleton<PathHttpDatasource>(
@@ -162,6 +182,7 @@ Future<void> init() async {
sl<TcpClient>(),
sl<AppUserCubit>(),
sl<NetMessageDispatcher>(),
sl<AuthTcpDatasource>(),
),
);

View File

@@ -16,7 +16,19 @@ class NetMessageDispatcher {
/// 示例解析方法:将 0x02 指令解析为 String
Stream<String> onStringMessage() {
return onCommand(0x02).map((p) => utf8.decode(p.payload));
print("0x02--TCP拦截推送解析开始");
return onCommand(0x02).map((p) {
try {
// 尝试解码
final result = utf8.decode(p.payload, allowMalformed: true); // 允许乱码,防止报错中断流
print('✅ 解码 0x02 成功:$result'); // 🔥 关键日志:看这里打印了吗?
return result;
} catch (e) {
print('❌ 解码 0x02 失败:$e, 原始字节:${p.payload}');
return ''; // 返回空字符串,避免流中断
}
});
/* ///return onCommand(0x02).map((p) => utf8.decode(p.payload));*/
}
/// 示例解析方法:将 0x12 指令解析为 JSON 并转为模型

View File

@@ -6,9 +6,14 @@ import 'dart:typed_data';
import 'package:flutter/cupertino.dart';
import 'package:maibu_satabot_v2/features/devices/data/repositories/route_planning_repository_impl.dart';
import '../../../features/auth/data/datasources/auth_tcp_datasource.dart';
import '../../../features/devices/data/models/route_plan_send_entity.dart';
import '../../../features/devices/domain/entities/device_entity.dart';
import '../../../features/devices/domain/entities/gps_entity.dart';
import '../../../features/devices/domain/entities/running_status_entity.dart';
import '../../../features/devices/domain/usecases/get_user_device_usecase.dart';
import '../../../features/devices/domain/usecases/switch_device_usecase.dart';
import '../../storage/user_storage.dart';
import '../protocol_decoder.dart';
@@ -18,39 +23,111 @@ class TcpClient {
// 使用 StreamController 将原始数据流暴露给外部,以便多个 DataSource 监听
final _controller = StreamController<RawPacket>.broadcast();
Stream<RawPacket> get packetStream => _controller.stream;
final GetUserDeviceUseCase getUserDeviceUseCase;
final SwitchDeviceUseCase switchDeviceUseCase;
TcpClient(this._userStorage, {required this.getUserDeviceUseCase, required this.switchDeviceUseCase});
// 新增:心跳定时器
Timer? _heartbeatTimer;
Timer? _reconnectTimer; // 重连定时器
String? _lastHost;
int? _lastPort;
final UserStorage _userStorage;
bool get isConnected => _socket != null;
// 通过参数配置,不硬编码
Future<void> connect({required String host, required int port}) async {
debugPrint('🔌 [TCP] 开始连接:$host:$port'); // ✅ 必须看到这条
_lastHost = host;
_lastPort = port;
try {
_socket = await Socket.connect(
host,
port,
timeout: const Duration(seconds: 5),
);
debugPrint('✅ [TCP] 连接成功!'); // ✅ 必须看到这条
// 开始认证tcp
await _sendAuthPacket();
_socket!.listen((data) {
debugPrint('📥 [TCP] 收到原始数据:${data.length} 字节, 内容:$data');
// var packets = _decoder.decode(data);
// debugPrint('📦 [TCP] 解码成功,包数量:${packets.length}');
//for (var packet in packets) {
// // 🔥若收到服务端心跳(cmd == 0xFF),立即回复一个心跳包
// if (packet.command == 0xFF) {
// debugPrint('收到服务端心跳,自动回复...');
// sendHeartbeat(); // 回复 AB AA FF AA AB
// }
// }
// _controller.add(packet);
// }
try {
var packets = _decoder.decode(data);
debugPrint('📦 [TCP] 解码成功,包数量:${packets.length}');
for (var packet in packets) {
// 🔥若收到服务端心跳(cmd == 0xFF),立即回复一个心跳包
if (packet.command == 0xFF) {
debugPrint('收到服务端心跳,自动回复...');
sendHeartbeat(); // 回复 AB AA FF AA AB
if (!_controller.isClosed) {
_controller.add(packet);
debugPrint('➡️ [TCP] 已分发 CMD: 0x${packet.command.toRadixString(16)}');
}
_controller.add(packet);
if (packet.command == 0xFF) {
debugPrint('收到服务端心跳,自动回复...');
sendHeartbeat(); // 回复 AB AA FF AA AB
}
if (packet.command == 0x03) {
debugPrint('⚠️ 收到认证响应:${packet.payload}');
// 解析 payload 看是否有错误信息
}
}
} catch (e, stackTrace) {
debugPrint('❌ [TCP] 解码数据时发生异常:$e\n$stackTrace'); // 🔥 捕获解码异常
}
},
onDone: (){
// TODO: 断线重连
debugPrint('onDone❌ [TCP] 连接已断开!');
_handleDisconnect();
},
onError: (e) {
debugPrint('❌ [TCP] 发生错误:$e');
_handleDisconnect(); // 统一走重连逻辑,保护 Controller 不被关闭
},
onDone: disconnect,
onError: (e) => disconnect(),
);
} catch (e) {
rethrow; // 向上抛出连接异常
}
}
void _handleDisconnect() {
stopHeartbeat();
_socket = null;
// 注意:这里不要关闭 _controller!否则监听者会丢失数据流
// _controller?.close();
_scheduleReconnect();
}
// ✅ 新增:调度重连
Future<void> _scheduleReconnect() async {
if (_reconnectTimer != null) return;
debugPrint('⏳ 调度重连:Host=${_lastHost}, Port=${_lastPort}'); // ✅ 检查 Host/Port 是否为空
if (_lastHost == null || _lastPort == null) {
debugPrint('❌ 无法重连:Host 或 Port 为空!');
return;
}
_reconnectTimer = Timer(const Duration(seconds: 5), () {
_reconnectTimer = null;
debugPrint('⏰ 定时器触发,开始执行重连...');
connect(host: _lastHost!, port: _lastPort!);
});
await _sendAuthPacket();
}
/// 发送数据
void send(Map<String, dynamic> data) {
if (_socket == null) throw Exception("Socket not connected");
@@ -89,7 +166,7 @@ class TcpClient {
// 新增:启动心跳(每 4 秒发送一次 0xFF 指令)
void startHeartbeat({Duration interval = const Duration(seconds: 4)}) {
if (_heartbeatTimer != null) return; // 防止重复启动
debugPrint('收到服务端心跳,自动回复...');
_heartbeatTimer = Timer.periodic(interval, (_) {
// 发送心跳帧:AB AA FF 00 00 AA AB(与 sendRaw 一致)
//sendRaw(0xFF, []);
@@ -130,6 +207,88 @@ class TcpClient {
}
//
Future<void> _sendAuthPacket() async {
if (_socket == null) return;
String? username= "";
String? token= "";
final user = await _userStorage.getUser();
debugPrint('tcp使用用户信息:$user');
if (user == null || user.token == null) {
debugPrint('❌ [TCP] 认证失败:用户未登录或 Token 为空,无法发送认证包');
return; // 直接返回,不要发送无效包
}
username = user.username;
token = user.token;
final authString = '$username:app:$token';
debugPrint('🔑 [TCP] 认证字符串:$authString');
final authBytes = utf8.encode(authString);
// 构造包结构:Head(2) + Cmd(1) + Payload(N) + CRC(2) + Foot(2)
// 总长度 = 3 + N + 4
final builder = BytesBuilder()
..addByte(0xAB)
..addByte(0xAA)
..addByte(0x03) // 认证指令
..add(authBytes);
// 添加 CRC (这里简单用 0x00 0x00 占位,如果协议严格校验需计算真实 CRC)
// Android 代码中是 packet[tailStart] = 0x00; packet[tailStart+1] = 0x00;
builder.addByte(0x00);
builder.addByte(0x00);
builder.addByte(0xAA);
builder.addByte(0xAB);
_socket!.add(builder.takeBytes());
debugPrint('🔑 [TCP] 已发送认证包 (0x03): $authString');
try {
// 1. 获取 Either 结果
final eitherResult = await getUserDeviceUseCase.repository.getUserDevice(username);
// 2. 使用 fold 解包 Either
// left: 处理错误情况 (DeviceFailure)
// right: 处理成功情况 (List<DeviceEntity>)
await eitherResult.fold(
(failure) {
// 处理失败:打印日志或抛出异常
debugPrint('❌ [AuthTcp] 获取设备列表失败:$failure');
throw Exception('获取设备列表失败:$failure');
},
(devices) async {
// 处理成功:devices 现在是真正的 List<DeviceEntity>
if (devices.isEmpty) {
debugPrint('⚠️ [AuthTcp] 当前用户无可用设备,跳过切换步骤');
return;
}
// 取第一个设备
final DeviceEntity targetDevice = devices.first;
debugPrint('📱 [AuthTcp] 准备切换至默认设备:${targetDevice.deviceName}');
// 切换设备 (同样,如果 switchDeviceUseCase 也返回 Either,也需要 fold 处理)
final switchResult = await switchDeviceUseCase.deviceRepository.switchDevice("app",targetDevice.deviceName);
await switchResult.fold(
(failure) {
debugPrint('❌ [AuthTcp] 切换设备失败:$failure');
throw Exception('切换设备失败:$failure');
},
(success) {
debugPrint('✅ [AuthTcp] 设备切换成功,服务端应开始推送数据');
},
);
},
);
} catch (e) {
debugPrint('❌ [AuthTcp] 设备订阅流程异常:$e');
rethrow;
}
}
void sendRawBytes(Uint8List bytes) {
if (_socket == null) return;
_socket!.add(bytes);
}
}

View File

@@ -0,0 +1,6 @@
abstract class AuthTcpDatasource {
Future<void> sendAuthPacket();
}

View File

@@ -0,0 +1,108 @@
// lib/features/auth/data/datasources/auth_tcp_datasource.dart
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/cupertino.dart';
import 'package:fpdart/src/either.dart';
import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity.dart';
import 'package:maibu_satabot_v2/features/devices/domain/errors/device_failure.dart';
import '../../../../../core/network/tcp/tcp_client.dart';
import '../../../../../core/storage/user_storage.dart';
import '../../../../devices/domain/usecases/get_user_device_usecase.dart';
import '../../../../devices/domain/usecases/switch_device_usecase.dart';
import '../auth_tcp_datasource.dart';
class AuthTcpDatasourceImpl implements AuthTcpDatasource {
final TcpClient _tcpClient;
final UserStorage _userStorage;
final GetUserDeviceUseCase getUserDeviceUseCase;
final SwitchDeviceUseCase switchDeviceUseCase;
AuthTcpDatasourceImpl(this._tcpClient, this._userStorage, {required this.getUserDeviceUseCase, required this.switchDeviceUseCase});
@override
Future<void> sendAuthPacket() async {
if (!_tcpClient.isConnected) {
throw Exception('TCP 未连接,无法发送认证包');
}
final user = await _userStorage.getUser();
if (user == null || user.token == null || user.username == null) {
throw Exception('用户未登录,无法认证');
}
final username = user.username!;
final token = user.token!;
final authString = '$username:app:$token';
debugPrint('🔑 [AuthTcp] 准备发送认证包:$authString');
final authBytes = utf8.encode(authString);
// 构造协议包:AB AA 03 [Payload] CRC(00 00) AA AB
// 注意:如果服务端校验 CRC,需在此处计算真实 CRC
final builder = BytesBuilder()
..addByte(0xAB)
..addByte(0xAA)
..addByte(0x03)
..add(authBytes)
..addByte(0x00)
..addByte(0x00)
..addByte(0xAA)
..addByte(0xAB);
_tcpClient.sendRawBytes(builder.takeBytes()); // 假设 TcpClient 增加了此方法,或用 socket.add
debugPrint('✅ [AuthTcp] 认证包已发送');
// 获取用户设备列表
try {
// 1. 获取 Either 结果
final eitherResult = await getUserDeviceUseCase.repository.getUserDevice(username);
// 2. 使用 fold 解包 Either
// left: 处理错误情况 (DeviceFailure)
// right: 处理成功情况 (List<DeviceEntity>)
await eitherResult.fold(
(failure) {
// 处理失败:打印日志或抛出异常
debugPrint('❌ [AuthTcp] 获取设备列表失败:$failure');
throw Exception('获取设备列表失败:$failure');
},
(devices) async {
// 处理成功:devices 现在是真正的 List<DeviceEntity>
if (devices.isEmpty) {
debugPrint('⚠️ [AuthTcp] 当前用户无可用设备,跳过切换步骤');
return;
}
// 取第一个设备
final DeviceEntity targetDevice = devices.first;
debugPrint('📱 [AuthTcp] 准备切换至默认设备:${targetDevice.deviceName}');
// 切换设备 (同样,如果 switchDeviceUseCase 也返回 Either,也需要 fold 处理)
final switchResult = await switchDeviceUseCase.deviceRepository.switchDevice("app",targetDevice.deviceName);
await switchResult.fold(
(failure) {
debugPrint('❌ [AuthTcp] 切换设备失败:$failure');
throw Exception('切换设备失败:$failure');
},
(success) {
debugPrint('✅ [AuthTcp] 设备切换成功,服务端应开始推送数据');
},
);
},
);
} catch (e) {
debugPrint('❌ [AuthTcp] 设备订阅流程异常:$e');
rethrow;
}
}
}

View File

@@ -8,6 +8,10 @@ import 'package:maibu_satabot_v2/core/network/net_message_dispatcher.dart';
import '../../../../core/app/app_user_cubit.dart';
import '../../../../core/network/tcp/tcp_client.dart';
import '../../../../core/storage/user_storage.dart';
import '../../../devices/domain/usecases/get_user_device_usecase.dart';
import '../../../devices/domain/usecases/switch_device_usecase.dart';
import '../../data/datasources/auth_tcp_datasource.dart';
import '../../data/datasources/impl/auth_tcp_datasource_impl.dart';
import 'auth_state.dart';
/// emit做的事情:
@@ -19,9 +23,11 @@ class AuthCubit extends Cubit<AuthState> {
final TcpClient tcp;
final AppUserCubit appCubit;
final NetMessageDispatcher dispatcher;
final AuthTcpDatasource _authTcpDatasource;
StreamSubscription? _kickOutSub; // 新增:用于管理监听生命周期
AuthCubit(this.storage, this.tcp, this.appCubit, this.dispatcher)
AuthCubit(this.storage, this.tcp, this.appCubit, this.dispatcher, this._authTcpDatasource)
: super(AuthInitial()) {
// Cubit 一启动就开始监听 TCP 的“自动逻辑”
_listenToAuthResponse();
@@ -33,6 +39,7 @@ class AuthCubit extends Cubit<AuthState> {
if (user != null) {
// 1. 建立 TCP 连接 (远程控制)
tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT);
await _authTcpDatasource.sendAuthPacket();//包括发送认证包和获取列表和切换函数
// 2. 同步全局 App 状态
appCubit.setAuth(user);
// 3. 进入已登录状态
@@ -46,7 +53,8 @@ class AuthCubit extends Cubit<AuthState> {
Future<void> loginSuccess(UserEntity user) async {
await storage.saveUser(user);
await tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT);
tcp.startHeartbeat(interval: const Duration(seconds: 4)); // 👈 启动心跳
await _authTcpDatasource.sendAuthPacket();//包括发送认证包和获取列表和切换函数
tcp.startHeartbeat(interval: const Duration(seconds: 4)); //启动心跳
appCubit.setAuth(user);
emit(AuthAuthenticated(user));
}

View File

@@ -1,14 +1,25 @@
import 'package:dio/dio.dart';
import 'package:flutter/widgets.dart';
import 'package:maibu_satabot_v2/core/consts/http_api_consts.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/storage/user_storage.dart';
import '../../../domain/entities/device_location_entity.dart';
class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
final Dio dio;
final UserStorage _userStorage; // 🔥 新增字段
DeviceHttpDatasourceImpl(this.dio);
// 🔥 修改构造函数,注入 UserStorage
DeviceHttpDatasourceImpl(this.dio, this._userStorage);
// 🔥 辅助方法:获取 Token
Future<String?> _getToken() async {
final user = await _userStorage.getUser();
debugPrint('用户信息: $user') ;
return user?.token;
}
@override
Future<int> bindDevice(String deviceId, String deviceAlias) async {
@@ -31,9 +42,17 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
@override
Future<List<DeviceEntity>> getUserDevices(String username) async {
final token = await _getToken();
var response = await dio.get(
HttpApiConsts.getUserDevicesList,
queryParameters: {'tenantName': username},
options: Options(
headers: {
'Authorization': token != null ? 'Bearer $token' : '',
// 如果后端不需要 Bearer 前缀,直接写 token 即可
},
),
);
if (response.statusCode != 200) {
throw Exception('网络请求失败:${response.statusCode}');
@@ -58,9 +77,16 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
@override
Future<int> switchDevice(String platform, String deviceId) async {
final token = await _getToken();
var response = await dio.post(
HttpApiConsts.switchDevice,
data: {'platform': platform, 'deviceId': deviceId},
options: Options(
headers: {
'Authorization': token != null ? 'Bearer $token' : '',
// 如果后端不需要 Bearer 前缀,直接写 token 即可
},
),
);
if (response.statusCode != 200) {
throw Exception('网络请求失败:${response.statusCode}');

View File

@@ -1,29 +1,43 @@
// lib/features/devices/domain/entities/running_status_entity.dart
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;
// === 基础电气信息 (字段 0-6) ===
final double voltage; // [0] 电压 (V)
final double leftTargetSpeed; // [1] 左轮目标速度
final double rightTargetSpeed; // [2] 右轮目标速度
final double leftMeasureSpeed; // [3] 左轮实测速度
final double rightMeasureSpeed; // [4] 右轮实测速度
final double leftCurrent; // [5] 左轮电流 (A)
final double rightCurrent; // [6] 右轮电流 (A)
// === 温度信息 (字段 7-9) ===
final double leftMotorTemp; // [7] 左电机温度 (℃)
final double rightMotorTemp; // [8] 右电机温度 (℃)
final double chipTemp; // [9] 芯片温度 (℃)
// === 姿态信息 (字段 10-12) ===
final double yaw; // [10] 偏航角 (°)
final double pitch; // [11] 俯仰角 (°)
final double roll; // [12] 翻滚角 (°)
// === GPS 信号质量 (字段 13-15) ===
final int satelliteCnt; // [13] 卫星数量
final int qual; // [14] 定位质量
final int headingStatus; // [15] 航向状态
// === GPS 位置信息 (字段 16-17) ===
final double latitude; // [16] 纬度
final double longitude; // [17] 经度
// === 扩展信息 (字段 18-23) ===
final String timestamp; // [18] 时间戳 (格式:20-1-11-31 08:00:00)
final String knifeCuttingSpeed; // [19] 割刀速度
final String controlMode; // [20] 控制模式
final String battery; // [21] 电量百分比
final String workingArea; // [22] 工作面积
final String obstacleFlag; // [23] 障碍物标志
const RunningStatusEntity({
this.voltage = 0.0,
@@ -44,13 +58,61 @@ class RunningStatusEntity extends Equatable {
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",
this.timestamp = '',
this.knifeCuttingSpeed = '0',
this.controlMode = '0',
this.battery = '0',
this.workingArea = '0',
this.obstacleFlag = '0',
});
/// 从字段数组创建实体 (核心解析方法)
factory RunningStatusEntity.fromFields(List<String> fields) {
// 确保至少有 24 个字段,不足的用默认值填充
while (fields.length < 24) {
fields.add('');
}
return RunningStatusEntity(
// 基础电气信息
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,
// GPS 信号质量
satelliteCnt: int.tryParse(fields[13]) ?? 0,
qual: int.tryParse(fields[14]) ?? 0,
headingStatus: int.tryParse(fields[15]) ?? 0,
// GPS 位置信息
latitude: double.tryParse(fields[16]) ?? 0.0,
longitude: double.tryParse(fields[17]) ?? 0.0,
// 扩展信息 (保持原始字符串,由调用方决定如何解析)
timestamp: fields[18].trim(),
knifeCuttingSpeed: fields[19].trim().isNotEmpty ? fields[19].trim() : '0',
controlMode: fields[20].trim().isNotEmpty ? fields[20].trim() : '0',
battery: fields[21].trim().isNotEmpty ? fields[21].trim() : '0',
workingArea: fields[22].trim().isNotEmpty ? fields[22].trim() : '0',
obstacleFlag: fields[23].trim().isNotEmpty ? fields[23].trim() : '0',
);
}
/// copyWith 方法 (用于局部更新)
RunningStatusEntity copyWith({
double? voltage,
double? leftTargetSpeed,
@@ -70,6 +132,7 @@ class RunningStatusEntity extends Equatable {
int? headingStatus,
double? latitude,
double? longitude,
String? timestamp,
String? knifeCuttingSpeed,
String? controlMode,
String? battery,
@@ -95,6 +158,7 @@ class RunningStatusEntity extends Equatable {
headingStatus: headingStatus ?? this.headingStatus,
latitude: latitude ?? this.latitude,
longitude: longitude ?? this.longitude,
timestamp: timestamp ?? this.timestamp,
knifeCuttingSpeed: knifeCuttingSpeed ?? this.knifeCuttingSpeed,
controlMode: controlMode ?? this.controlMode,
battery: battery ?? this.battery,
@@ -105,9 +169,35 @@ class RunningStatusEntity extends Equatable {
@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,
voltage,
leftTargetSpeed,
rightTargetSpeed,
leftMeasureSpeed,
rightMeasureSpeed,
leftCurrent,
rightCurrent,
leftMotorTemp,
rightMotorTemp,
chipTemp,
yaw,
pitch,
roll,
satelliteCnt,
qual,
headingStatus,
latitude,
longitude,
timestamp,
knifeCuttingSpeed,
controlMode,
battery,
workingArea,
obstacleFlag,
];
@override
String toString() {
return 'RunningStatusEntity(voltage: $voltage, chipTemp: $chipTemp, '
'latitude: $latitude, longitude: $longitude, timestamp: $timestamp)';
}
}

View File

@@ -14,77 +14,82 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
final NetMessageDispatcher _dispatcher;
DeviceStatusBloc(this._dispatcher) : super(DeviceStatusInitial()) {
// 直接订阅 0x02 指令的字符串流
// 订阅 TCP 数据流
_dispatcher.onStringMessage().listen((jsonString) {
debugPrint('📩 Bloc 收到 0x02 数据');
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);
}
}
//
on<DeviceStatusLoaded>(_handleDeviceStatusLoaded);
on<PushMessageReceived>(_handlePushMessageReceived);
Stream<DeviceStatusState> _mapDeviceStatusLoaded(DeviceStatusLoaded event) async* {
// 🔥 新增:模拟测试数据(调试用)
_testMockData();
}
void _testMockData() {
// 用户提供的真实指令数据(字节数组)
final mockBytes = [
171, 170, 2, 48, 46, 48, 48, 44, 48, 44, 48, 44, 48, 44, 48, 44, 48, 46, 48, 48, 44, 48, 46, 48, 48, 44, 48, 46, 48, 48, 44, 48, 46, 48, 48, 44, 50, 55, 46, 52, 55, 44, 49, 56, 48, 46, 48, 48, 44, 48, 46, 48, 48, 44, 48, 46, 48, 48, 44, 52, 49, 44, 52, 44, 49, 44, 51, 50, 46, 48, 52, 49, 50, 57, 54, 56, 54, 52, 48, 48, 57, 57, 57, 44, 49, 50, 48, 46, 56, 48, 56, 57, 57, 51, 48, 56, 49, 49, 53, 56, 53, 48, 44, 50, 48, 45, 49, 45, 49, 49, 45, 51, 49, 32, 48, 56, 58, 48, 48, 58, 48, 48, 44, 48, 44, 51, 44, 54, 54, 44, 57, 53, 50, 55, 46, 48, 48, 48, 48, 44, 48, 0, 0, 0, 170, 171
];
// 解码为字符串(去掉帧头 3 字节和帧尾 5 字节)
final payloadBytes = mockBytes.sublist(3, mockBytes.length - 5);
final mockData = String.fromCharCodes(payloadBytes);
debugPrint('🧪 模拟测试数据:$mockData');
// 发送模拟事件
add(DeviceStatusLoaded(mockData));
}
// 🔥 修改:使用 Emitter 发送状态
Future<void> _handleDeviceStatusLoaded(
DeviceStatusLoaded event,
Emitter<DeviceStatusState> emit,
) async {
try {
final fields = event.jsonString.trim().split(' ');
debugPrint('🔍 开始解析数据:${event.jsonString}');
final fields = event.jsonString.trim().split(',');
debugPrint('🔍 字段数量:${fields.length}');
if (fields.length < 18) {
yield DeviceStatusError('字段不足,期望 ≥18,实际: ${fields.length}');
emit(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 status = RunningStatusEntity.fromFields(fields);
debugPrint('设备运行时候状态:$status');
final gps = GPSEntity(status.latitude, status.longitude);
yield DeviceStatusUpdated(status, gps);
debugPrint('GPS: ${gps.latitude}, ${gps.longitude}');
emit(DeviceStatusUpdated(status, gps)); // 🔥 使用 emit 发送状态
} catch (e) {
yield DeviceStatusError('解析设备状态失败: $e');
debugPrint('❌ 解析异常:$e');
emit(DeviceStatusError('解析设备运行时候状态失败:$e'));
}
}
/// 解析 推送消息
Stream<DeviceStatusState> _mapPushMessageReceived(PushMessageReceived event) async* {
Future<void> _handlePushMessageReceived(
PushMessageReceived event,
Emitter<DeviceStatusState> emit,
) async {
try {
final eventStr = event.jsonData['event'] ?? '';
final deviceId = event.jsonData['deviceId'] ?? '未知';
debugPrint('收到推送事件: $eventStr, 设备: $deviceId');
// 可扩展:yield PushMessageState(event.jsonData)
debugPrint('收到推送事件:$eventStr, 设备:$deviceId');
} catch (e) {
yield DeviceStatusError('解析推送消息失败: $e');
emit(DeviceStatusError('解析推送消息失败:$e'));
}
}
@override
Future<void> close() {
return super.close();
}
}

View File

@@ -21,3 +21,4 @@ class PushMessageReceived extends DeviceStatusEvent {
@override
List<Object?> get props => [jsonData];
}

View File

@@ -9,6 +9,8 @@ import '../../../../core/di/injection.dart';
import '../../../../core/network/net_message_dispatcher.dart';
import '../../../../core/network/protocol_decoder.dart';
import '../../../devices/presentation/bloc/devices_cubit.dart';
import '../../../devices/presentation/bloc/device_status_bloc.dart'; // 👈 新增导入
import '../../../devices/presentation/bloc/device_status_state.dart'; // 👈 新增导入
class RunningStatusPage extends StatefulWidget {
const RunningStatusPage({super.key});
@@ -18,90 +20,18 @@ class RunningStatusPage extends StatefulWidget {
}
class _RunningStatusPageState extends State<RunningStatusPage> {
final _dispatcher = sl<NetMessageDispatcher>();
late StreamSubscription<RawPacket> _sub;
// 实时设备状态数据
String _yaw = '--';
String _satelliteCnt = '--';
String _qual = '--';
String _leftSpeed = '--';
String _rightSpeed = '--';
String _voltage = '--';
// 实时设备状态数据
String _pitch = '--';
String _roll = '--';
String _latitude = '--';
String _longitude = '--';
String _timeStamp = '--';
String _knifeCuttingSpeed = '--';
String _controlMode = '--';
String _workingArea = '--';
String _battery = '--';
String _obstacleFlag = '--';
// 视图切换状态
bool _isCardView = true;
@override
void initState() {
super.initState();
_sub = _dispatcher.onCommand(0x02).listen(_onDeviceData);
}
void _onDeviceData(RawPacket packet) {
try {
final csv = utf8.decode(packet.payload);
final fields = csv.split(',');
// 至少需 16 个字段(索引 10=航向角, 13=卫星数, 14=定位质量)
if (fields.length >= 16) {
setState(() {
_voltage = fields[0]; // 电压
_leftSpeed = fields[1]; // 左轮目标速度
_rightSpeed = fields[2]; // 右轮目标速度
_pitch = fields[11]; // 俯仰角
_roll = fields[12]; // 翻滚角
_latitude = fields[16]; // 纬度
_longitude = fields[17]; // 经度
_timeStamp = '${fields[16]} ${fields[17]}'; // 时间戳(合并字段)
_knifeCuttingSpeed = fields[19]; // 割刀速度
_controlMode = fields[20]; // 控制模式
_workingArea = fields[21]; // 作业面积
_battery = fields[22]; // 电量
_obstacleFlag = fields[23]; // 障碍物标志
_yaw = fields[10]; // 航向角
_satelliteCnt = fields[13]; // 卫星数
_qual = fields[14]; // 定位质量
});
}
} catch (_) {
// 解析失败时忽略,避免崩溃
}
}
@override
void dispose() {
_sub.cancel();
super.dispose();
}
void _refreshDeviceData() {
final deviceState = context.read<DevicesCubit>().state;
final currentDevice = deviceState.selectedDevice;
if (currentDevice != null) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("数据刷新成功!"), duration: Duration(seconds: 1)));
} else {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("暂无设备,无法刷新"), duration: Duration(seconds: 1)));
}
}
@override
Widget build(BuildContext context) {
final deviceState = context.read<DevicesCubit>().state;
final currentDevice = deviceState.selectedDevice;
if (currentDevice == null) {
return const Scaffold(body: Center(child: Text("加载中...")));
return const Scaffold(
body: Center(child: Text("加载中...")),
);
}
return Scaffold(
@@ -125,20 +55,38 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
),
),
// 2. 状态信息行(动态更新)
SliverToBoxAdapter(
child: Container(
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("航向角状态: $_yaw°", style: const TextStyle(fontSize: 14)),
Text("定位质量: $_qual", style: const TextStyle(fontSize: 14)),
Text("卫星数: $_satelliteCnt", style: const TextStyle(fontSize: 14)),
],
),
),
// 2. 状态信息行(动态更新)- 使用 BlocBuilder 监听
BlocBuilder<DeviceStatusBloc, DeviceStatusState>(
builder: (context, state) {
String yaw = '--';
String qual = '--';
String satelliteCnt = '--';
if (state is DeviceStatusUpdated) {
yaw = state.status.yaw.toStringAsFixed(2);
qual = state.status.qual.toString();
satelliteCnt = state.status.satelliteCnt.toString();
} else if (state is DeviceStatusError) {
yaw = '错误';
qual = '-';
satelliteCnt = '-';
}
return SliverToBoxAdapter(
child: Container(
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("航向角状态:$yaw°", style: const TextStyle(fontSize: 14)),
Text("定位质量:$qual", style: const TextStyle(fontSize: 14)),
Text("卫星数:$satelliteCnt", style: const TextStyle(fontSize: 14)),
],
),
),
);
},
),
// 3. 选项卡 + 刷新按钮
@@ -180,12 +128,19 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
),
),
// 4. 内容区域
// 4. 内容区域 - 使用 BlocBuilder 监听
SliverFillRemaining(
child: Container(
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)),
child: _isCardView ? _buildCardContentView() : _buildChartContentView(),
child: BlocBuilder<DeviceStatusBloc, DeviceStatusState>(
builder: (context, state) {
return Container(
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: _isCardView ? _buildCardContentView(state) : _buildChartContentView(),
);
},
),
),
],
@@ -207,33 +162,89 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
);
}
Widget _buildCardContentView() {
return SingleChildScrollView(
// 添加 SingleChildScrollView 使内容可滚动
padding: const EdgeInsets.all(20.0),
Widget _buildCardItem(String title, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(title, style: const TextStyle(fontSize: 16, color: Color(0xFF333333))),
Text(value, style: const TextStyle(fontSize: 16, color: Color(0xFF666666))),
],
),
);
}
Widget _buildCardContentView(DeviceStatusState state) {
// 从 Bloc 状态中提取数据
String voltage = '--';
String leftSpeed = '--';
String rightSpeed = '--';
String yaw = '--';
String pitch = '--';
String roll = '--';
String satelliteCnt = '--';
String qual = '--';
String latitude = '--';
String longitude = '--';
String timeStamp = '--';
String knifeCuttingSpeed = '--';
String controlMode = '--';
String workingArea = '--';
String battery = '--';
String obstacleFlag = '--';
if (state is DeviceStatusUpdated) {
final status = state.status;
voltage = status.voltage.toStringAsFixed(2);
leftSpeed = status.leftTargetSpeed.toStringAsFixed(2);
rightSpeed = status.rightTargetSpeed.toStringAsFixed(2);
yaw = status.yaw.toStringAsFixed(2);
pitch = status.pitch.toStringAsFixed(2);
roll = status.roll.toStringAsFixed(2);
satelliteCnt = status.satelliteCnt.toString();
qual = status.qual.toString();
latitude = status.latitude.toStringAsFixed(8);
longitude = status.longitude.toStringAsFixed(8);
timeStamp = '${status.latitude} ${status.longitude}';
knifeCuttingSpeed = status.knifeCuttingSpeed;
controlMode = status.controlMode;
workingArea = status.workingArea;
battery = status.battery;
obstacleFlag = status.obstacleFlag;
} else if (state is DeviceStatusError) {
voltage = '错误';
}
return Padding(
padding: const EdgeInsets.all(20.0),child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("设备实时状态", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
_CardItem(title: "电压", value: "$_voltage V"),
_CardItem(title: "左轮速度", value: "$_leftSpeed rpm"),
_CardItem(title: "右轮速度", value: "$_rightSpeed rpm"),
_CardItem(title: "航向角", value: "$_yaw°"),
_CardItem(title: "俯仰角", value: "$_pitch°"),
_CardItem(title: "翻滚角", value: "$_roll°"),
_CardItem(title: "卫星数", value: _satelliteCnt),
_CardItem(title: "定位质量", value: _qual),
_CardItem(title: "纬度", value: _latitude),
_CardItem(title: "经度", value: _longitude),
_CardItem(title: "时间戳", value: _timeStamp),
_CardItem(title: "割刀速度", value: "$_knifeCuttingSpeed rpm"),
_CardItem(title: "控制模式", value: _controlMode),
_CardItem(title: "作业面积", value: "$_workingArea m²"),
_CardItem(title: "电量", value: "$_battery%"),
_CardItem(title: "障碍物标志", value: _obstacleFlag),
_buildCardItem("电压", "$voltage V"),
_buildCardItem("左轮速度", "$leftSpeed rpm"),
_buildCardItem( "右轮速度", "$rightSpeed rpm"),
_buildCardItem( "航向角", "$yaw°"),
_buildCardItem( "俯仰角", "$pitch°"),
_buildCardItem( "翻滚角", "$roll°"),
_buildCardItem( "卫星数", satelliteCnt),
_buildCardItem( "定位质量", qual),
_buildCardItem( "纬度", latitude),
_buildCardItem( "经度", longitude),
/* //_buildCardItem( "时间戳", timeStamp),*/
_buildCardItem( "割刀速度", "$knifeCuttingSpeed rpm"),
_buildCardItem( "控制模式", controlMode),
_buildCardItem( "作业面积", "$workingArea m²"),
_buildCardItem( "电量", "$battery%"),
_buildCardItem( "障碍物标志", obstacleFlag),
],
),
),
);
}
@@ -249,25 +260,19 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
),
);
}
}
class _CardItem extends StatelessWidget {
final String title;
final String value;
void _refreshDeviceData() {
final deviceState = context.read<DevicesCubit>().state;
final currentDevice = deviceState.selectedDevice;
const _CardItem({required this.title, required this.value});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(title, style: const TextStyle(fontSize: 16, color: Color(0xFF333333))),
Text(value, style: const TextStyle(fontSize: 16, color: Color(0xFF666666))),
],
),
);
if (currentDevice != null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("数据刷新成功!"), duration: Duration(seconds: 1)),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("暂无设备,无法刷新"), duration: Duration(seconds: 1)),
);
}
}
}

View File

@@ -4,6 +4,7 @@ import 'package:maibu_satabot_v2/features/home/presentation/pages/home_page.dart
import '../../../../core/di/injection.dart';
import '../../../../core/router/route_paths.dart';
import '../../../devices/presentation/bloc/device_status_bloc.dart';
import '../../../remote_control/presentation/bloc/remote_control_cubit.dart';
import '../../../remote_control/presentation/pages/remote_control_page.dart';
import '../pages/route_plan_page.dart';
@@ -27,7 +28,11 @@ class HomeRoutes {
),
GoRoute(
path: RoutePaths.runningStatus,
builder: (context, state) => const RunningStatusPage(),
// builder: (context, state) => const RunningStatusPage(),
builder: (context, state) => BlocProvider(
create: (_) => sl<DeviceStatusBloc>(), // 从 GetIt 获取单例
child: const RunningStatusPage(),
),
),
];