#连接库初步完成

This commit is contained in:
CC
2026-01-27 18:44:49 +08:00
parent 8c39f0daf8
commit 949743972c
82 changed files with 1187 additions and 650 deletions

View File

@@ -1,5 +1,5 @@
import '../models/user_model.dart';
abstract class AuthHttpDataSource {
abstract class IAuthHttpDataSource {
Future<UserModel> login(String username, String password, int sourceType);
}

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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,
);
}
}

View File

@@ -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,

View 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,
];
}

View File

@@ -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 {

View File

@@ -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';

View 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());
}
}

View 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];
}

View File

@@ -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();
}
}
}

View File

@@ -1,4 +1,4 @@
import 'package:maibu_satabot_v2/core/domain/entities/user_entity.dart';
import '../../domain/entities/user_entity.dart';
abstract class AuthState {}

View File

@@ -1,4 +1,4 @@
import '../../../../core/domain/entities/user_entity.dart';
import '../../domain/entities/user_entity.dart';
abstract class LoginState {}

View File

@@ -0,0 +1 @@
enum ControlMode { TCP, BLE, LOCAL, NONE, OTHER }

View File

@@ -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();
}
}

View File

@@ -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];
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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,

View File

@@ -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);

View File

@@ -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';

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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

View File

@@ -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 {

View File

@@ -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,
);
}

View File

@@ -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();
}
}

View File

@@ -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)
);
}
}

View File

@@ -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;
}

View File

@@ -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,
];
}

View File

@@ -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,
];
}

View File

@@ -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);
}

View File

@@ -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) {}
}
}

View File

@@ -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,
];
}
}

View File

@@ -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';

View File

@@ -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;