完成修复登录缓慢问题-主要接口响应时间决定
完成切换设备机器状态的实时接受和UI实时刷新 完成优化tcp重连的体检,手动切换设备或自动断开后重连的区分
This commit is contained in:
@@ -164,9 +164,10 @@ Future<void> init() async {
|
||||
/// 5. 状态管理 (Cubit/Bloc)
|
||||
sl.registerLazySingleton(() => AppUserCubit()); // AuthCubit 依赖它,必须先注册
|
||||
sl.registerLazySingleton(() => GetDeviceLocationUseCase(sl()));
|
||||
sl.registerLazySingleton(() => DevicesCubit(sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl()));
|
||||
sl.registerLazySingleton(() => DevicesCubit(sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(),sl(),sl()));
|
||||
sl.registerFactory(() => RemoteControlCubit(sl()));
|
||||
/* // 工厂模式(留存,每次获取新实例)
|
||||
|
||||
sl.registerFactory(() => DeviceStatusBloc(sl<NetMessageDispatcher>()));
|
||||
*/
|
||||
//单例模式(全局共享一个实例)
|
||||
|
||||
@@ -33,11 +33,28 @@ class TcpClient {
|
||||
|
||||
String? _lastHost;
|
||||
int? _lastPort;
|
||||
bool isUserSwitch = false; // 用户是否切换设备
|
||||
bool _isSwitching = false;
|
||||
|
||||
final UserStorage _userStorage;
|
||||
|
||||
bool get isConnected => _socket != null;
|
||||
void disconnects({bool forSwitch = false}) {
|
||||
if (forSwitch) {
|
||||
_isSwitching = true;
|
||||
debugPrint('🚫 [TCP] 标记为切换断开,将禁止自动重连');
|
||||
}
|
||||
|
||||
// 🔥 关键:立即取消可能已经存在或即将触发的重连定时器
|
||||
_reconnectTimer?.cancel();
|
||||
_heartbeatTimer?.cancel();
|
||||
|
||||
if (_socket != null) {
|
||||
debugPrint('🔌 [TCP] 物理断开 Socket...');
|
||||
_socket!.destroy(); // 或者 .close()
|
||||
_socket = null;
|
||||
}
|
||||
}
|
||||
// 通过参数配置,不硬编码
|
||||
Future<void> connect({required String host, required int port}) async {
|
||||
debugPrint('🔌 [TCP] 开始连接:$host:$port'); // ✅ 必须看到这条
|
||||
@@ -89,19 +106,29 @@ class TcpClient {
|
||||
},
|
||||
onDone: (){
|
||||
// TODO: 断线重连
|
||||
debugPrint('onDone❌ [TCP] 连接已断开!');
|
||||
debugPrint('来到断线重连!');
|
||||
if (!_isSwitching) {
|
||||
debugPrint('onDone❌ [TCP] 连接已断开!');
|
||||
_handleDisconnect();
|
||||
}
|
||||
_isSwitching = false; // 重置标志,以免影响下次
|
||||
|
||||
},
|
||||
onError: (e) {
|
||||
debugPrint('❌ [TCP] 发生错误:$e');
|
||||
_handleDisconnect(); // 统一走重连逻辑,保护 Controller 不被关闭
|
||||
debugPrint('来到断线重连!error');
|
||||
if (!_isSwitching) {
|
||||
debugPrint('❌ [TCP] 发生错误:$e');
|
||||
_handleDisconnect();
|
||||
} // 统一走重连逻辑,保护 Controller 不被关闭
|
||||
_isSwitching = false; // 重置标志,以免影响下次
|
||||
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
rethrow; // 向上抛出连接异常
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
void _handleDisconnect() {
|
||||
stopHeartbeat();
|
||||
|
||||
@@ -113,6 +140,7 @@ class TcpClient {
|
||||
|
||||
// ✅ 新增:调度重连
|
||||
Future<void> _scheduleReconnect() async {
|
||||
if (isUserSwitch) return;
|
||||
if (_reconnectTimer != null) return;
|
||||
|
||||
debugPrint('⏳ 调度重连:Host=${_lastHost}, Port=${_lastPort}'); // ✅ 检查 Host/Port 是否为空
|
||||
@@ -128,6 +156,25 @@ class TcpClient {
|
||||
});
|
||||
await _sendAuthPacket();
|
||||
}
|
||||
|
||||
|
||||
// ✅ 新增:调度重连
|
||||
Future<void> _scheduleReconnectBySwitch(devname) async {
|
||||
|
||||
|
||||
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('⏰ 被动-定时器触发,开始执行重连...');
|
||||
connectBySwitch(host: _lastHost!, port: _lastPort!, deviceName: devname);
|
||||
});
|
||||
await _sendAuthPacketBySwitch(devname);
|
||||
}
|
||||
/// 发送数据
|
||||
void send(Map<String, dynamic> data) {
|
||||
if (_socket == null) throw Exception("Socket not connected");
|
||||
@@ -182,16 +229,42 @@ class TcpClient {
|
||||
}
|
||||
|
||||
|
||||
// void disconnect() {
|
||||
// stopHeartbeat(); // 先停心跳
|
||||
// _socket?.destroy();
|
||||
// _socket = null;
|
||||
// // 2. 🔥 关键:取消重连定时器!防止断开后自动触发旧逻辑重连回第一个设备
|
||||
// _reconnectTimer?.cancel();
|
||||
// _reconnectTimer = null;
|
||||
// // _controller.close();
|
||||
// debugPrint("TCP Disconnected");
|
||||
//
|
||||
// }
|
||||
void disconnect() {
|
||||
stopHeartbeat(); // 先停心跳
|
||||
_socket?.destroy();
|
||||
_socket = null;
|
||||
_controller.close();
|
||||
debugPrint("TCP Disconnected");
|
||||
debugPrint('🛑 [TCP] 主动断开连接...');
|
||||
|
||||
// 1. 停止心跳
|
||||
stopHeartbeat();
|
||||
|
||||
// 2. 🔥 关键:取消重连定时器!防止断开后自动触发旧逻辑重连回第一个设备
|
||||
_reconnectTimer?.cancel();
|
||||
_reconnectTimer = null;
|
||||
|
||||
// 3. 销毁 Socket
|
||||
if (_socket != null) {
|
||||
_socket!.destroy();
|
||||
_socket = null;
|
||||
debugPrint('✅ [TCP] Socket 已物理销毁');
|
||||
}
|
||||
|
||||
// ❌ 绝对不要关闭 _controller!否则数据流断裂,重连后收不到数据
|
||||
// _controller.close();
|
||||
|
||||
debugPrint('✅ [TCP] 连接已断开,等待手动重连');
|
||||
}
|
||||
|
||||
void sendPathPoint(RoutePlanSendEntity routePlanSendEntity) {
|
||||
|
||||
void sendPathPoint(RoutePlanSendEntity routePlanSendEntity) {
|
||||
if (_socket == null) return;
|
||||
final payload = routePlanSendEntity.toBytes();
|
||||
final packet = sendRaw(0x01, payload); // 第1种的测试 声明指令结构和类型0x01 为命令类型
|
||||
@@ -270,7 +343,7 @@ class TcpClient {
|
||||
final DeviceEntity targetDevice = devices.first;
|
||||
debugPrint('📱 [AuthTcp] 准备切换至默认设备:${targetDevice.deviceName}');
|
||||
|
||||
// 切换设备 (同样,如果 switchDeviceUseCase 也返回 Either,也需要 fold 处理)
|
||||
// 切换设备
|
||||
final switchResult = await switchDeviceUseCase.deviceRepository.switchDevice("app",targetDevice.deviceName);
|
||||
|
||||
await switchResult.fold(
|
||||
@@ -295,4 +368,132 @@ class TcpClient {
|
||||
if (_socket == null) return;
|
||||
_socket!.add(bytes);
|
||||
}
|
||||
|
||||
Future<void> connectBySwitch({required String host, required int port, required String deviceName}) async {
|
||||
isUserSwitch=true;
|
||||
debugPrint('🔌被动 [TCP] 开始连接:$host:$port'); // ✅ 必须看到这条
|
||||
_lastHost = host;
|
||||
_lastPort = port;
|
||||
|
||||
if (_socket != null) {
|
||||
_socket!.destroy();
|
||||
_socket = null;
|
||||
}
|
||||
|
||||
try {
|
||||
_socket = await Socket.connect(
|
||||
host,
|
||||
port,
|
||||
timeout: const Duration(seconds: 5),
|
||||
);
|
||||
debugPrint('✅ 被动[TCP] 连接成功!'); // ✅ 必须看到这条
|
||||
|
||||
_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) {
|
||||
if (!_controller.isClosed) {
|
||||
_controller.add(packet);
|
||||
debugPrint('➡️被动 [TCP] 已分发 CMD: 0x${packet.command.toRadixString(16)}');
|
||||
}
|
||||
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] 连接已断开!');
|
||||
_handleDisconnectBySwitch(deviceName);
|
||||
},
|
||||
onError: (e) {
|
||||
debugPrint('❌ 被动[TCP] 发生错误:$e');
|
||||
_handleDisconnectBySwitch(deviceName); // 统一走重连逻辑,保护 Controller 不被关闭
|
||||
},
|
||||
);
|
||||
// 开始认证tcp
|
||||
await _sendAuthPacketBySwitch(deviceName);
|
||||
} catch (e) {
|
||||
rethrow; // 向上抛出连接异常
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _sendAuthPacketBySwitch(String deviceName) async {
|
||||
if (_socket == null) return;
|
||||
String? username= "";
|
||||
String? token= "";
|
||||
final user = await _userStorage.getUser();
|
||||
deviceName= deviceName;
|
||||
debugPrint('tcp被动切换认证:$user');
|
||||
if (user == null || user.token == null) {
|
||||
debugPrint('❌ 被动[TCP] 认证失败:用户未登录或 Token 为空,无法发送认证包');
|
||||
disconnect();
|
||||
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 结果
|
||||
await switchDeviceUseCase.deviceRepository.switchDevice("app",deviceName);
|
||||
} catch (e) {
|
||||
debugPrint('❌ 被动[AuthTcp] 设备订阅流程异常:$e');
|
||||
rethrow;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void _handleDisconnectBySwitch( String deviceName) {
|
||||
stopHeartbeat();
|
||||
|
||||
_socket = null;
|
||||
// 注意:这里不要关闭 _controller!否则监听者会丢失数据流
|
||||
// _controller?.close();
|
||||
_scheduleReconnectBySwitch(deviceName);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ 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);
|
||||
await _authTcpDatasource.sendAuthPacket(); //包括发送认证包和获取列表和切换函数
|
||||
// await _authTcpDatasource.sendAuthPacket(); //包括发送认证包和获取列表和切换函数
|
||||
tcp.startHeartbeat(interval: const Duration(seconds: 4)); //启动心跳
|
||||
appCubit.setAuth(user);
|
||||
emit(AuthAuthenticated(user));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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/core/network/tcp/tcp_client.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';
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
|
||||
import 'dart:async';
|
||||
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 'package:maibu_satabot_v2/core/network/protocol_decoder.dart';
|
||||
|
||||
import '../../../../core/network/net_message_dispatcher.dart';
|
||||
import 'device_status_event.dart';
|
||||
@@ -13,41 +13,70 @@ import 'device_status_state.dart';
|
||||
class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
final NetMessageDispatcher _dispatcher;
|
||||
|
||||
// 持有订阅引用,仅在 close 时取消
|
||||
StreamSubscription? _stringSub;
|
||||
StreamSubscription? _jsonSub;
|
||||
|
||||
// 🔥 新增:标记是否已初始化订阅,防止重复订阅
|
||||
bool _isSubscribed = false;
|
||||
|
||||
DeviceStatusBloc(this._dispatcher) : super(DeviceStatusInitial()) {
|
||||
// 订阅 TCP 数据流
|
||||
_dispatcher.onStringMessage().listen((jsonString) {
|
||||
debugPrint('📩 Bloc 收到 0x02 数据');
|
||||
add(DeviceStatusLoaded(jsonString));
|
||||
});
|
||||
// 1. 初始建立订阅(终身有效,除非 Bloc 关闭)
|
||||
_setupStreamListeners();
|
||||
|
||||
_dispatcher.onJsonMessage(0x12).listen((jsonData) {
|
||||
add(PushMessageReceived(jsonData));
|
||||
});
|
||||
|
||||
//
|
||||
// 2. 注册事件处理
|
||||
on<DeviceStatusReset>(_handleReset);
|
||||
on<DeviceStatusLoaded>(_handleDeviceStatusLoaded);
|
||||
on<PushMessageReceived>(_handlePushMessageReceived);
|
||||
|
||||
|
||||
// 🔥 新增:模拟测试数据(调试用)
|
||||
// _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);
|
||||
// 🔥 核心修复:订阅逻辑只执行一次,不再随意 cancel/relisten
|
||||
void _setupStreamListeners() {
|
||||
if (_isSubscribed) {
|
||||
return; // 如果已经订阅过,直接返回,避免重复操作
|
||||
}
|
||||
|
||||
debugPrint('🧪 模拟测试数据:$mockData');
|
||||
debugPrint('🔗 [DeviceStatusBloc] 初始化 TCP 数据流订阅(终身有效)');
|
||||
|
||||
// 发送模拟事件
|
||||
add(DeviceStatusLoaded(mockData));
|
||||
// 订阅字符串流 (0x02)
|
||||
_stringSub = _dispatcher.onStringMessage().listen(
|
||||
(jsonString) {
|
||||
debugPrint('Bloc层已经!!收到 0x02 数据事件,长度:${jsonString.length}');
|
||||
if (!isClosed) {
|
||||
add(DeviceStatusLoaded(jsonString));
|
||||
}
|
||||
},
|
||||
onDone: () => debugPrint('⚠️ [Bloc] 0x02 流已结束 (onDone) - 这通常意味着底层 TCP 彻底关闭'),
|
||||
onError: (e) => debugPrint('❌ [Bloc] 0x02 流发生错误:$e'),
|
||||
);
|
||||
|
||||
// 订阅 JSON 流 (0x12)
|
||||
_jsonSub = _dispatcher.onJsonMessage(0x12).listen(
|
||||
(jsonData) {
|
||||
debugPrint('📩 [Bloc] 收到 JSON 数据事件:$jsonData');
|
||||
if (!isClosed) {
|
||||
add(PushMessageReceived(jsonData));
|
||||
}
|
||||
},
|
||||
onDone: () => debugPrint('⚠️ [Bloc] JSON 流已结束 (onDone)'),
|
||||
onError: (e) => debugPrint('❌ [Bloc] JSON 流发生错误:$e'),
|
||||
);
|
||||
|
||||
_isSubscribed = true;
|
||||
debugPrint('✅ [DeviceStatusBloc] 订阅建立完成,将持续监听数据流');
|
||||
}
|
||||
// 🔥 修改:使用 Emitter 发送状态
|
||||
|
||||
// 🔥 核心修复:重置时仅清空状态,绝对不再触碰订阅关系
|
||||
Future<void> _handleReset(DeviceStatusReset event, Emitter<DeviceStatusState> emit) async {
|
||||
debugPrint('🔄 收到重置事件:仅清空状态,保持订阅活跃(不重连)');
|
||||
|
||||
// 只 emit 初始状态,让 UI 清除旧设备的数据(如速度归零、轨迹清除)
|
||||
emit(DeviceStatusInitial());
|
||||
|
||||
// ❌ 严禁在此处调用 _setupStreamListeners() 或 cancel 订阅
|
||||
// 因为 TCP 重连期间数据流可能一直在推送,cancel 会导致关键首包丢失
|
||||
}
|
||||
|
||||
Future<void> _handleDeviceStatusLoaded(
|
||||
DeviceStatusLoaded event,
|
||||
Emitter<DeviceStatusState> emit,
|
||||
@@ -55,22 +84,21 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
try {
|
||||
debugPrint('🔍 开始解析数据:${event.jsonString}');
|
||||
final fields = event.jsonString.trim().split(',');
|
||||
debugPrint('🔍 字段数量:${fields.length}');
|
||||
|
||||
if (fields.length < 18) {
|
||||
debugPrint('⚠️ 字段不足:${fields.length}');
|
||||
emit(DeviceStatusError('字段不足,期望 ≥18,实际:${fields.length}'));
|
||||
return;
|
||||
}
|
||||
|
||||
final status = RunningStatusEntity.fromFields(fields);
|
||||
debugPrint('设备运行时候状态:$status');
|
||||
final gps = GPSEntity(status.latitude, status.longitude);
|
||||
debugPrint('GPS: ${gps.latitude}, ${gps.longitude}');
|
||||
|
||||
emit(DeviceStatusUpdated(status, gps)); // 🔥 使用 emit 发送状态
|
||||
} catch (e) {
|
||||
debugPrint('❌ 解析异常:$e');
|
||||
emit(DeviceStatusError('解析设备运行时候状态失败:$e'));
|
||||
debugPrint('✅ 解析成功,更新状态:Lat=${gps.latitude}, Lng=${gps.longitude}');
|
||||
emit(DeviceStatusUpdated(status, gps));
|
||||
} catch (e, stack) {
|
||||
debugPrint('❌ 解析异常:$e\n$stack');
|
||||
emit(DeviceStatusError('解析失败:$e'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +110,7 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
final eventStr = event.jsonData['event'] ?? '';
|
||||
final deviceId = event.jsonData['deviceId'] ?? '未知';
|
||||
debugPrint('收到推送事件:$eventStr, 设备:$deviceId');
|
||||
// 这里可以根据需要 emit 新状态
|
||||
} catch (e) {
|
||||
emit(DeviceStatusError('解析推送消息失败:$e'));
|
||||
}
|
||||
@@ -89,7 +118,15 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
return super.close();
|
||||
// 只有在 Bloc 彻底销毁时才取消订阅
|
||||
//debugPrint('🚫 [DeviceStatusBloc] 正在关闭,取消所有订阅 ');
|
||||
// debugPrint('🚫 [DeviceStatusBloc] 正在关闭,取消所有订阅');
|
||||
//debugPrint('🔥 [DeviceStatusBloc] 销毁堆栈跟踪:\n${StackTrace.current}');
|
||||
// _stringSub?.cancel();
|
||||
// _jsonSub?.cancel();
|
||||
//return super.close();
|
||||
return Future.value();
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,3 +22,8 @@ class PushMessageReceived extends DeviceStatusEvent {
|
||||
List<Object?> get props => [jsonData];
|
||||
}
|
||||
|
||||
class DeviceStatusReset extends DeviceStatusEvent {
|
||||
@override
|
||||
// TODO: implement props
|
||||
List<Object?> get props => throw UnimplementedError();
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import 'package:maibu_satabot_v2/features/devices/domain/usecases/select_work_re
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/usecases/unbind_device_usecase.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/usecases/update_devicename_usecase.dart';
|
||||
|
||||
import '../../../../core/consts/tcp_consts.dart';
|
||||
import '../../../../core/network/tcp/tcp_client.dart';
|
||||
import '../../data/models/device_add_path_point_model.dart';
|
||||
import '../../data/models/device_work_area_param_model.dart';
|
||||
import '../../domain/usecases/bind_device_usecase.dart';
|
||||
@@ -18,6 +20,8 @@ import '../../domain/usecases/delete_work_record_usecase.dart';
|
||||
import '../../domain/usecases/get_device_location_usecase.dart';
|
||||
import '../../domain/usecases/get_work_record_usecase.dart';
|
||||
import '../../domain/usecases/route_planning_usecase.dart';
|
||||
import 'device_status_bloc.dart';
|
||||
import 'device_status_event.dart';
|
||||
import 'devices_state.dart';
|
||||
|
||||
class DevicesCubit extends Cubit<DevicesState> {
|
||||
@@ -33,7 +37,8 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
final SaveWorkRecordUseCase _saveWorkRecordUseCase;
|
||||
final GeneratePathUseCase _generatePathUseCase;
|
||||
final RoutePlanningUseCase _routePlanningUseCase; //
|
||||
|
||||
final DeviceStatusBloc _deviceStatusBloc; // 🔥 新增字段
|
||||
final TcpClient _tcpClient;
|
||||
DevicesCubit(
|
||||
this.repository,
|
||||
this._getUserDeviceUseCase,
|
||||
@@ -47,6 +52,9 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
this._generatePathUseCase,
|
||||
this._routePlanningUseCase,
|
||||
this._bindDeviceUseCase,
|
||||
this._deviceStatusBloc, this._tcpClient,
|
||||
|
||||
|
||||
) : super(const DevicesState());
|
||||
|
||||
Future<void> unbindDevice(String deviceId, String deviceName) async {
|
||||
@@ -192,10 +200,41 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
|
||||
final result = await repository.switchDevice("app", device.deviceName);
|
||||
|
||||
result.fold((l) {
|
||||
emit(state.copyWith(isLoading: false, errorMessage: l.message));
|
||||
selectDevice(device);
|
||||
}, (r) => emit(state.copyWith(isLoading: false, selectedDevice: device)));
|
||||
result.fold(
|
||||
// 失败处理
|
||||
(l) {
|
||||
emit(state.copyWith(isLoading: false, errorMessage: l.message));
|
||||
selectDevice(device);
|
||||
},
|
||||
// 成功处理:改为 async 函数块
|
||||
(r) async {
|
||||
// 3. 最后更新 UI 状态
|
||||
try {
|
||||
// 🔥 关键修复 1:先强制断开旧连接!
|
||||
// 这一步会销毁旧 Socket,清除旧 Listener,防止旧数据继续推送
|
||||
if (_tcpClient.isConnected) {
|
||||
debugPrint('🛑 检测到已连接,先断开旧 TCP 连接...');
|
||||
_tcpClient.disconnects(forSwitch: true);
|
||||
// 稍微等待一下,确保底层 Socket 资源释放 (可选,但推荐)
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
}
|
||||
|
||||
// 🔥 关键修复 2:发起新连接
|
||||
// connect 方法内部会自动调用 _sendAuthPacket -> 获取设备列表 -> 自动订阅当前选中的设备
|
||||
debugPrint('🔌 开始重新新连接 TCP,将自动订阅新设备:${device.deviceName}');
|
||||
await _tcpClient.connectBySwitch(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT, deviceName: device.deviceName);
|
||||
// 🔥 关键修复 3:重置设备状态 Bloc,清除旧设备图表数据
|
||||
_deviceStatusBloc.add(DeviceStatusReset());
|
||||
|
||||
debugPrint('✅ 新设备切换流程完成');
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ 设备切换成功,但 TCP 重连或状态重置失败:$e');
|
||||
// 即使 TCP 失败,也更新 UI 选中状态,让用户知道切换了,只是没数据
|
||||
}
|
||||
|
||||
emit(state.copyWith(isLoading: false, selectedDevice: device));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 绑定设备
|
||||
|
||||
@@ -878,7 +878,19 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
|
||||
SliverToBoxAdapter(
|
||||
child: BlocBuilder<DeviceStatusBloc, DeviceStatusState>(
|
||||
builder: (context, state) {
|
||||
debugPrint('🎨 [UI-Build] BlocBuilder 重建!当前状态类型:${state.runtimeType}');
|
||||
|
||||
|
||||
// 检测到设备切换(状态重置)时,清空图表历史数据
|
||||
if (state is DeviceStatusInitial) {
|
||||
debugPrint('🧹 [UI] 检测到 Initial 状态,准备重置图表数据');
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_resetChartData();
|
||||
});
|
||||
}
|
||||
|
||||
if (state is DeviceStatusUpdated) {
|
||||
debugPrint('📈 [UI] 检测到 Updated 状态,准备追加图表数据');
|
||||
_appendChartData(state);
|
||||
}
|
||||
return Container(margin: const EdgeInsets.all(8), child: _isCardView ? _buildCardContentView(state) : _buildChartContentView(state));
|
||||
@@ -889,6 +901,8 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _resetChartData() {}
|
||||
}
|
||||
|
||||
// ====================== 带动画的圆形仪表盘 Widget ======================
|
||||
|
||||
@@ -47,7 +47,7 @@ class MyApp extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
// 在 runApp 之前调用 appStarted,确保 GoRouter 初始化时能获取到正确的初始状态
|
||||
sl<AuthCubit>().appStarted();
|
||||
|
||||
final deviceStatusBloc = sl<DeviceStatusBloc>();
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
// 核心修正:在这里提供 AuthCubit
|
||||
@@ -66,7 +66,9 @@ class MyApp extends StatelessWidget {
|
||||
return devicesCubit;
|
||||
},
|
||||
),
|
||||
BlocProvider<DeviceStatusBloc>(create: (_) => sl<DeviceStatusBloc>()),
|
||||
BlocProvider<DeviceStatusBloc>.value(
|
||||
value: deviceStatusBloc,
|
||||
),
|
||||
// 其他 Cubit...
|
||||
],
|
||||
child: MaterialApp.router(title: 'Maibu Satabot', theme: AppTheme.lightTheme, routerConfig: sl<GoRouter>()),
|
||||
|
||||
Reference in New Issue
Block a user