#连接库初步完成
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import '../models/user_model.dart';
|
||||
|
||||
abstract class AuthHttpDataSource {
|
||||
abstract class IAuthHttpDataSource {
|
||||
Future<UserModel> login(String username, String password, int sourceType);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import '../../domain/entities/user_entity.dart';
|
||||
|
||||
abstract class IAuthLocalDataSource {
|
||||
Future<void> saveUser(UserEntity user);
|
||||
Future<UserEntity?> getUser();
|
||||
Future<void> deleteUser();
|
||||
Future<bool> hasUser();
|
||||
}
|
||||
@@ -2,9 +2,9 @@ import 'package:dio/dio.dart';
|
||||
import 'package:maibu_satabot_v2/core/consts/http_api_consts.dart';
|
||||
|
||||
import '../../models/user_model.dart';
|
||||
import '../auth_http_datasource.dart';
|
||||
import '../i_auth_http_datasource.dart';
|
||||
|
||||
class AuthHttpDataSourceImpl implements AuthHttpDataSource {
|
||||
class AuthHttpDataSourceImpl implements IAuthHttpDataSource {
|
||||
final Dio dio;
|
||||
|
||||
AuthHttpDataSourceImpl(this.dio);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../models/user_model.dart';
|
||||
import '../../../domain/entities/user_entity.dart';
|
||||
import '../i_auth_local_datasource.dart';
|
||||
|
||||
class AuthLocalDatasourceImpl implements IAuthLocalDataSource {
|
||||
final SharedPreferences _prefs;
|
||||
static const _userKey = 'auth_user';
|
||||
|
||||
AuthLocalDatasourceImpl(this._prefs);
|
||||
|
||||
@override
|
||||
Future<void> saveUser(UserEntity user) async {
|
||||
// 1. 将对象转为 Map -> 2. 将 Map 转为 JSON 字符串
|
||||
final model = UserModel.fromEntity(user);
|
||||
await _prefs.setString(_userKey, json.encode(model.toJson()));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<UserEntity?> getUser() async {
|
||||
final jsonString = _prefs.getString(_userKey);
|
||||
if (jsonString == null) return null;
|
||||
|
||||
// 将 JSON 转回 Model,再转成 Entity 返回给业务层
|
||||
final model = UserModel.fromJson(json.decode(jsonString));
|
||||
return model.toEntity();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteUser() async {
|
||||
await _prefs.remove(_userKey);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> hasUser() async {
|
||||
return _prefs.containsKey(_userKey);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import 'package:maibu_satabot_v2/core/data/base_model.dart';
|
||||
|
||||
import '../../../../core/domain/entities/user_entity.dart';
|
||||
import '../../domain/entities/user_entity.dart';
|
||||
|
||||
class UserModel extends UserEntity implements BaseModel {
|
||||
|
||||
class UserModel extends UserEntity implements BaseModel<UserEntity> {
|
||||
UserModel({
|
||||
required super.userId,
|
||||
required super.username,
|
||||
@@ -26,6 +27,7 @@ class UserModel extends UserEntity implements BaseModel {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'userId': userId,
|
||||
@@ -38,17 +40,7 @@ class UserModel extends UserEntity implements BaseModel {
|
||||
};
|
||||
}
|
||||
|
||||
toEntity() {
|
||||
return UserEntity(
|
||||
userId: userId,
|
||||
username: username,
|
||||
nickname: nickname,
|
||||
token: token,
|
||||
avatar: avatar,
|
||||
email: email,
|
||||
phone: phone,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
factory UserModel.fromEntity(UserEntity entity) {
|
||||
return UserModel(
|
||||
@@ -61,4 +53,17 @@ class UserModel extends UserEntity implements BaseModel {
|
||||
phone: entity.phone,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
UserEntity toEntity() {
|
||||
return UserEntity(
|
||||
userId: userId,
|
||||
username: username,
|
||||
nickname: nickname,
|
||||
token: token,
|
||||
avatar: avatar,
|
||||
email: email,
|
||||
phone: phone,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:maibu_satabot_v2/core/storage/user_storage.dart';
|
||||
import 'package:maibu_satabot_v2/features/auth/data/datasources/i_auth_local_datasource.dart';
|
||||
|
||||
import '../../../../core/domain/entities/user_entity.dart';
|
||||
import '../../domain/entities/user_entity.dart';
|
||||
import '../../domain/errors/auth_failure.dart';
|
||||
import '../../domain/repositories/auth_repository.dart';
|
||||
import '../datasources/auth_http_datasource.dart';
|
||||
import '../datasources/i_auth_http_datasource.dart';
|
||||
|
||||
class AuthRepositoryImpl implements AuthRepository {
|
||||
final AuthHttpDataSource httpRemote;
|
||||
final UserStorage localTokenStorage;
|
||||
final IAuthHttpDataSource httpRemote;
|
||||
final IAuthLocalDataSource localTokenStorage;
|
||||
|
||||
AuthRepositoryImpl({
|
||||
required this.httpRemote,
|
||||
|
||||
32
lib/features/auth/domain/entities/user_entity.dart
Normal file
32
lib/features/auth/domain/entities/user_entity.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class UserEntity extends Equatable {
|
||||
final String userId;
|
||||
final String username;
|
||||
final String nickname;
|
||||
final String token;
|
||||
final String? avatar;
|
||||
final String? email;
|
||||
final String? phone;
|
||||
|
||||
UserEntity({
|
||||
required this.userId,
|
||||
required this.username,
|
||||
required this.nickname,
|
||||
required this.token,
|
||||
this.avatar,
|
||||
this.email,
|
||||
this.phone,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
userId,
|
||||
username,
|
||||
nickname,
|
||||
avatar,
|
||||
token,
|
||||
email,
|
||||
phone,
|
||||
];
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
|
||||
import '../../../../core/domain/entities/user_entity.dart';
|
||||
import '../entities/user_entity.dart';
|
||||
import '../errors/auth_failure.dart';
|
||||
|
||||
abstract class AuthRepository {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
|
||||
import '../../../../core/domain/entities/user_entity.dart';
|
||||
import '../../../../core/domain/usecases/base_usecase.dart';
|
||||
import '../entities/user_entity.dart';
|
||||
import '../errors/auth_failure.dart';
|
||||
import '../repositories/auth_repository.dart';
|
||||
|
||||
|
||||
30
lib/features/auth/presentation/bloc/app_user_cubit.dart
Normal file
30
lib/features/auth/presentation/bloc/app_user_cubit.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../data/datasources/i_auth_local_datasource.dart';
|
||||
import '../../domain/entities/user_entity.dart';
|
||||
import 'app_user_state.dart';
|
||||
|
||||
class AppUserCubit extends Cubit<AppUserState> {
|
||||
// 注入你的 UserStorage (之前在 core/storage 里的那个)
|
||||
final IAuthLocalDataSource _storage;
|
||||
|
||||
AppUserCubit(this._storage) : super(const AppUserState()) {
|
||||
// 初始化时尝试从本地加载
|
||||
_loadFromStorage();
|
||||
}
|
||||
|
||||
Future<void> _loadFromStorage() async {
|
||||
final savedUser = await _storage.getUser();
|
||||
if (savedUser != null) emit(AppUserState(user: savedUser));
|
||||
}
|
||||
|
||||
void setAuth(UserEntity user) {
|
||||
_storage.saveUser(user); // 持久化
|
||||
emit(AppUserState(user: user));
|
||||
}
|
||||
|
||||
void clearAuth() {
|
||||
_storage.deleteUser(); // 清除本地数据
|
||||
emit(const AppUserState());
|
||||
}
|
||||
}
|
||||
21
lib/features/auth/presentation/bloc/app_user_state.dart
Normal file
21
lib/features/auth/presentation/bloc/app_user_state.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import '../../domain/entities/user_entity.dart';
|
||||
|
||||
|
||||
class AppUserState extends Equatable {
|
||||
final UserEntity? user;
|
||||
|
||||
const AppUserState({this.user});
|
||||
|
||||
bool get isLoggedIn => user != null;
|
||||
|
||||
AppUserState copyWith({UserEntity? user, bool clearUser = false}) {
|
||||
return AppUserState(
|
||||
user: clearUser ? null : (user ?? this.user),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [user];
|
||||
}
|
||||
@@ -1,77 +1,77 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:maibu_satabot_v2/core/consts/tcp_consts.dart';
|
||||
import 'package:maibu_satabot_v2/core/domain/entities/user_entity.dart';
|
||||
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 'package:maibu_satabot_v2/core/network/enums/control_mode.dart'; // 引入
|
||||
import 'package:maibu_satabot_v2/core/network/tcp/tcp_connection.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../core/protocol/protocol_constants.dart';
|
||||
import '../../../connectivity/presentation/bloc/connectivity_cubit.dart';
|
||||
import '../../domain/entities/user_entity.dart';
|
||||
import 'app_user_cubit.dart';
|
||||
import '../../data/datasources/i_auth_local_datasource.dart';
|
||||
import 'auth_state.dart';
|
||||
|
||||
/// emit做的事情:
|
||||
/// 1.修改状态标志:通过改变类的类型(从 Initial 变成 Authenticated)
|
||||
/// 2.携带数据:把 user 对象塞进了状态里,让外部能拿到
|
||||
/// 3.发送通知:自动触发监听(如 GoRouter 的刷新)。
|
||||
class AuthCubit extends Cubit<AuthState> {
|
||||
final UserStorage storage;
|
||||
final TcpClient tcp;
|
||||
final IAuthLocalDataSource storage;
|
||||
final AppUserCubit appCubit;
|
||||
final NetMessageDispatcher dispatcher;
|
||||
StreamSubscription? _kickOutSub; // 新增:用于管理监听生命周期
|
||||
final ConnectivityCubit connectivityCubit;
|
||||
StreamSubscription? _eventSub;
|
||||
|
||||
AuthCubit(this.storage, this.tcp, this.appCubit, this.dispatcher)
|
||||
: super(AuthInitial()) {
|
||||
// Cubit 一启动就开始监听 TCP 的“自动逻辑”
|
||||
AuthCubit({
|
||||
required this.storage,
|
||||
required this.appCubit,
|
||||
required this.connectivityCubit,
|
||||
}) : super(AuthInitial()) {
|
||||
_listenToAuthResponse();
|
||||
}
|
||||
|
||||
/// App 启动时检查本地缓存
|
||||
Future<void> appStarted() async {
|
||||
final user = await storage.getUser();
|
||||
if (user != null) {
|
||||
// 1. 建立 TCP 连接 (远程控制)
|
||||
tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT);
|
||||
// 2. 同步全局 App 状态
|
||||
_connectTcp();
|
||||
appCubit.setAuth(user);
|
||||
// 3. 进入已登录状态
|
||||
emit(AuthAuthenticated(user));
|
||||
} else {
|
||||
emit(AuthUnauthenticated());
|
||||
}
|
||||
}
|
||||
|
||||
/// 当 HTTP 登录/注册成功后调用
|
||||
Future<void> loginSuccess(UserEntity user) async {
|
||||
await storage.saveUser(user);
|
||||
tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT);
|
||||
_connectTcp();
|
||||
appCubit.setAuth(user);
|
||||
emit(AuthAuthenticated(user));
|
||||
}
|
||||
|
||||
/// 退出登录 (主动或被动)
|
||||
void _connectTcp() {
|
||||
// 动态注入 TCP 连接
|
||||
final tcp = sl<TcpConnection>();
|
||||
|
||||
connectivityCubit.setConnection(tcp, ControlMode.TCP);
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
await storage.deleteUser();
|
||||
tcp.disconnect();
|
||||
// 现在 setConnection 接受 null 了,不会再爆红
|
||||
connectivityCubit.setConnection(null, ControlMode.NONE);
|
||||
appCubit.clearAuth();
|
||||
emit(AuthUnauthenticated());
|
||||
}
|
||||
|
||||
/// TCP 指令监听
|
||||
void _listenToAuthResponse() {
|
||||
_kickOutSub?.cancel(); // 防止重复监听
|
||||
_eventSub?.cancel();
|
||||
|
||||
// 假设 0x12 是踢下线或多设备登录提醒
|
||||
_kickOutSub = dispatcher.onJsonMessage(0x12).listen((json) {
|
||||
// 如果后端发来指令确认需要退出
|
||||
logout();
|
||||
// 监听全局解析后的 Packet 流
|
||||
_eventSub = connectivityCubit.activePacketStream
|
||||
.where((packet) => packet.cmdType == ProtocolConstants.cmdGetAuth)
|
||||
.listen((packet) {
|
||||
// 这里处理 0x04 授权逻辑
|
||||
print("Auth Response Received: ${packet.payload}");
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_kickOutSub?.cancel(); // 销毁 Cubit 时关闭监听
|
||||
_eventSub?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:maibu_satabot_v2/core/domain/entities/user_entity.dart';
|
||||
import '../../domain/entities/user_entity.dart';
|
||||
|
||||
abstract class AuthState {}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import '../../../../core/domain/entities/user_entity.dart';
|
||||
import '../../domain/entities/user_entity.dart';
|
||||
|
||||
abstract class LoginState {}
|
||||
|
||||
|
||||
1
lib/features/connectivity/domain/control_mode.dart
Normal file
1
lib/features/connectivity/domain/control_mode.dart
Normal file
@@ -0,0 +1 @@
|
||||
enum ControlMode { TCP, BLE, LOCAL, NONE, OTHER }
|
||||
@@ -0,0 +1,133 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../core/error/failure.dart';
|
||||
import '../../../../core/logging/i_logger_service.dart';
|
||||
import '../../../../core/network/base/i_connection.dart';
|
||||
import '../../../../core/network/enums/control_mode.dart';
|
||||
import '../../../../core/protocol/packet_entity.dart';
|
||||
import '../../../../core/protocol/packet_parser_transformer.dart';
|
||||
import '../../../../core/protocol/protocol_constants.dart';
|
||||
import 'connectivity_state.dart';
|
||||
|
||||
class ConnectivityCubit extends Cubit<ConnectivityState> {
|
||||
IConnection? _activeConnection;
|
||||
StreamSubscription? _statusSub;
|
||||
StreamSubscription<PacketEntity>? _packetSub;
|
||||
Timer? _watchdogTimer;
|
||||
|
||||
// 核心:这是唯一的流入口
|
||||
Stream<PacketEntity>? _broadcastPacketStream;
|
||||
|
||||
ConnectivityCubit() : super(ConnectivityState.initial());
|
||||
|
||||
void setConnection(IConnection? connection, ControlMode mode) {
|
||||
// 1. 彻底清理(按顺序来)
|
||||
_stopMonitoring();
|
||||
_statusSub?.cancel();
|
||||
_activeConnection?.disconnect();
|
||||
|
||||
// 2. 关键:切断旧流的所有引用
|
||||
_broadcastPacketStream = null;
|
||||
|
||||
_activeConnection = connection;
|
||||
|
||||
if (connection != null) {
|
||||
_statusSub = connection.statusStream.listen((status) {
|
||||
emit(state.copyWith(activeMode: mode, status: status));
|
||||
|
||||
if (status == ConnectionStatus.connected) {
|
||||
// 3. 只有连接成功时,才初始化唯一的广播流
|
||||
_ensureStreamInitialized();
|
||||
_startMonitoring();
|
||||
} else {
|
||||
_stopMonitoring();
|
||||
}
|
||||
});
|
||||
connection.connect();
|
||||
} else {
|
||||
emit(state.copyWith(activeMode: ControlMode.NONE, status: ConnectionStatus.disconnected));
|
||||
}
|
||||
}
|
||||
|
||||
// 核心:强制初始化,确保 transform 只跑一次
|
||||
void _ensureStreamInitialized() {
|
||||
if (_activeConnection != null && _broadcastPacketStream == null) {
|
||||
print("DEBUG: 正在创建唯一的广播流转换器");
|
||||
|
||||
// 确保 rawStream 是有效的
|
||||
if (_activeConnection!.rawStream != null) {
|
||||
_broadcastPacketStream = _activeConnection!.rawStream
|
||||
.transform(PacketParserTransformer())
|
||||
.asBroadcastStream();
|
||||
} else {
|
||||
print("DEBUG: rawStream is null,无法初始化广播流");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void _startMonitoring() {
|
||||
_stopMonitoring();
|
||||
_resetWatchdog();
|
||||
|
||||
// 确保流已存在
|
||||
_ensureStreamInitialized();
|
||||
|
||||
// 此时 listen 的是已经转换好的广播流,绝对不会触发 rawStream.transform
|
||||
_packetSub = _broadcastPacketStream?.listen((packet) {
|
||||
_resetWatchdog();
|
||||
if (packet.cmdType == 0xFF) {
|
||||
sendCommand(0xFF, null);
|
||||
sl<ILoggerService>().log("receive heartbeat,has response...");
|
||||
}
|
||||
}, onError: (e) => print("流监听错误: $e"));
|
||||
}
|
||||
|
||||
// Getter 也要改,保证它只返回已经创建好的流
|
||||
Stream<PacketEntity> get activePacketStream {
|
||||
_ensureStreamInitialized();
|
||||
return _broadcastPacketStream ?? const Stream.empty();
|
||||
}
|
||||
|
||||
// --- 其他逻辑保持不变 ---
|
||||
void _resetWatchdog() {
|
||||
_watchdogTimer?.cancel();
|
||||
_watchdogTimer = Timer(const Duration(seconds: 10), () {
|
||||
print("警告:10秒内数据,触发断开逻辑");
|
||||
emit(state.copyWith(status: ConnectionStatus.disconnected));
|
||||
});
|
||||
}
|
||||
|
||||
void _stopMonitoring() {
|
||||
_packetSub?.cancel();
|
||||
_packetSub = null;
|
||||
_watchdogTimer?.cancel();
|
||||
_watchdogTimer = null;
|
||||
}
|
||||
|
||||
Future<Either<Failure, Unit>> sendCommand(int cmd, List<int>? payload) async {
|
||||
if (_activeConnection == null) return left(NetworkFailure("无连接"));
|
||||
|
||||
final frame = <int>[
|
||||
ProtocolConstants.header1,
|
||||
ProtocolConstants.header2,
|
||||
cmd,
|
||||
...?payload,
|
||||
if (payload != null && payload.isNotEmpty) ...[0x00, 0x00],
|
||||
ProtocolConstants.endFlag1,
|
||||
ProtocolConstants.endFlag2
|
||||
];
|
||||
|
||||
return await _activeConnection!.send(frame);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_stopMonitoring();
|
||||
_statusSub?.cancel();
|
||||
_activeConnection?.disconnect();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import '../../../../core/network/base/i_connection.dart';
|
||||
import '../../../../core/network/enums/control_mode.dart';
|
||||
|
||||
class ConnectivityState extends Equatable{
|
||||
final ControlMode activeMode;
|
||||
final ConnectionStatus status;
|
||||
|
||||
const ConnectivityState({required this.activeMode, required this.status});
|
||||
|
||||
factory ConnectivityState.initial() => ConnectivityState(
|
||||
activeMode: ControlMode.NONE,
|
||||
status: ConnectionStatus.disconnected,
|
||||
);
|
||||
|
||||
ConnectivityState copyWith({ControlMode? activeMode, ConnectionStatus? status}) {
|
||||
return ConnectivityState(
|
||||
activeMode: activeMode ?? this.activeMode,
|
||||
status: status ?? this.status,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
// TODO: implement props
|
||||
List<Object?> get props => [activeMode, status];
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity.dart';
|
||||
|
||||
abstract class DeviceHttpDatasource {
|
||||
abstract class IDeviceHttpDatasource {
|
||||
Future<List<DeviceEntity>> getUserDevices(String username);
|
||||
Future<int> bindDevice(String deviceId, String deviceAlias);
|
||||
Future<int> unbindDevice(String deviceId);
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:dio/dio.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/i_device_http_datasource.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity.dart';
|
||||
|
||||
class DeviceHttpDatasourceImpl implements DeviceHttpDatasource {
|
||||
class DeviceHttpDatasourceImpl implements IDeviceHttpDatasource {
|
||||
final Dio dio;
|
||||
|
||||
DeviceHttpDatasourceImpl(this.dio);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:maibu_satabot_v2/core/data/base_model.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity.dart';
|
||||
|
||||
class DeviceModel extends DeviceEntity implements BaseModel {
|
||||
class DeviceModel extends DeviceEntity implements BaseModel<DeviceEntity> {
|
||||
DeviceModel({
|
||||
required super.deviceName,
|
||||
required super.productId,
|
||||
@@ -30,6 +30,7 @@ class DeviceModel extends DeviceEntity implements BaseModel {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'deviceName': deviceName,
|
||||
@@ -45,7 +46,8 @@ class DeviceModel extends DeviceEntity implements BaseModel {
|
||||
};
|
||||
}
|
||||
|
||||
toEntity() {
|
||||
@override
|
||||
DeviceEntity toEntity() {
|
||||
return DeviceEntity(
|
||||
deviceName: deviceName,
|
||||
productId: productId,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:fpdart/src/either.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/data/datasources/device_http_datasource.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/data/datasources/i_device_http_datasource.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 '../../domain/repositories/device_repository.dart';
|
||||
|
||||
class DeviceRepositoryImpl implements DeviceRepository {
|
||||
DeviceHttpDatasource _deviceHttpDatasource;
|
||||
IDeviceHttpDatasource _deviceHttpDatasource;
|
||||
|
||||
DeviceRepositoryImpl(this._deviceHttpDatasource);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/auth/presentation/bloc/app_user_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_cubit.dart';
|
||||
|
||||
import '../widgets/ImmersionHeader.dart';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../core/app/app_user_cubit.dart';
|
||||
import '../../../auth/presentation/bloc/app_user_cubit.dart';
|
||||
import '../../../devices/presentation/bloc/devices_cubit.dart';
|
||||
|
||||
class RoutePlanPage extends StatelessWidget {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../core/app/app_user_cubit.dart';
|
||||
import '../../../auth/presentation/bloc/app_user_cubit.dart';
|
||||
import '../../../devices/presentation/bloc/devices_cubit.dart';
|
||||
|
||||
class RunningStatusPage extends StatelessWidget {
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity.dart';
|
||||
|
||||
import '../../../../core/app/app_user_cubit.dart';
|
||||
import '../../../auth/presentation/bloc/app_user_cubit.dart';
|
||||
import '../../../devices/presentation/bloc/devices_cubit.dart';
|
||||
import '../../../devices/presentation/bloc/devices_state.dart';
|
||||
// 导入你的主题文件以获取 offWhite
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
class RemoteHttpDatasource {
|
||||
class IRemoteHttpDatasource {
|
||||
final Dio _dio;
|
||||
|
||||
RemoteHttpDatasource(this._dio);
|
||||
IRemoteHttpDatasource(this._dio);
|
||||
|
||||
/// 示例:获取控制授权 (对应之前逻辑中的权限申请)
|
||||
Future<Map<String, dynamic>> requestControlAuth(String robotId) async {
|
||||
@@ -1,30 +0,0 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../../../../core/protocol/machine_protocol_codec.dart';
|
||||
|
||||
class LawnMoverProtocolModel {
|
||||
final int left;
|
||||
final int right;
|
||||
final int lift;
|
||||
final int mower;
|
||||
final int ignition;
|
||||
final int emergency;
|
||||
|
||||
LawnMoverProtocolModel({
|
||||
required this.left,
|
||||
required this.right,
|
||||
this.lift = 0,
|
||||
this.mower = 0,
|
||||
this.ignition = 0,
|
||||
this.emergency = 0,
|
||||
});
|
||||
|
||||
Uint8List toBytes() => MachineProtocolCodec.encodeRemoteControlPayload(
|
||||
left: left,
|
||||
right: right,
|
||||
lift: lift,
|
||||
mower: mower,
|
||||
ignition: ignition,
|
||||
emergency: emergency,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:maibu_satabot_v2/core/data/base_model.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/domain/entities/machine_control_entity.dart';
|
||||
|
||||
class MachineControlModel extends MachineControlEntity implements BaseModel<MachineControlEntity>{
|
||||
MachineControlModel({
|
||||
required super.leftSpeed,
|
||||
required super.rightSpeed,
|
||||
required super.chassisLift,
|
||||
required super.mowerSpeed,
|
||||
required super.ignitionStatus,
|
||||
required super.isEmergency,
|
||||
});
|
||||
|
||||
factory MachineControlModel.fromEntity(MachineControlEntity entity) {
|
||||
return MachineControlModel(
|
||||
leftSpeed: entity.leftSpeed,
|
||||
rightSpeed: entity.rightSpeed,
|
||||
chassisLift: entity.chassisLift,
|
||||
mowerSpeed: entity.mowerSpeed,
|
||||
ignitionStatus: entity.ignitionStatus,
|
||||
isEmergency: entity.isEmergency,
|
||||
);
|
||||
}
|
||||
|
||||
/// 需要传入计算好的左右轮速度
|
||||
Uint8List toRawPacket(Map<String, int> speeds) {
|
||||
// 1. 调用 Codec:生成协议定义的 8 字节 Payload
|
||||
final payload = encodeRemoteControlPayload(
|
||||
left: speeds['left'] ?? 0,
|
||||
right: speeds['right'] ?? 0,
|
||||
lift: chassisLift,
|
||||
mower: mowerSpeed,
|
||||
ignition: ignitionStatus,
|
||||
emergency: isEmergency ? 1 : 0,
|
||||
);
|
||||
|
||||
// 2. 调用 Builder:封装帧头、指令、CRC、帧尾
|
||||
return payload;
|
||||
}
|
||||
|
||||
@override
|
||||
MachineControlEntity toEntity() {
|
||||
return MachineControlEntity(
|
||||
leftSpeed: leftSpeed,
|
||||
rightSpeed: rightSpeed,
|
||||
chassisLift: chassisLift,
|
||||
mowerSpeed: mowerSpeed,
|
||||
ignitionStatus: ignitionStatus,
|
||||
isEmergency: isEmergency,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'leftSpeed': leftSpeed,
|
||||
'rightSpeed': rightSpeed,
|
||||
'chassisLift': chassisLift,
|
||||
'mowerSpeed': mowerSpeed,
|
||||
'ignitionStatus': ignitionStatus,
|
||||
'isEmergency': isEmergency,
|
||||
};
|
||||
}
|
||||
|
||||
Uint8List encodeRemoteControlPayload({
|
||||
required int left,
|
||||
required int right,
|
||||
int lift = 0,
|
||||
int mower = 0,
|
||||
int ignition = 0,
|
||||
int emergency = 0,
|
||||
}) {
|
||||
final bd = ByteData(8); // 只分配 8 字节
|
||||
int pos = 0;
|
||||
|
||||
bd.setInt16(pos, left, Endian.little);
|
||||
pos += 2;
|
||||
bd.setInt16(pos, right, Endian.little);
|
||||
pos += 2;
|
||||
bd.setUint8(pos++, lift);
|
||||
bd.setUint8(pos++, mower);
|
||||
bd.setUint8(pos++, ignition);
|
||||
bd.setUint8(pos++, emergency);
|
||||
|
||||
return bd.buffer.asUint8List();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,103 +1,132 @@
|
||||
class RunningStatusModel {
|
||||
final String voltage;
|
||||
final String leftTargetSpeed;
|
||||
final String rightTargetSpeed;
|
||||
final String leftMeasureSpeed;
|
||||
final String rightMeasureSpeed;
|
||||
final String leftCurrent;
|
||||
final String rightCurrent;
|
||||
final String leftMotorTemp;
|
||||
final String rightMotorTemp;
|
||||
final String chipTemp;
|
||||
final String yaw;
|
||||
final String pitch;
|
||||
final String roll;
|
||||
final String satelliteCnt;
|
||||
final String qual;
|
||||
final String headingStatus;
|
||||
final String latitude;
|
||||
final String longitude;
|
||||
final String time;
|
||||
final String knifeCuttingSpeed;
|
||||
final String controlMode;
|
||||
final String workingArea;
|
||||
final String battery;
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:maibu_satabot_v2/core/data/base_model.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/domain/entities/running_status_entity.dart';
|
||||
|
||||
import '../../../../core/network/enums/control_mode.dart';
|
||||
|
||||
class RunningStatusModel extends RunningStatusEntity
|
||||
implements BaseModel<RunningStatusEntity> {
|
||||
RunningStatusModel({
|
||||
this.voltage = '',
|
||||
this.leftTargetSpeed = '',
|
||||
this.rightTargetSpeed = '',
|
||||
this.leftMeasureSpeed = '',
|
||||
this.rightMeasureSpeed = '',
|
||||
this.leftCurrent = '',
|
||||
this.rightCurrent = '',
|
||||
this.leftMotorTemp = '',
|
||||
this.rightMotorTemp = '',
|
||||
this.chipTemp = '',
|
||||
this.yaw = '',
|
||||
this.pitch = '',
|
||||
this.roll = '',
|
||||
this.satelliteCnt = '',
|
||||
this.qual = '',
|
||||
this.headingStatus = '',
|
||||
this.latitude = '',
|
||||
this.longitude = '',
|
||||
this.time = '',
|
||||
this.knifeCuttingSpeed = '',
|
||||
this.controlMode = '',
|
||||
this.workingArea = '',
|
||||
this.battery = '',
|
||||
required super.voltage,
|
||||
required super.leftTargetSpeed,
|
||||
required super.rightTargetSpeed,
|
||||
required super.leftMeasureSpeed,
|
||||
required super.rightMeasureSpeed,
|
||||
required super.leftCurrent,
|
||||
required super.rightCurrent,
|
||||
required super.leftMotorTemp,
|
||||
required super.rightMotorTemp,
|
||||
required super.chipTemp,
|
||||
required super.yaw,
|
||||
required super.pitch,
|
||||
required super.roll,
|
||||
required super.satelliteCnt,
|
||||
required super.qual,
|
||||
required super.headingStatus,
|
||||
required super.latitude,
|
||||
required super.longitude,
|
||||
required super.time,
|
||||
required super.knifeCuttingSpeed,
|
||||
required super.controlMode,
|
||||
required super.workingArea,
|
||||
required super.battery,
|
||||
required super.obstacleFlag,
|
||||
});
|
||||
|
||||
RunningStatusModel copyWith({
|
||||
String? voltage,
|
||||
String? leftTargetSpeed,
|
||||
String? rightTargetSpeed,
|
||||
String? leftMeasureSpeed,
|
||||
String? rightMeasureSpeed,
|
||||
String? leftCurrent,
|
||||
String? rightCurrent,
|
||||
String? leftMotorTemp,
|
||||
String? rightMotorTemp,
|
||||
String? chipTemp,
|
||||
String? yaw,
|
||||
String? pitch,
|
||||
String? roll,
|
||||
String? satelliteCnt,
|
||||
String? qual,
|
||||
String? headingStatus,
|
||||
String? latitude,
|
||||
String? longitude,
|
||||
String? time,
|
||||
String? knifeCuttingSpeed,
|
||||
String? controlMode,
|
||||
String? workingArea,
|
||||
String? battery,
|
||||
}) {
|
||||
@override
|
||||
RunningStatusEntity toEntity() => this;
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'voltage': voltage,
|
||||
'leftTargetSpeed': leftTargetSpeed,
|
||||
'rightTargetSpeed': rightTargetSpeed,
|
||||
'leftMeasureSpeed': leftMeasureSpeed,
|
||||
'rightMeasureSpeed': rightMeasureSpeed,
|
||||
'leftCurrent': leftCurrent,
|
||||
'rightCurrent': rightCurrent,
|
||||
'leftMotorTemp': leftMotorTemp,
|
||||
'rightMotorTemp': rightMotorTemp,
|
||||
'chipTemp': chipTemp,
|
||||
'yaw': yaw,
|
||||
'pitch': pitch,
|
||||
'roll': roll,
|
||||
'satelliteCnt': satelliteCnt,
|
||||
'qual': qual,
|
||||
'headingStatus': headingStatus,
|
||||
'latitude': latitude,
|
||||
'longitude': longitude,
|
||||
'time': time,
|
||||
'knifeCuttingSpeed': knifeCuttingSpeed,
|
||||
'controlMode': controlMode,
|
||||
'workingArea': workingArea,
|
||||
'battery': battery,
|
||||
'obstacleFlag': obstacleFlag,
|
||||
};
|
||||
}
|
||||
|
||||
factory RunningStatusModel.fromBytes(Uint8List payload) {
|
||||
// 将 Uint8List 转换为字符串 (通常是 utf8 或 ascii)
|
||||
final rawString = utf8.decode(payload);
|
||||
|
||||
// 按逗号分割成数组
|
||||
final parts = rawString.split(',');
|
||||
|
||||
// 辅助函数:安全转换数字,防止硬件发空字符串导致崩溃
|
||||
double parseD(int index) =>
|
||||
(index < parts.length && parts[index].isNotEmpty)
|
||||
? double.tryParse(parts[index]) ?? 0.0
|
||||
: 0.0;
|
||||
|
||||
int parseI(int index) => (index < parts.length && parts[index].isNotEmpty)
|
||||
? int.tryParse(parts[index]) ?? 0
|
||||
: 0;
|
||||
|
||||
String parseS(int index) => (index < parts.length) ? parts[index] : "";
|
||||
|
||||
ControlMode mapMode(int val) {
|
||||
switch (val) {
|
||||
case 1:
|
||||
return ControlMode.LOCAL;
|
||||
case 2:
|
||||
return ControlMode.BLE;
|
||||
case 3:
|
||||
return ControlMode.TCP;
|
||||
case 4:
|
||||
return ControlMode.OTHER;
|
||||
default:
|
||||
return ControlMode.NONE; // 0 或其他异常值
|
||||
}
|
||||
}
|
||||
|
||||
return RunningStatusModel(
|
||||
voltage: voltage ?? this.voltage,
|
||||
leftTargetSpeed: leftTargetSpeed ?? this.leftTargetSpeed,
|
||||
rightTargetSpeed: rightTargetSpeed ?? this.rightTargetSpeed,
|
||||
leftMeasureSpeed: leftMeasureSpeed ?? this.leftMeasureSpeed,
|
||||
rightMeasureSpeed: rightMeasureSpeed ?? this.rightMeasureSpeed,
|
||||
leftCurrent: leftCurrent ?? this.leftCurrent,
|
||||
rightCurrent: rightCurrent ?? this.rightCurrent,
|
||||
leftMotorTemp: leftMotorTemp ?? this.leftMotorTemp,
|
||||
rightMotorTemp: rightMotorTemp ?? this.rightMotorTemp,
|
||||
chipTemp: chipTemp ?? this.chipTemp,
|
||||
yaw: yaw ?? this.yaw,
|
||||
pitch: pitch ?? this.pitch,
|
||||
roll: roll ?? this.roll,
|
||||
satelliteCnt: satelliteCnt ?? this.satelliteCnt,
|
||||
qual: qual ?? this.qual,
|
||||
headingStatus: headingStatus ?? this.headingStatus,
|
||||
latitude: latitude ?? this.latitude,
|
||||
longitude: longitude ?? this.longitude,
|
||||
time: time ?? this.time,
|
||||
knifeCuttingSpeed: knifeCuttingSpeed ?? this.knifeCuttingSpeed,
|
||||
controlMode: controlMode ?? this.controlMode,
|
||||
workingArea: workingArea ?? this.workingArea,
|
||||
battery: battery ?? this.battery,
|
||||
voltage: parseD(0), // 电压
|
||||
leftTargetSpeed: parseI(1), // 左轮目标速度
|
||||
rightTargetSpeed: parseI(2), // 右轮目标速度
|
||||
leftMeasureSpeed: parseI(3), // 左轮测量速度
|
||||
rightMeasureSpeed: parseI(4), // 右轮测量速度
|
||||
leftCurrent: parseD(5), // 左轮电流
|
||||
rightCurrent: parseD(6), // 右轮电流
|
||||
leftMotorTemp: parseD(7), // 左轮温度
|
||||
rightMotorTemp: parseD(8), // 右轮温度
|
||||
chipTemp: parseD(9), // 芯片温度
|
||||
yaw: parseD(10), // 偏航角
|
||||
pitch: parseD(11), // 仰俯角
|
||||
roll: parseD(12), // 翻滚角
|
||||
satelliteCnt: parseI(13), // 卫星数
|
||||
qual: parseI(14), // 定位质量 (0-7)
|
||||
headingStatus: parseI(15), // 航向角状态
|
||||
latitude: parseD(16), // 纬度
|
||||
longitude: parseD(17), // 经度
|
||||
time: parseS(18), // 时间字符串
|
||||
knifeCuttingSpeed: parseI(19), // 割刀速度
|
||||
controlMode: mapMode(20), // 控制模式 (0-4)
|
||||
battery: parseD(21), // 电池电量 (0-100)
|
||||
workingArea: parseD(22), // 作业面积
|
||||
obstacleFlag: parseI(23), // 障碍物标志 (0/1)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,65 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'package:maibu_satabot_v2/core/protocol/protocol_constants.dart';
|
||||
import 'package:maibu_satabot_v2/core/protocol/packet_entity.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/data/models/machine_control_model.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/domain/entities/running_status_entity.dart';
|
||||
|
||||
import '../../../../core/network/protocol_decoder.dart';
|
||||
import '../../../../core/network/tcp/tcp_client.dart';
|
||||
import '../../../../core/protocol/machine_protocol_codec.dart';
|
||||
import '../../../../core/protocol/machine_protocol_constants.dart';
|
||||
import '../../domain/entities/machine_control_status_entity.dart';
|
||||
import '../../../connectivity/presentation/bloc/connectivity_cubit.dart';
|
||||
import '../../domain/entities/machine_control_entity.dart';
|
||||
import '../../domain/repositories/remote_control_repository.dart';
|
||||
import '../../domain/usecase/diff_steer_usecase.dart';
|
||||
import '../models/running_status_model.dart';
|
||||
|
||||
class RemoteControlRepositoryImpl implements RemoteControlRepository {
|
||||
final TcpClient _tcpClient;
|
||||
final ConnectivityCubit _connectivityCubit;
|
||||
final DiffSteerUseCase _diffSteer;
|
||||
|
||||
RemoteControlRepositoryImpl(this._tcpClient, this._diffSteer);
|
||||
RemoteControlRepositoryImpl(
|
||||
this._connectivityCubit, // 现在统一通过 Cubit 管理连接和分发
|
||||
this._diffSteer,
|
||||
);
|
||||
|
||||
@override
|
||||
void changeWebViewDirection(String direction) {
|
||||
// TODO: implement changeWebViewDirection
|
||||
// 逻辑保持不变
|
||||
}
|
||||
|
||||
@override
|
||||
void sendControlMachineCmd(MachineControlStatusEntity status) {
|
||||
// 1. 调用算法:将摇杆坐标 (x, y) 转换为左右轮电机转速
|
||||
final speeds = _diffSteer.calculate(status.originX, status.originY);
|
||||
Future<void> sendControlMachineCmd(MachineControlEntity entity) async {
|
||||
// 1. 调用差速算法计算左右轮速度
|
||||
final speeds = _diffSteer.calculate(entity.rightSpeed, entity.leftSpeed);
|
||||
|
||||
// 2. 调用 Codec:仅生成协议要求的 8 字节 Payload 负载数据
|
||||
final payload = MachineProtocolCodec.encodeRemoteControlPayload(
|
||||
left: speeds['left']!,
|
||||
right: speeds['right']!,
|
||||
lift: status.chassisLift,
|
||||
mower: status.mowerSpeed,
|
||||
ignition: status.ignitionStatus,
|
||||
emergency: status.isEmergency ? 1 : 0,
|
||||
);
|
||||
var model = MachineControlModel.fromEntity(entity);
|
||||
|
||||
// 3. 调用 TcpClient:发送指令。
|
||||
// TcpClient.sendRaw 会自动帮你加上 [0xAB, 0xAA] 头和 [0xAA, 0xAB] 尾
|
||||
_tcpClient.sendRaw(
|
||||
MachineProtocolConstants.cmdRemoteControl, // 这里通常是 0x00
|
||||
payload,
|
||||
var rawPacket = model.toRawPacket(speeds);
|
||||
|
||||
// 3. 通过连接管理器发出,内部会自动处理 [AB AA] ... [CRC] [AA AB]
|
||||
// 并且会执行 flush() 确保 100ms 实时性
|
||||
await _connectivityCubit.sendCommand(
|
||||
ProtocolConstants.cmdRemoteControl, // 0x00
|
||||
rawPacket,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// 过滤出 0x02 状态包并转换为业务 Entity
|
||||
@override
|
||||
Stream<RawPacket> get responseStream => _tcpClient.packetStream;
|
||||
}
|
||||
Stream<RunningStatusEntity> get runningStatusStream {
|
||||
return _connectivityCubit.activePacketStream
|
||||
.where((p) => p.cmdType == ProtocolConstants.cmdStatusInfo) // 0x02
|
||||
.map((packet) {
|
||||
// 第一步:由 Model 负责处理“脏活”(字符串拆分、类型转换)
|
||||
final model = RunningStatusModel.fromBytes(Uint8List.fromList(packet.payload));
|
||||
|
||||
// 第二步:转换为 Entity 交付给 UI 或 UseCase
|
||||
return model.toEntity();
|
||||
});
|
||||
}
|
||||
|
||||
/// 暴露原始解析流 (RawPacket 在我们这里对应 Packet 类)
|
||||
/// UI 或其他 Cubit 监听此流来处理机器人主动推送的消息
|
||||
@override
|
||||
Stream<PacketEntity> get responseStream => _connectivityCubit.activePacketStream;
|
||||
}
|
||||
@@ -1,21 +1,23 @@
|
||||
class MachineControlStatusEntity {
|
||||
final int originX;
|
||||
final int originY;
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class MachineControlEntity extends Equatable{
|
||||
final int leftSpeed;
|
||||
final int rightSpeed;
|
||||
final int chassisLift; // 0:停, 1:上, 2:下
|
||||
final int mowerSpeed; // 0:停, 1:+, 2:-
|
||||
final int ignitionStatus; // 0:停, 1:火, 2:熄
|
||||
final bool isEmergency;
|
||||
|
||||
MachineControlStatusEntity({
|
||||
this.originX = 0,
|
||||
this.originY = 0,
|
||||
MachineControlEntity({
|
||||
this.leftSpeed = 0,
|
||||
this.rightSpeed = 0,
|
||||
this.chassisLift = 0,
|
||||
this.mowerSpeed = 0,
|
||||
this.ignitionStatus = 0,
|
||||
this.isEmergency = false,
|
||||
});
|
||||
|
||||
MachineControlStatusEntity copyWith({
|
||||
MachineControlEntity copyWith({
|
||||
int? x,
|
||||
int? y,
|
||||
int? lift,
|
||||
@@ -23,13 +25,23 @@ class MachineControlStatusEntity {
|
||||
int? ignition,
|
||||
bool? emergency,
|
||||
}) {
|
||||
return MachineControlStatusEntity(
|
||||
originX: x ?? originX,
|
||||
originY: y ?? originY,
|
||||
return MachineControlEntity(
|
||||
leftSpeed: y ?? leftSpeed,
|
||||
rightSpeed: x ?? rightSpeed,
|
||||
chassisLift: lift ?? chassisLift,
|
||||
mowerSpeed: mower ?? mowerSpeed,
|
||||
ignitionStatus: ignition ?? ignitionStatus,
|
||||
isEmergency: emergency ?? isEmergency,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
leftSpeed,
|
||||
rightSpeed,
|
||||
chassisLift,
|
||||
mowerSpeed,
|
||||
ignitionStatus,
|
||||
isEmergency,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import '../../../../core/network/enums/control_mode.dart';
|
||||
import '../../data/models/running_status_model.dart';
|
||||
|
||||
class RunningStatusEntity extends Equatable {
|
||||
final double voltage; //电压
|
||||
final int leftTargetSpeed; //左目标速度
|
||||
final int rightTargetSpeed; //右目标速度
|
||||
final int leftMeasureSpeed; //左测速
|
||||
final int 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; //定位质量: 0:无效 1:GPS单点定位 2:DGPS 3:RTK固定解 4:RTK浮点解 5.手动输入模式
|
||||
final int headingStatus; //航向角状态: 0:未完成, 1:完成
|
||||
final double latitude; //纬度
|
||||
final double longitude; //经度
|
||||
final String time; //时间
|
||||
final int knifeCuttingSpeed; //割刀速度
|
||||
final ControlMode controlMode; //控制模式: 0.无控制 1.本地控制 2.蓝牙 3.TCP 4.其他接口
|
||||
final double battery; //电池电量
|
||||
final double workingArea; //作业面积
|
||||
final int obstacleFlag; //障碍物标志位 0:无 1:有
|
||||
|
||||
RunningStatusEntity({
|
||||
this.voltage = 0.0,
|
||||
this.leftTargetSpeed = 0,
|
||||
this.rightTargetSpeed = 0,
|
||||
this.leftMeasureSpeed = 0,
|
||||
this.rightMeasureSpeed = 0,
|
||||
this.leftCurrent = 0.0,
|
||||
this.rightCurrent = 0.0,
|
||||
this.leftMotorTemp = 0.0,
|
||||
this.rightMotorTemp = 0.0,
|
||||
this.chipTemp = 0.0,
|
||||
this.yaw = 0.0,
|
||||
this.pitch = 0.0,
|
||||
this.roll = 0.0,
|
||||
this.satelliteCnt = 0,
|
||||
this.qual = 0,
|
||||
this.headingStatus = 0,
|
||||
this.latitude = 0.0,
|
||||
this.longitude = 0.0,
|
||||
this.time = '',
|
||||
this.knifeCuttingSpeed = 0,
|
||||
this.controlMode = ControlMode.NONE,
|
||||
this.workingArea = 0.0,
|
||||
this.battery = 0.0,
|
||||
this.obstacleFlag = 0,
|
||||
});
|
||||
|
||||
factory RunningStatusEntity.fromModel(RunningStatusModel model) {
|
||||
return RunningStatusEntity(
|
||||
voltage: model.voltage,
|
||||
leftTargetSpeed: model.leftTargetSpeed,
|
||||
rightTargetSpeed: model.rightTargetSpeed,
|
||||
leftMeasureSpeed: model.leftMeasureSpeed,
|
||||
rightMeasureSpeed: model.rightMeasureSpeed,
|
||||
leftCurrent: model.leftCurrent,
|
||||
rightCurrent: model.rightCurrent,
|
||||
leftMotorTemp: model.leftMotorTemp,
|
||||
rightMotorTemp: model.rightMotorTemp,
|
||||
chipTemp: model.chipTemp,
|
||||
yaw: model.yaw,
|
||||
pitch: model.pitch,
|
||||
roll: model.roll,
|
||||
satelliteCnt: model.satelliteCnt,
|
||||
qual: model.qual,
|
||||
headingStatus: model.headingStatus,
|
||||
latitude: model.latitude,
|
||||
longitude: model.longitude,
|
||||
time: model.time,
|
||||
knifeCuttingSpeed: model.knifeCuttingSpeed,
|
||||
controlMode: model.controlMode,
|
||||
workingArea: model.workingArea,
|
||||
battery: model.battery,
|
||||
obstacleFlag: model.obstacleFlag,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
voltage,
|
||||
leftTargetSpeed,
|
||||
rightTargetSpeed,
|
||||
leftMeasureSpeed,
|
||||
rightMeasureSpeed,
|
||||
leftCurrent,
|
||||
rightCurrent,
|
||||
leftMotorTemp,
|
||||
rightMotorTemp,
|
||||
chipTemp,
|
||||
yaw,
|
||||
pitch,
|
||||
roll,
|
||||
satelliteCnt,
|
||||
qual,
|
||||
headingStatus,
|
||||
latitude,
|
||||
longitude,
|
||||
time,
|
||||
knifeCuttingSpeed,
|
||||
controlMode,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import '../../../../core/network/protocol_decoder.dart';
|
||||
import '../../domain/entities/machine_control_status_entity.dart';
|
||||
import 'package:maibu_satabot_v2/core/protocol/packet_entity.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/domain/entities/running_status_entity.dart';
|
||||
|
||||
import '../../domain/entities/machine_control_entity.dart';
|
||||
|
||||
abstract class RemoteControlRepository {
|
||||
/// 高频发送遥控指令 (被 Cubit 的 100ms 定时器调用)
|
||||
/// 负责将 Entity 转换为底层的 0x00 指令并发出
|
||||
void sendControlMachineCmd(MachineControlStatusEntity status);
|
||||
/// 高频发送遥控指令
|
||||
/// 修改建议:返回 Future<void> 没问题,但内部建议处理错误
|
||||
Future<void> sendControlMachineCmd(MachineControlEntity status);
|
||||
|
||||
/// 暴露解析后的回包流 (如 0x12 权限申请)
|
||||
/// UI 或 Cubit 监听此流来处理机器人主动推送的消息
|
||||
Stream<RawPacket> get responseStream;
|
||||
/// 状态信息流 (过滤后的 0x02)
|
||||
Stream<RunningStatusEntity> get runningStatusStream;
|
||||
|
||||
/// WebView 视角控制 (逻辑层面的切换,不涉及 TCP)
|
||||
/// 暴露解析后的原始包流 (统一改为 Packet)
|
||||
/// 这里的 Packet 已经是去掉了 AB AA 和 AA AB 的清洁包
|
||||
Stream<PacketEntity> get responseStream;
|
||||
|
||||
/// WebView 视角控制 (保持不变)
|
||||
void changeWebViewDirection(String direction);
|
||||
}
|
||||
|
||||
@@ -1,47 +1,100 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/data/models/running_status_model.dart';
|
||||
import 'package:maibu_satabot_v2/core/protocol/packet_entity.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/domain/entities/running_status_entity.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/presentation/bloc/remote_control_state.dart';
|
||||
|
||||
import '../../domain/entities/machine_control_status_entity.dart';
|
||||
import '../../domain/entities/machine_control_entity.dart';
|
||||
import '../../domain/repositories/remote_control_repository.dart';
|
||||
|
||||
class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
final RemoteControlRepository _repository;
|
||||
|
||||
// 定时器与流订阅管理
|
||||
Timer? _timer;
|
||||
StreamSubscription<RunningStatusEntity>? _statusSubscription;
|
||||
StreamSubscription<PacketEntity>? _responseSubscription;
|
||||
|
||||
RemoteControlCubit(this._repository)
|
||||
: super(
|
||||
RemoteControlState(
|
||||
controlEntity: MachineControlStatusEntity(),
|
||||
runningStatusModel: RunningStatusModel(),
|
||||
),
|
||||
) {
|
||||
: super(
|
||||
RemoteControlState(
|
||||
controlEntity: MachineControlEntity(),
|
||||
runningStatusEntity: RunningStatusEntity(),
|
||||
),
|
||||
) {
|
||||
// 构造时立即开启监听
|
||||
_initPacketListener();
|
||||
_initRunningStatusListener();
|
||||
}
|
||||
|
||||
// 1. 初始化回包监听 (如 0x12 权限)
|
||||
// ==========================================
|
||||
// 1. 数据监听逻辑 (Data Listening)
|
||||
// ==========================================
|
||||
|
||||
/// 监听原始回包 (处理如 0x12 权限申请等指令)
|
||||
void _initPacketListener() {
|
||||
_repository.responseStream.listen((packet) {
|
||||
if (packet.command == 0x12) {
|
||||
// 根据负载判断是否有权限,更新状态
|
||||
emit(state.copyWith(hasPermission: true));
|
||||
_responseSubscription?.cancel();
|
||||
_responseSubscription = _repository.responseStream.listen((packet) {
|
||||
// 处理权限申请回包 (假设 0x12)
|
||||
if (packet.cmdType == 0x12) {
|
||||
// 假设 payload[0] == 1 表示获得权限,这里可以解析出平台信息
|
||||
final bool granted = packet.payload.isNotEmpty && packet.payload[0] == 0x01;
|
||||
emit(state.copyWith(
|
||||
hasPermission: granted,
|
||||
// 假设协议中后续字节带有平台名称
|
||||
permissionPlatform: granted ? "Other Device" : "",
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 2. 开启 100ms 控制循环 (在进入遥控页面或点击“开始”时调用)
|
||||
/// 监听运行状态回包 (0x02) 并更新 UI
|
||||
void _initRunningStatusListener() {
|
||||
_statusSubscription?.cancel();
|
||||
_statusSubscription = _repository.runningStatusStream.listen((entity) {
|
||||
// 同步硬件状态到 Bloc State
|
||||
emit(state.copyWith(
|
||||
runningStatusEntity: entity,
|
||||
obstacleFlag: entity.obstacleFlag == 1 ? "检测到障碍物" : "无",
|
||||
// 如果硬件上报了急停或锁定,这里也可以同步更新状态
|
||||
isEmergency: entity.obstacleFlag == 1,
|
||||
));
|
||||
}, onError: (e) {
|
||||
emit(state.copyWith(errorMessage: "运行状态流监听异常: $e"));
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 2. 控制循环逻辑 (Control Loop)
|
||||
// ==========================================
|
||||
|
||||
/// 启动 100ms 高频控制循环
|
||||
void startControlLoop() {
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
|
||||
// 核心调用:直接把 state 里的实体丢给 repository
|
||||
_repository.sendControlMachineCmd(state.controlEntity);
|
||||
_timer = Timer.periodic(const Duration(milliseconds: 100), (timer) async {
|
||||
// 核心:在具备驾驶条件(有权限、未锁定、非急停)时发送指令
|
||||
if (state.canDrive) {
|
||||
try {
|
||||
await _repository.sendControlMachineCmd(state.controlEntity);
|
||||
} catch (e) {
|
||||
print("发送控制指令失败: $e");
|
||||
}
|
||||
}
|
||||
});
|
||||
emit(state.copyWith(status: RemoteControlStatus.controlling));
|
||||
}
|
||||
|
||||
// 3. 更新摇杆数据
|
||||
/// 停止控制循环
|
||||
void stopControlLoop() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
emit(state.copyWith(status: RemoteControlStatus.initial));
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 3. UI 交互方法 (UI Actions)
|
||||
// ==========================================
|
||||
|
||||
/// 更新摇杆坐标 (x, y 为 -100 到 100)
|
||||
void updateJoystick(double x, double y) {
|
||||
final updatedEntity = state.controlEntity.copyWith(
|
||||
x: x.toInt(),
|
||||
@@ -50,53 +103,65 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
emit(state.copyWith(controlEntity: updatedEntity));
|
||||
}
|
||||
|
||||
// 4. 更新功能开关 (比如割刀速度、灯光、点火等)
|
||||
void updateFunction({int? mower, int? lift, int? ignition, bool? emergency}) {
|
||||
final updatedEntity = state.controlEntity.copyWith(
|
||||
mower: mower,
|
||||
lift: lift,
|
||||
ignition: ignition,
|
||||
emergency: emergency,
|
||||
);
|
||||
/// 更新原点 Y 轴 (针对特定的 UI 逻辑)
|
||||
void updateOriginY(int y) {
|
||||
final updatedEntity = state.controlEntity.copyWith(y: y);
|
||||
emit(state.copyWith(controlEntity: updatedEntity));
|
||||
}
|
||||
|
||||
void updateOriginY(int y) {}
|
||||
|
||||
// 5. 停止控制循环
|
||||
void stopControlLoop() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
emit(state.copyWith(status: RemoteControlStatus.initial));
|
||||
/// 更新底盘升降
|
||||
void updateChassisLift(int level) {
|
||||
final updatedEntity = state.controlEntity.copyWith(lift: level);
|
||||
emit(state.copyWith(controlEntity: updatedEntity));
|
||||
}
|
||||
|
||||
void toggleLock() {}
|
||||
/// 更新急停状态
|
||||
void updateEmergency(bool emergency) {
|
||||
final updatedEntity = state.controlEntity.copyWith(emergency: emergency);
|
||||
emit(state.copyWith(
|
||||
controlEntity: updatedEntity,
|
||||
isEmergency: emergency,
|
||||
));
|
||||
}
|
||||
|
||||
/// 更新割刀/功能开关
|
||||
void updateMower(int speed) {
|
||||
final updatedEntity = state.controlEntity.copyWith(mower: speed);
|
||||
emit(state.copyWith(controlEntity: updatedEntity));
|
||||
}
|
||||
|
||||
/// 切换控制锁定
|
||||
void toggleLock() {
|
||||
emit(state.copyWith(isLocked: !state.isLocked));
|
||||
}
|
||||
|
||||
/// 权限请求弹窗控制
|
||||
void togglePermissionDialog(bool show) {
|
||||
emit(state.copyWith(showPermissionRequestDialog: show));
|
||||
}
|
||||
|
||||
void requestControlPermission() {
|
||||
// 1. 关闭弹窗
|
||||
emit(state.copyWith(showPermissionRequestDialog: false));
|
||||
|
||||
// 2. 这里执行你发送 0x12 指令的逻辑
|
||||
// _sendProtocolData(0x12, ...);
|
||||
/// 发起/响应权限请求
|
||||
void respondPermission(bool accept) {
|
||||
// 这里可以添加发送 0x12 响应包给硬件的逻辑
|
||||
emit(state.copyWith(
|
||||
hasPermission: accept,
|
||||
showPermissionRequestDialog: false,
|
||||
));
|
||||
}
|
||||
|
||||
/// 切换画中画显示
|
||||
void toggleLeftPip() => emit(state.copyWith(showLeftPip: !state.showLeftPip));
|
||||
void toggleRightPip() =>
|
||||
emit(state.copyWith(showRightPip: !state.showRightPip));
|
||||
void toggleRightPip() => emit(state.copyWith(showRightPip: !state.showRightPip));
|
||||
|
||||
// ==========================================
|
||||
// 4. 销毁与清理 (Cleanup)
|
||||
// ==========================================
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_timer?.cancel(); // 退出页面时务必销毁定时器
|
||||
_timer?.cancel();
|
||||
_statusSubscription?.cancel();
|
||||
_responseSubscription?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void updateChassisLift(int i) {}
|
||||
|
||||
void updateEmergency(bool bool) {}
|
||||
|
||||
void respondPermission(bool bool) {}
|
||||
}
|
||||
}
|
||||
@@ -1,88 +1,85 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/data/models/running_status_model.dart';
|
||||
|
||||
import '../../domain/entities/machine_control_status_entity.dart';
|
||||
import '../../domain/entities/machine_control_entity.dart';
|
||||
import '../../domain/entities/running_status_entity.dart';
|
||||
|
||||
enum RemoteControlStatus { initial, controlling, error }
|
||||
|
||||
enum ControlMode { TCP, BLE, LOCAL, NONE, OTHER }
|
||||
|
||||
class RemoteControlState extends Equatable {
|
||||
// --- 核心业务状态 ---
|
||||
final RemoteControlStatus status;
|
||||
final MachineControlStatusEntity controlEntity; // 控制业务实体
|
||||
final RunningStatusModel runningStatusModel;
|
||||
//状态业务实体
|
||||
final String? errorMessage;
|
||||
final bool hasPermission; // 是否获得了 0x12 权限
|
||||
final bool isEmergency;
|
||||
final bool isLocked;
|
||||
final int ping;
|
||||
final int battery;
|
||||
final int voltage;
|
||||
final String permissionPlatform;
|
||||
final String currentPlatform;
|
||||
final bool showPermissionRequestDialog;
|
||||
final bool showLeftPip;
|
||||
final bool showRightPip;
|
||||
final MachineControlEntity controlEntity; // 发送给机器的控制业务实体
|
||||
final RunningStatusEntity runningStatusEntity; // 机器回传的状态业务实体
|
||||
|
||||
final String obstacleFlag; //障碍物标志位
|
||||
// --- 权限与平台逻辑 ---
|
||||
final bool hasPermission; // 0x12 权限状态
|
||||
final String permissionPlatform; // 权限来源平台 (如 "Web", "App")
|
||||
final String currentPlatform; // 当前操作平台
|
||||
final bool showPermissionRequestDialog; // 是否显示权限请求弹窗
|
||||
|
||||
// --- 安全与锁定 ---
|
||||
final bool isEmergency; // 紧急停止状态
|
||||
final bool isLocked; // 控制锁定状态
|
||||
final String obstacleFlag; // 障碍物标志位文字 (对应 Entity 里的 0/1)
|
||||
|
||||
// --- 网络与连接 ---
|
||||
final int ping; // 延迟/心跳
|
||||
final String? errorMessage; // 错误提示
|
||||
|
||||
// --- UI 视图配置 ---
|
||||
final bool showLeftPip; // 左侧画中画显示
|
||||
final bool showRightPip; // 右侧画中画显示
|
||||
|
||||
const RemoteControlState({
|
||||
this.status = RemoteControlStatus.initial,
|
||||
required this.controlEntity,
|
||||
this.errorMessage,
|
||||
required this.runningStatusEntity,
|
||||
this.hasPermission = false,
|
||||
this.isEmergency = false,
|
||||
this.isLocked = false,
|
||||
this.ping = 0,
|
||||
this.battery = 0,
|
||||
this.voltage = 0,
|
||||
this.permissionPlatform = '',
|
||||
this.currentPlatform = '',
|
||||
this.showPermissionRequestDialog = false,
|
||||
this.isEmergency = false,
|
||||
this.isLocked = false,
|
||||
this.obstacleFlag = '无',
|
||||
this.ping = 0,
|
||||
this.errorMessage,
|
||||
this.showLeftPip = true,
|
||||
this.showRightPip = true,
|
||||
this.obstacleFlag = '',
|
||||
required this.runningStatusModel,
|
||||
});
|
||||
|
||||
// 方便 UI 更新部分属性
|
||||
// 辅助属性:判断当前是否具备物理驾驶条件
|
||||
bool get canDrive => hasPermission && !isEmergency && !isLocked;
|
||||
|
||||
RemoteControlState copyWith({
|
||||
RemoteControlStatus? status,
|
||||
MachineControlStatusEntity? controlEntity,
|
||||
String? errorMessage,
|
||||
MachineControlEntity? controlEntity,
|
||||
RunningStatusEntity? runningStatusEntity,
|
||||
bool? hasPermission,
|
||||
bool? isEmergency,
|
||||
bool? isLocked,
|
||||
int? ping,
|
||||
int? battery,
|
||||
int? voltage,
|
||||
String? permissionPlatform,
|
||||
String? currentPlatform,
|
||||
bool? showPermissionRequestDialog,
|
||||
bool? isEmergency,
|
||||
bool? isLocked,
|
||||
String? obstacleFlag,
|
||||
int? ping,
|
||||
String? errorMessage,
|
||||
bool? showLeftPip,
|
||||
bool? showRightPip,
|
||||
RunningStatusModel? runningStatusModel,
|
||||
String? obstacleFlag,
|
||||
}) {
|
||||
return RemoteControlState(
|
||||
status: status ?? this.status,
|
||||
controlEntity: controlEntity ?? this.controlEntity,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
runningStatusEntity: runningStatusEntity ?? this.runningStatusEntity,
|
||||
hasPermission: hasPermission ?? this.hasPermission,
|
||||
isEmergency: isEmergency ?? this.isEmergency,
|
||||
isLocked: isLocked ?? this.isLocked,
|
||||
ping: ping ?? this.ping,
|
||||
battery: battery ?? this.battery,
|
||||
voltage: voltage ?? this.voltage,
|
||||
permissionPlatform: permissionPlatform ?? this.permissionPlatform,
|
||||
currentPlatform: currentPlatform ?? this.currentPlatform,
|
||||
showPermissionRequestDialog:
|
||||
showPermissionRequestDialog ?? this.showPermissionRequestDialog,
|
||||
showPermissionRequestDialog: showPermissionRequestDialog ?? this.showPermissionRequestDialog,
|
||||
isEmergency: isEmergency ?? this.isEmergency,
|
||||
isLocked: isLocked ?? this.isLocked,
|
||||
obstacleFlag: obstacleFlag ?? this.obstacleFlag,
|
||||
ping: ping ?? this.ping,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
showLeftPip: showLeftPip ?? this.showLeftPip,
|
||||
showRightPip: showRightPip ?? this.showRightPip,
|
||||
runningStatusModel: runningStatusModel ?? this.runningStatusModel,
|
||||
obstacleFlag: obstacleFlag ?? this.obstacleFlag,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -90,19 +87,17 @@ class RemoteControlState extends Equatable {
|
||||
List<Object?> get props => [
|
||||
status,
|
||||
controlEntity,
|
||||
errorMessage,
|
||||
runningStatusEntity,
|
||||
hasPermission,
|
||||
isEmergency,
|
||||
isLocked,
|
||||
ping,
|
||||
battery,
|
||||
voltage,
|
||||
permissionPlatform,
|
||||
currentPlatform,
|
||||
showPermissionRequestDialog,
|
||||
isEmergency,
|
||||
isLocked,
|
||||
obstacleFlag,
|
||||
ping,
|
||||
errorMessage,
|
||||
showLeftPip,
|
||||
showRightPip,
|
||||
runningStatusModel,
|
||||
obstacleFlag,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:maibu_satabot_v2/core/consts/tcp_consts.dart';
|
||||
|
||||
import '../../../../core/app/app_user_cubit.dart';
|
||||
import '../../../auth/presentation/bloc/app_user_cubit.dart';
|
||||
import '../../../devices/presentation/bloc/devices_cubit.dart';
|
||||
import '../bloc/remote_control_cubit.dart';
|
||||
import '../bloc/remote_control_state.dart';
|
||||
|
||||
@@ -2,9 +2,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/presentation/bloc/remote_control_state.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/presentation/widgets/status_chip.dart';
|
||||
|
||||
import '../../../../core/network/enums/control_mode.dart';
|
||||
import '../../../devices/presentation/bloc/devices_cubit.dart';
|
||||
import '../bloc/remote_control_cubit.dart';
|
||||
|
||||
@@ -60,12 +60,11 @@ class TopStatusBar extends StatelessWidget {
|
||||
|
||||
const Spacer(),
|
||||
|
||||
// TODO
|
||||
_buildControlModeChip(,remoteState.runningStatusModel.controlMode),
|
||||
_buildControlModeChip(remoteState.runningStatusEntity.controlMode),
|
||||
|
||||
const SizedBox(width: 8),
|
||||
|
||||
_buildVoltageChip(remoteState.voltage),
|
||||
_buildVoltageChip(remoteState.runningStatusEntity.voltage),
|
||||
|
||||
const SizedBox(width: 8),
|
||||
|
||||
@@ -74,7 +73,7 @@ class TopStatusBar extends StatelessWidget {
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// 6. 电量 (对应 StatusChipRight)
|
||||
_buildBatteryChip(remoteState.battery),
|
||||
_buildBatteryChip(remoteState.runningStatusEntity.battery),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -138,9 +137,7 @@ class TopStatusBar extends StatelessWidget {
|
||||
case ControlMode.OTHER:
|
||||
displayText = '其他模式';
|
||||
break;
|
||||
default:
|
||||
displayText = '未知模式';
|
||||
}
|
||||
}
|
||||
|
||||
return StatusChip(
|
||||
text: displayText,
|
||||
@@ -150,7 +147,7 @@ class TopStatusBar extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVoltageChip(int voltage) {
|
||||
Widget _buildVoltageChip(double voltage) {
|
||||
return StatusChip(
|
||||
text: "$voltage V",
|
||||
color: Color(0xFFFACC15).withValues(alpha: 0.6),
|
||||
@@ -159,7 +156,7 @@ class TopStatusBar extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBatteryChip(int level) {
|
||||
Widget _buildBatteryChip(double level) {
|
||||
// 1. 根据数值判断图片路径 (逻辑与之前一致)
|
||||
String svgPath;
|
||||
Color color;
|
||||
|
||||
Reference in New Issue
Block a user