完成页面对接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 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../../features/auth/data/datasources/auth_http_datasource.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_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/data/repositories/auth_repository_impl.dart';
|
||||||
import '../../features/auth/domain/repositories/auth_repository.dart';
|
import '../../features/auth/domain/repositories/auth_repository.dart';
|
||||||
import '../../features/auth/domain/usecases/login_usecase.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/route_planning_usecase.dart';
|
||||||
import '../../features/devices/domain/usecases/save_work_record_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/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 '../../features/remote_control/presentation/bloc/remote_control_cubit.dart';
|
||||||
import '../app/app_user_cubit.dart';
|
import '../app/app_user_cubit.dart';
|
||||||
import '../network/dio_client.dart';
|
import '../network/dio_client.dart';
|
||||||
@@ -59,7 +63,9 @@ Future<void> init() async {
|
|||||||
sl.registerLazySingleton<Dio>(() => DioClient.create());
|
sl.registerLazySingleton<Dio>(() => DioClient.create());
|
||||||
|
|
||||||
/// 1.1.2 TcpClient:TCP客户端
|
/// 1.1.2 TcpClient:TCP客户端
|
||||||
sl.registerLazySingleton(() => TcpClient());
|
sl.registerLazySingleton(() => TcpClient( sl<UserStorage>(),
|
||||||
|
getUserDeviceUseCase: sl<GetUserDeviceUseCase>(),
|
||||||
|
switchDeviceUseCase: sl<SwitchDeviceUseCase>(),));
|
||||||
|
|
||||||
/// 1.1.3 NetMessageDispatcher:消息调度器,并将 TcpClient 注入给它
|
/// 1.1.3 NetMessageDispatcher:消息调度器,并将 TcpClient 注入给它
|
||||||
sl.registerLazySingleton(() => NetMessageDispatcher(sl()));
|
sl.registerLazySingleton(() => NetMessageDispatcher(sl()));
|
||||||
@@ -83,7 +89,7 @@ Future<void> init() async {
|
|||||||
() => AuthHttpDataSourceImpl(sl()),
|
() => AuthHttpDataSourceImpl(sl()),
|
||||||
);
|
);
|
||||||
sl.registerLazySingleton<DeviceHttpDatasource>(
|
sl.registerLazySingleton<DeviceHttpDatasource>(
|
||||||
() => DeviceHttpDatasourceImpl(sl()),
|
() => DeviceHttpDatasourceImpl(sl(), sl<UserStorage>()), //
|
||||||
);
|
);
|
||||||
|
|
||||||
/// 3. 仓库 (Repository)
|
/// 3. 仓库 (Repository)
|
||||||
@@ -126,7 +132,16 @@ Future<void> init() async {
|
|||||||
sl.registerLazySingleton(() => UnbindDeviceUseCase(sl()));
|
sl.registerLazySingleton(() => UnbindDeviceUseCase(sl()));
|
||||||
sl.registerLazySingleton(() => UpdateDevicenameUsecase(sl()));
|
sl.registerLazySingleton(() => UpdateDevicenameUsecase(sl()));
|
||||||
sl.registerLazySingleton<SaveWorkRecordUseCase>(() => SaveWorkRecordUseCase(sl<PathRepository>()));
|
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)
|
/// 5. 状态管理 (Cubit/Bloc)
|
||||||
@@ -136,6 +151,11 @@ Future<void> init() async {
|
|||||||
() => DevicesCubit(sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(),sl(),sl()),
|
() => DevicesCubit(sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(), sl(),sl(),sl()),
|
||||||
);
|
);
|
||||||
sl.registerFactory(() => RemoteControlCubit(sl()));
|
sl.registerFactory(() => RemoteControlCubit(sl()));
|
||||||
|
/* // 工厂模式(留存,每次获取新实例)
|
||||||
|
sl.registerFactory(() => DeviceStatusBloc(sl<NetMessageDispatcher>()));
|
||||||
|
*/
|
||||||
|
//单例模式(全局共享一个实例)
|
||||||
|
sl.registerLazySingleton(() => DeviceStatusBloc(sl<NetMessageDispatcher>()));
|
||||||
|
|
||||||
/// . 路径生成
|
/// . 路径生成
|
||||||
sl.registerLazySingleton<PathHttpDatasource>(
|
sl.registerLazySingleton<PathHttpDatasource>(
|
||||||
@@ -162,6 +182,7 @@ Future<void> init() async {
|
|||||||
sl<TcpClient>(),
|
sl<TcpClient>(),
|
||||||
sl<AppUserCubit>(),
|
sl<AppUserCubit>(),
|
||||||
sl<NetMessageDispatcher>(),
|
sl<NetMessageDispatcher>(),
|
||||||
|
sl<AuthTcpDatasource>(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,19 @@ class NetMessageDispatcher {
|
|||||||
|
|
||||||
/// 示例解析方法:将 0x02 指令解析为 String
|
/// 示例解析方法:将 0x02 指令解析为 String
|
||||||
Stream<String> onStringMessage() {
|
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 并转为模型
|
/// 示例解析方法:将 0x12 指令解析为 JSON 并转为模型
|
||||||
|
|||||||
@@ -6,9 +6,14 @@ import 'dart:typed_data';
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:maibu_satabot_v2/features/devices/data/repositories/route_planning_repository_impl.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/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/gps_entity.dart';
|
||||||
import '../../../features/devices/domain/entities/running_status_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';
|
import '../protocol_decoder.dart';
|
||||||
|
|
||||||
|
|
||||||
@@ -18,39 +23,111 @@ class TcpClient {
|
|||||||
// 使用 StreamController 将原始数据流暴露给外部,以便多个 DataSource 监听
|
// 使用 StreamController 将原始数据流暴露给外部,以便多个 DataSource 监听
|
||||||
final _controller = StreamController<RawPacket>.broadcast();
|
final _controller = StreamController<RawPacket>.broadcast();
|
||||||
Stream<RawPacket> get packetStream => _controller.stream;
|
Stream<RawPacket> get packetStream => _controller.stream;
|
||||||
|
final GetUserDeviceUseCase getUserDeviceUseCase;
|
||||||
|
final SwitchDeviceUseCase switchDeviceUseCase;
|
||||||
|
TcpClient(this._userStorage, {required this.getUserDeviceUseCase, required this.switchDeviceUseCase});
|
||||||
|
|
||||||
// 新增:心跳定时器
|
// 新增:心跳定时器
|
||||||
Timer? _heartbeatTimer;
|
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 {
|
Future<void> connect({required String host, required int port}) async {
|
||||||
|
debugPrint('🔌 [TCP] 开始连接:$host:$port'); // ✅ 必须看到这条
|
||||||
|
_lastHost = host;
|
||||||
|
_lastPort = port;
|
||||||
try {
|
try {
|
||||||
_socket = await Socket.connect(
|
_socket = await Socket.connect(
|
||||||
host,
|
host,
|
||||||
port,
|
port,
|
||||||
timeout: const Duration(seconds: 5),
|
timeout: const Duration(seconds: 5),
|
||||||
);
|
);
|
||||||
|
debugPrint('✅ [TCP] 连接成功!'); // ✅ 必须看到这条
|
||||||
|
// 开始认证tcp
|
||||||
|
await _sendAuthPacket();
|
||||||
_socket!.listen((data) {
|
_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);
|
var packets = _decoder.decode(data);
|
||||||
|
debugPrint('📦 [TCP] 解码成功,包数量:${packets.length}');
|
||||||
for (var packet in packets) {
|
for (var packet in packets) {
|
||||||
// 🔥若收到服务端心跳(cmd == 0xFF),立即回复一个心跳包
|
if (!_controller.isClosed) {
|
||||||
if (packet.command == 0xFF) {
|
_controller.add(packet);
|
||||||
debugPrint('收到服务端心跳,自动回复...');
|
debugPrint('➡️ [TCP] 已分发 CMD: 0x${packet.command.toRadixString(16)}');
|
||||||
sendHeartbeat(); // 回复 AB AA FF AA AB
|
|
||||||
}
|
}
|
||||||
_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) {
|
} catch (e) {
|
||||||
rethrow; // 向上抛出连接异常
|
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) {
|
void send(Map<String, dynamic> data) {
|
||||||
if (_socket == null) throw Exception("Socket not connected");
|
if (_socket == null) throw Exception("Socket not connected");
|
||||||
@@ -89,7 +166,7 @@ class TcpClient {
|
|||||||
// 新增:启动心跳(每 4 秒发送一次 0xFF 指令)
|
// 新增:启动心跳(每 4 秒发送一次 0xFF 指令)
|
||||||
void startHeartbeat({Duration interval = const Duration(seconds: 4)}) {
|
void startHeartbeat({Duration interval = const Duration(seconds: 4)}) {
|
||||||
if (_heartbeatTimer != null) return; // 防止重复启动
|
if (_heartbeatTimer != null) return; // 防止重复启动
|
||||||
|
debugPrint('收到服务端心跳,自动回复...');
|
||||||
_heartbeatTimer = Timer.periodic(interval, (_) {
|
_heartbeatTimer = Timer.periodic(interval, (_) {
|
||||||
// 发送心跳帧:AB AA FF 00 00 AA AB(与 sendRaw 一致)
|
// 发送心跳帧:AB AA FF 00 00 AA AB(与 sendRaw 一致)
|
||||||
//sendRaw(0xFF, []);
|
//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/app/app_user_cubit.dart';
|
||||||
import '../../../../core/network/tcp/tcp_client.dart';
|
import '../../../../core/network/tcp/tcp_client.dart';
|
||||||
import '../../../../core/storage/user_storage.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';
|
import 'auth_state.dart';
|
||||||
|
|
||||||
/// emit做的事情:
|
/// emit做的事情:
|
||||||
@@ -19,9 +23,11 @@ class AuthCubit extends Cubit<AuthState> {
|
|||||||
final TcpClient tcp;
|
final TcpClient tcp;
|
||||||
final AppUserCubit appCubit;
|
final AppUserCubit appCubit;
|
||||||
final NetMessageDispatcher dispatcher;
|
final NetMessageDispatcher dispatcher;
|
||||||
|
final AuthTcpDatasource _authTcpDatasource;
|
||||||
|
|
||||||
StreamSubscription? _kickOutSub; // 新增:用于管理监听生命周期
|
StreamSubscription? _kickOutSub; // 新增:用于管理监听生命周期
|
||||||
|
|
||||||
AuthCubit(this.storage, this.tcp, this.appCubit, this.dispatcher)
|
AuthCubit(this.storage, this.tcp, this.appCubit, this.dispatcher, this._authTcpDatasource)
|
||||||
: super(AuthInitial()) {
|
: super(AuthInitial()) {
|
||||||
// Cubit 一启动就开始监听 TCP 的“自动逻辑”
|
// Cubit 一启动就开始监听 TCP 的“自动逻辑”
|
||||||
_listenToAuthResponse();
|
_listenToAuthResponse();
|
||||||
@@ -33,6 +39,7 @@ class AuthCubit extends Cubit<AuthState> {
|
|||||||
if (user != null) {
|
if (user != null) {
|
||||||
// 1. 建立 TCP 连接 (远程控制)
|
// 1. 建立 TCP 连接 (远程控制)
|
||||||
tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT);
|
tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT);
|
||||||
|
await _authTcpDatasource.sendAuthPacket();//包括发送认证包和获取列表和切换函数
|
||||||
// 2. 同步全局 App 状态
|
// 2. 同步全局 App 状态
|
||||||
appCubit.setAuth(user);
|
appCubit.setAuth(user);
|
||||||
// 3. 进入已登录状态
|
// 3. 进入已登录状态
|
||||||
@@ -46,7 +53,8 @@ class AuthCubit extends Cubit<AuthState> {
|
|||||||
Future<void> loginSuccess(UserEntity user) async {
|
Future<void> loginSuccess(UserEntity user) async {
|
||||||
await storage.saveUser(user);
|
await storage.saveUser(user);
|
||||||
await tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT);
|
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);
|
appCubit.setAuth(user);
|
||||||
emit(AuthAuthenticated(user));
|
emit(AuthAuthenticated(user));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
import 'package:dio/dio.dart';
|
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/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/data/datasources/device_http_datasource.dart';
|
||||||
import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity.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';
|
import '../../../domain/entities/device_location_entity.dart';
|
||||||
|
|
||||||
class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
|
class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
|
||||||
final Dio dio;
|
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
|
@override
|
||||||
Future<int> bindDevice(String deviceId, String deviceAlias) async {
|
Future<int> bindDevice(String deviceId, String deviceAlias) async {
|
||||||
@@ -31,9 +42,17 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<DeviceEntity>> getUserDevices(String username) async {
|
Future<List<DeviceEntity>> getUserDevices(String username) async {
|
||||||
|
final token = await _getToken();
|
||||||
|
|
||||||
var response = await dio.get(
|
var response = await dio.get(
|
||||||
HttpApiConsts.getUserDevicesList,
|
HttpApiConsts.getUserDevicesList,
|
||||||
queryParameters: {'tenantName': username},
|
queryParameters: {'tenantName': username},
|
||||||
|
options: Options(
|
||||||
|
headers: {
|
||||||
|
'Authorization': token != null ? 'Bearer $token' : '',
|
||||||
|
// 如果后端不需要 Bearer 前缀,直接写 token 即可
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throw Exception('网络请求失败:${response.statusCode}');
|
throw Exception('网络请求失败:${response.statusCode}');
|
||||||
@@ -58,9 +77,16 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<int> switchDevice(String platform, String deviceId) async {
|
Future<int> switchDevice(String platform, String deviceId) async {
|
||||||
|
final token = await _getToken();
|
||||||
var response = await dio.post(
|
var response = await dio.post(
|
||||||
HttpApiConsts.switchDevice,
|
HttpApiConsts.switchDevice,
|
||||||
data: {'platform': platform, 'deviceId': deviceId},
|
data: {'platform': platform, 'deviceId': deviceId},
|
||||||
|
options: Options(
|
||||||
|
headers: {
|
||||||
|
'Authorization': token != null ? 'Bearer $token' : '',
|
||||||
|
// 如果后端不需要 Bearer 前缀,直接写 token 即可
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throw Exception('网络请求失败:${response.statusCode}');
|
throw Exception('网络请求失败:${response.statusCode}');
|
||||||
|
|||||||
@@ -1,29 +1,43 @@
|
|||||||
|
// lib/features/devices/domain/entities/running_status_entity.dart
|
||||||
|
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
class RunningStatusEntity extends Equatable {
|
class RunningStatusEntity extends Equatable {
|
||||||
final double voltage;
|
// === 基础电气信息 (字段 0-6) ===
|
||||||
final double leftTargetSpeed;
|
final double voltage; // [0] 电压 (V)
|
||||||
final double rightTargetSpeed;
|
final double leftTargetSpeed; // [1] 左轮目标速度
|
||||||
final double leftMeasureSpeed;
|
final double rightTargetSpeed; // [2] 右轮目标速度
|
||||||
final double rightMeasureSpeed;
|
final double leftMeasureSpeed; // [3] 左轮实测速度
|
||||||
final double leftCurrent;
|
final double rightMeasureSpeed; // [4] 右轮实测速度
|
||||||
final double rightCurrent;
|
final double leftCurrent; // [5] 左轮电流 (A)
|
||||||
final double leftMotorTemp;
|
final double rightCurrent; // [6] 右轮电流 (A)
|
||||||
final double rightMotorTemp;
|
|
||||||
final double chipTemp;
|
// === 温度信息 (字段 7-9) ===
|
||||||
final double yaw;
|
final double leftMotorTemp; // [7] 左电机温度 (℃)
|
||||||
final double pitch;
|
final double rightMotorTemp; // [8] 右电机温度 (℃)
|
||||||
final double roll;
|
final double chipTemp; // [9] 芯片温度 (℃)
|
||||||
final int satelliteCnt;
|
|
||||||
final int qual;
|
// === 姿态信息 (字段 10-12) ===
|
||||||
final int headingStatus;
|
final double yaw; // [10] 偏航角 (°)
|
||||||
final double latitude;
|
final double pitch; // [11] 俯仰角 (°)
|
||||||
final double longitude;
|
final double roll; // [12] 翻滚角 (°)
|
||||||
final String knifeCuttingSpeed;
|
|
||||||
final String controlMode;
|
// === GPS 信号质量 (字段 13-15) ===
|
||||||
final String battery;
|
final int satelliteCnt; // [13] 卫星数量
|
||||||
final String workingArea;
|
final int qual; // [14] 定位质量
|
||||||
final String obstacleFlag;
|
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({
|
const RunningStatusEntity({
|
||||||
this.voltage = 0.0,
|
this.voltage = 0.0,
|
||||||
@@ -44,13 +58,61 @@ class RunningStatusEntity extends Equatable {
|
|||||||
this.headingStatus = 0,
|
this.headingStatus = 0,
|
||||||
this.latitude = 0.0,
|
this.latitude = 0.0,
|
||||||
this.longitude = 0.0,
|
this.longitude = 0.0,
|
||||||
this.knifeCuttingSpeed = "0",
|
this.timestamp = '',
|
||||||
this.controlMode = "0",
|
this.knifeCuttingSpeed = '0',
|
||||||
this.battery = "0",
|
this.controlMode = '0',
|
||||||
this.workingArea = "0",
|
this.battery = '0',
|
||||||
this.obstacleFlag = "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({
|
RunningStatusEntity copyWith({
|
||||||
double? voltage,
|
double? voltage,
|
||||||
double? leftTargetSpeed,
|
double? leftTargetSpeed,
|
||||||
@@ -70,6 +132,7 @@ class RunningStatusEntity extends Equatable {
|
|||||||
int? headingStatus,
|
int? headingStatus,
|
||||||
double? latitude,
|
double? latitude,
|
||||||
double? longitude,
|
double? longitude,
|
||||||
|
String? timestamp,
|
||||||
String? knifeCuttingSpeed,
|
String? knifeCuttingSpeed,
|
||||||
String? controlMode,
|
String? controlMode,
|
||||||
String? battery,
|
String? battery,
|
||||||
@@ -95,6 +158,7 @@ class RunningStatusEntity extends Equatable {
|
|||||||
headingStatus: headingStatus ?? this.headingStatus,
|
headingStatus: headingStatus ?? this.headingStatus,
|
||||||
latitude: latitude ?? this.latitude,
|
latitude: latitude ?? this.latitude,
|
||||||
longitude: longitude ?? this.longitude,
|
longitude: longitude ?? this.longitude,
|
||||||
|
timestamp: timestamp ?? this.timestamp,
|
||||||
knifeCuttingSpeed: knifeCuttingSpeed ?? this.knifeCuttingSpeed,
|
knifeCuttingSpeed: knifeCuttingSpeed ?? this.knifeCuttingSpeed,
|
||||||
controlMode: controlMode ?? this.controlMode,
|
controlMode: controlMode ?? this.controlMode,
|
||||||
battery: battery ?? this.battery,
|
battery: battery ?? this.battery,
|
||||||
@@ -105,9 +169,35 @@ class RunningStatusEntity extends Equatable {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [
|
List<Object?> get props => [
|
||||||
voltage, leftTargetSpeed, rightTargetSpeed, leftMeasureSpeed, rightMeasureSpeed,
|
voltage,
|
||||||
leftCurrent, rightCurrent, leftMotorTemp, rightMotorTemp, chipTemp,
|
leftTargetSpeed,
|
||||||
yaw, pitch, roll, satelliteCnt, qual, headingStatus,
|
rightTargetSpeed,
|
||||||
latitude, longitude, knifeCuttingSpeed, controlMode, battery, workingArea, obstacleFlag,
|
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;
|
final NetMessageDispatcher _dispatcher;
|
||||||
|
|
||||||
DeviceStatusBloc(this._dispatcher) : super(DeviceStatusInitial()) {
|
DeviceStatusBloc(this._dispatcher) : super(DeviceStatusInitial()) {
|
||||||
// 直接订阅 0x02 指令的字符串流
|
// 订阅 TCP 数据流
|
||||||
_dispatcher.onStringMessage().listen((jsonString) {
|
_dispatcher.onStringMessage().listen((jsonString) {
|
||||||
|
debugPrint('📩 Bloc 收到 0x02 数据');
|
||||||
add(DeviceStatusLoaded(jsonString));
|
add(DeviceStatusLoaded(jsonString));
|
||||||
});
|
});
|
||||||
|
|
||||||
// 订阅 0x12 的 JSON 流()
|
|
||||||
_dispatcher.onJsonMessage(0x12).listen((jsonData) {
|
_dispatcher.onJsonMessage(0x12).listen((jsonData) {
|
||||||
add(PushMessageReceived(jsonData));
|
add(PushMessageReceived(jsonData));
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
//
|
||||||
Stream<DeviceStatusState> mapEventToState(DeviceStatusEvent event) async* {
|
on<DeviceStatusLoaded>(_handleDeviceStatusLoaded);
|
||||||
if (event is DeviceStatusLoaded) {
|
on<PushMessageReceived>(_handlePushMessageReceived);
|
||||||
yield* _mapDeviceStatusLoaded(event);
|
|
||||||
} else if (event is PushMessageReceived) {
|
|
||||||
yield* _mapPushMessageReceived(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
try {
|
||||||
final fields = event.jsonString.trim().split(' ');
|
debugPrint('🔍 开始解析数据:${event.jsonString}');
|
||||||
|
final fields = event.jsonString.trim().split(',');
|
||||||
|
debugPrint('🔍 字段数量:${fields.length}');
|
||||||
|
|
||||||
if (fields.length < 18) {
|
if (fields.length < 18) {
|
||||||
yield DeviceStatusError('字段不足,期望 ≥18,实际: ${fields.length}');
|
emit(DeviceStatusError('字段不足,期望 ≥18,实际:${fields.length}'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
///解析设备状态
|
|
||||||
final status = RunningStatusEntity().copyWith(
|
final status = RunningStatusEntity.fromFields(fields);
|
||||||
voltage: double.tryParse(fields[0]) ?? 0.0,
|
debugPrint('设备运行时候状态:$status');
|
||||||
leftTargetSpeed: double.tryParse(fields[1]) ?? 0.0,
|
|
||||||
rightTargetSpeed: double.tryParse(fields[2]) ?? 0.0,
|
|
||||||
leftMeasureSpeed: double.tryParse(fields[3]) ?? 0.0,
|
|
||||||
rightMeasureSpeed: double.tryParse(fields[4]) ?? 0.0,
|
|
||||||
leftCurrent: double.tryParse(fields[5]) ?? 0.0,
|
|
||||||
rightCurrent: double.tryParse(fields[6]) ?? 0.0,
|
|
||||||
leftMotorTemp: double.tryParse(fields[7]) ?? 0.0,
|
|
||||||
rightMotorTemp: double.tryParse(fields[8]) ?? 0.0,
|
|
||||||
chipTemp: double.tryParse(fields[9]) ?? 0.0,
|
|
||||||
yaw: double.tryParse(fields[10]) ?? 0.0,
|
|
||||||
pitch: double.tryParse(fields[11]) ?? 0.0,
|
|
||||||
roll: double.tryParse(fields[12]) ?? 0.0,
|
|
||||||
satelliteCnt: int.tryParse(fields[13]) ?? 0,
|
|
||||||
qual: int.tryParse(fields[14]) ?? 0,
|
|
||||||
headingStatus: int.tryParse(fields[15]) ?? 0,
|
|
||||||
latitude: double.tryParse(fields[16]) ?? 0.0,
|
|
||||||
longitude: double.tryParse(fields[17]) ?? 0.0,
|
|
||||||
knifeCuttingSpeed: fields.length > 19 && fields[19].isNotEmpty ? fields[19] : "0",
|
|
||||||
controlMode: fields.length > 20 && fields[20].isNotEmpty ? fields[20] : "0",
|
|
||||||
battery: fields.length > 21 && fields[21].isNotEmpty ? fields[21] : "0",
|
|
||||||
workingArea: fields.length > 22 && fields[22].isNotEmpty ? fields[22] : "0",
|
|
||||||
obstacleFlag: fields.length > 23 && fields[23].isNotEmpty ? fields[23] : "0",
|
|
||||||
);
|
|
||||||
///解析GPS
|
|
||||||
final gps = GPSEntity(status.latitude, status.longitude);
|
final gps = GPSEntity(status.latitude, status.longitude);
|
||||||
yield DeviceStatusUpdated(status, gps);
|
debugPrint('GPS: ${gps.latitude}, ${gps.longitude}');
|
||||||
|
|
||||||
|
emit(DeviceStatusUpdated(status, gps)); // 🔥 使用 emit 发送状态
|
||||||
} catch (e) {
|
} 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 {
|
try {
|
||||||
final eventStr = event.jsonData['event'] ?? '';
|
final eventStr = event.jsonData['event'] ?? '';
|
||||||
final deviceId = event.jsonData['deviceId'] ?? '未知';
|
final deviceId = event.jsonData['deviceId'] ?? '未知';
|
||||||
debugPrint('收到推送事件: $eventStr, 设备: $deviceId');
|
debugPrint('收到推送事件:$eventStr, 设备:$deviceId');
|
||||||
|
|
||||||
// 可扩展:yield PushMessageState(event.jsonData)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
yield DeviceStatusError('解析推送消息失败: $e');
|
emit(DeviceStatusError('解析推送消息失败:$e'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() {
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,3 +21,4 @@ class PushMessageReceived extends DeviceStatusEvent {
|
|||||||
@override
|
@override
|
||||||
List<Object?> get props => [jsonData];
|
List<Object?> get props => [jsonData];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import '../../../../core/di/injection.dart';
|
|||||||
import '../../../../core/network/net_message_dispatcher.dart';
|
import '../../../../core/network/net_message_dispatcher.dart';
|
||||||
import '../../../../core/network/protocol_decoder.dart';
|
import '../../../../core/network/protocol_decoder.dart';
|
||||||
import '../../../devices/presentation/bloc/devices_cubit.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 {
|
class RunningStatusPage extends StatefulWidget {
|
||||||
const RunningStatusPage({super.key});
|
const RunningStatusPage({super.key});
|
||||||
@@ -18,90 +20,18 @@ class RunningStatusPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _RunningStatusPageState extends State<RunningStatusPage> {
|
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;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final deviceState = context.read<DevicesCubit>().state;
|
final deviceState = context.read<DevicesCubit>().state;
|
||||||
final currentDevice = deviceState.selectedDevice;
|
final currentDevice = deviceState.selectedDevice;
|
||||||
|
|
||||||
if (currentDevice == null) {
|
if (currentDevice == null) {
|
||||||
return const Scaffold(body: Center(child: Text("加载中...")));
|
return const Scaffold(
|
||||||
|
body: Center(child: Text("加载中...")),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -125,20 +55,38 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 2. 状态信息行(动态更新)
|
// 2. 状态信息行(动态更新)- 使用 BlocBuilder 监听
|
||||||
SliverToBoxAdapter(
|
BlocBuilder<DeviceStatusBloc, DeviceStatusState>(
|
||||||
child: Container(
|
builder: (context, state) {
|
||||||
color: Colors.white,
|
String yaw = '--';
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
String qual = '--';
|
||||||
child: Row(
|
String satelliteCnt = '--';
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
if (state is DeviceStatusUpdated) {
|
||||||
Text("航向角状态: $_yaw°", style: const TextStyle(fontSize: 14)),
|
yaw = state.status.yaw.toStringAsFixed(2);
|
||||||
Text("定位质量: $_qual", style: const TextStyle(fontSize: 14)),
|
qual = state.status.qual.toString();
|
||||||
Text("卫星数: $_satelliteCnt", style: const TextStyle(fontSize: 14)),
|
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. 选项卡 + 刷新按钮
|
// 3. 选项卡 + 刷新按钮
|
||||||
@@ -180,12 +128,19 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 4. 内容区域
|
// 4. 内容区域 - 使用 BlocBuilder 监听
|
||||||
SliverFillRemaining(
|
SliverFillRemaining(
|
||||||
child: Container(
|
child: BlocBuilder<DeviceStatusBloc, DeviceStatusState>(
|
||||||
margin: const EdgeInsets.all(16),
|
builder: (context, state) {
|
||||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)),
|
return Container(
|
||||||
child: _isCardView ? _buildCardContentView() : _buildChartContentView(),
|
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() {
|
Widget _buildCardItem(String title, String value) {
|
||||||
return SingleChildScrollView(
|
return Padding(
|
||||||
// 添加 SingleChildScrollView 使内容可滚动
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||||
padding: const EdgeInsets.all(20.0),
|
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(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text("设备实时状态", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
const Text("设备实时状态", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_CardItem(title: "电压", value: "$_voltage V"),
|
_buildCardItem("电压", "$voltage V"),
|
||||||
_CardItem(title: "左轮速度", value: "$_leftSpeed rpm"),
|
_buildCardItem("左轮速度", "$leftSpeed rpm"),
|
||||||
_CardItem(title: "右轮速度", value: "$_rightSpeed rpm"),
|
_buildCardItem( "右轮速度", "$rightSpeed rpm"),
|
||||||
_CardItem(title: "航向角", value: "$_yaw°"),
|
_buildCardItem( "航向角", "$yaw°"),
|
||||||
_CardItem(title: "俯仰角", value: "$_pitch°"),
|
_buildCardItem( "俯仰角", "$pitch°"),
|
||||||
_CardItem(title: "翻滚角", value: "$_roll°"),
|
_buildCardItem( "翻滚角", "$roll°"),
|
||||||
_CardItem(title: "卫星数", value: _satelliteCnt),
|
_buildCardItem( "卫星数", satelliteCnt),
|
||||||
_CardItem(title: "定位质量", value: _qual),
|
_buildCardItem( "定位质量", qual),
|
||||||
_CardItem(title: "纬度", value: _latitude),
|
_buildCardItem( "纬度", latitude),
|
||||||
_CardItem(title: "经度", value: _longitude),
|
_buildCardItem( "经度", longitude),
|
||||||
_CardItem(title: "时间戳", value: _timeStamp),
|
/* //_buildCardItem( "时间戳", timeStamp),*/
|
||||||
_CardItem(title: "割刀速度", value: "$_knifeCuttingSpeed rpm"),
|
_buildCardItem( "割刀速度", "$knifeCuttingSpeed rpm"),
|
||||||
_CardItem(title: "控制模式", value: _controlMode),
|
_buildCardItem( "控制模式", controlMode),
|
||||||
_CardItem(title: "作业面积", value: "$_workingArea m²"),
|
_buildCardItem( "作业面积", "$workingArea m²"),
|
||||||
_CardItem(title: "电量", value: "$_battery%"),
|
_buildCardItem( "电量", "$battery%"),
|
||||||
_CardItem(title: "障碍物标志", value: _obstacleFlag),
|
_buildCardItem( "障碍物标志", obstacleFlag),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,25 +260,19 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
class _CardItem extends StatelessWidget {
|
void _refreshDeviceData() {
|
||||||
final String title;
|
final deviceState = context.read<DevicesCubit>().state;
|
||||||
final String value;
|
final currentDevice = deviceState.selectedDevice;
|
||||||
|
|
||||||
const _CardItem({required this.title, required this.value});
|
if (currentDevice != null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@override
|
const SnackBar(content: Text("数据刷新成功!"), duration: Duration(seconds: 1)),
|
||||||
Widget build(BuildContext context) {
|
);
|
||||||
return Padding(
|
} else {
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
child: Row(
|
const SnackBar(content: Text("暂无设备,无法刷新"), duration: Duration(seconds: 1)),
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
);
|
||||||
children: [
|
}
|
||||||
Text(title, style: const TextStyle(fontSize: 16, color: Color(0xFF333333))),
|
|
||||||
Text(value, style: const TextStyle(fontSize: 16, color: Color(0xFF666666))),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:maibu_satabot_v2/features/home/presentation/pages/home_page.dart
|
|||||||
|
|
||||||
import '../../../../core/di/injection.dart';
|
import '../../../../core/di/injection.dart';
|
||||||
import '../../../../core/router/route_paths.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/bloc/remote_control_cubit.dart';
|
||||||
import '../../../remote_control/presentation/pages/remote_control_page.dart';
|
import '../../../remote_control/presentation/pages/remote_control_page.dart';
|
||||||
import '../pages/route_plan_page.dart';
|
import '../pages/route_plan_page.dart';
|
||||||
@@ -27,7 +28,11 @@ class HomeRoutes {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: RoutePaths.runningStatus,
|
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