完成页面对接tcp推送的设备状态数据
完成页面测试解析tcp推送的设备状态数据的UI展示
This commit is contained in:
@@ -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>(),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -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 并转为模型
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
|
||||
abstract class AuthTcpDatasource {
|
||||
|
||||
Future<void> sendAuthPacket();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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}');
|
||||
|
||||
@@ -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)';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,3 +21,4 @@ class PushMessageReceived extends DeviceStatusEvent {
|
||||
@override
|
||||
List<Object?> get props => [jsonData];
|
||||
}
|
||||
|
||||
|
||||
@@ -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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user