#连接库初步完成

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 +0,0 @@
<EFBFBD><EFBFBD>timestamp<EFBFBD>2026-01-21T02:30:45.965Z<EFBFBD>message<EFBFBD>Bloc State Change<67>category<72>DevicesCubit<69>level<65>info<66><6F>timestamp<6D>2026-01-21T02:30:47.487Z<EFBFBD>message<EFBFBD>Bloc State Change<67>category<72>DevicesCubit<69>level<65>info<66><6F>timestamp<6D>2026-01-21T02:31:06.232Z<EFBFBD>message<EFBFBD>Bloc State Change<67>category<72>RemoteControlCubit<69>level<65>info

View File

@@ -1 +0,0 @@
{"init":true,"sid":"eecc3202-8da3-4dc3-029b-163ce45aa4e5","status":"ok","errors":0,"started":"2026-01-21T02:30:45.355553Z","duration":3.9e-05,"attrs":{"release":"maibu_satabot_v2@1.0.0+1","environment":"dev"}}

View File

@@ -0,0 +1 @@
<EFBFBD><EFBFBD>timestamp<EFBFBD>2026-01-24T10:30:11.075Z<EFBFBD>message<EFBFBD>Bloc State Change<67>category<72>DevicesCubit<69>level<65>info<66><6F>timestamp<6D>2026-01-24T10:30:14.363Z<EFBFBD>message<EFBFBD>Bloc State Change<67>category<72>DevicesCubit<69>level<65>info<66><6F>timestamp<6D>2026-01-24T10:30:26.850Z<EFBFBD>message<EFBFBD>Bloc State Change<67>category<72>RemoteControlCubit<69>level<65>info

View File

@@ -1 +1 @@
勘event_idル$709a4f2d-cd3d-458c-e80f-a9cba0425332・level・fatalィplatformヲnativeァreleaseクmaibu_satabot_v2@1.0.0+1、dist。1ォenvironment」dev」sdk└nameオsentry.native.flutterァversionヲ0.12.3ィpackages曹、nameセgithub:getsentry/sentry-nativeァversionヲ0.12.3ャintegrations捉crashpad、tags€・extra€ィcontextsいos└nameァWindowsョkernel_versionッ10.0.26100.7623ァversionェ10.0.26200・build、7623・traceΚtrace_idル be1adb8b6f544f5503b93be1d08a18f4ァspan_idー239fbdcb568d4d61ォsample_randヒ?<3F>>fカFチ
Ѝevent_idÙ$203810ea-7c6b-40c7-c343-f304b1f6c205¥level¥fatal¨platform¦native§release¸maibu_satabot_v2@1.0.0+1¤dist¡1«environment£dev£sdk„¤nameµsentry.native.flutter§version¦0.12.3¨packages‘‚¤name¾github:getsentry/sentry-native§version¦0.12.3¬integrations‘¨crashpad¤tags€¥extra€¨contexts‚¢os„¤name§Windows®kernel_version¯10.0.26100.7623§versionª10.0.26200¥build¤7623¥traceƒ¨trace_idÙ 41524da056a643d33f9eae264462216b§span_id°09beaeab06c54f1b«sample_randË?Úã å¸

View File

@@ -0,0 +1 @@
{"init":true,"sid":"c6b0cdd0-39ec-4d8f-a32b-15cb2dc4252e","status":"ok","errors":0,"started":"2026-01-24T10:30:10.421356Z","duration":3.5e-05,"attrs":{"release":"maibu_satabot_v2@1.0.0+1","environment":"dev"}}

View File

@@ -0,0 +1,3 @@
{"dsn":"https://your_test_dsn@sentry.io/456"}
{"type":"session","length":220}
{"init":true,"sid":"eef89628-c1c4-408b-0ddd-05b7e99a184d","status":"crashed","errors":1,"started":"2026-01-24T09:07:31.042828Z","duration":191201.217557,"attrs":{"release":"maibu_satabot_v2@1.0.0+1","environment":"dev"}}

View File

@@ -0,0 +1 @@
<EFBFBD><EFBFBD>timestamp<EFBFBD>2026-01-24T09:07:31.634Z<EFBFBD>message<EFBFBD>Bloc State Change<67>category<72>DevicesCubit<69>level<65>info<66><6F>timestamp<6D>2026-01-24T09:07:33.913Z<EFBFBD>message<EFBFBD>Bloc State Change<67>category<72>DevicesCubit<69>level<65>info<66><6F>timestamp<6D>2026-01-24T09:07:42.606Z<EFBFBD>message<EFBFBD>Bloc State Change<67>category<72>RemoteControlCubit<69>level<65>info<66><6F>timestamp<6D>2026-01-24T09:07:52.514Z<EFBFBD>message<EFBFBD>Bloc State Change<67>category<72>DevicesCubit<69>level<65>info<66><6F>timestamp<6D>2026-01-24T09:07:54.536Z<EFBFBD>message<EFBFBD>Bloc State Change<67>category<72>DevicesCubit<69>level<65>info

View File

@@ -0,0 +1 @@
<EFBFBD><EFBFBD>event_id<EFBFBD>$717898bc-f383-4998-6fc7-a8de3b0a83d3<64>timestamp<6D>2026-01-26T14:14:12.258950Z<EFBFBD>platform<EFBFBD>native<EFBFBD>level<EFBFBD>fatal<EFBFBD>release<EFBFBD>maibu_satabot_v2@1.0.0+1<>dist<73>1<EFBFBD>environment<6E>dev<65>sdk<64><6B>name<6D>sentry.native.flutter<65>version<6F>0.12.3<EFBFBD>packages<EFBFBD><EFBFBD><EFBFBD>name<EFBFBD>github:getsentry/sentry-native<76>version<6F>0.12.3<EFBFBD>integrations<EFBFBD><EFBFBD>crashpad<EFBFBD>tags<EFBFBD><EFBFBD>extra<EFBFBD><EFBFBD>contexts<EFBFBD><EFBFBD>os<EFBFBD><EFBFBD>name<EFBFBD>Windows<EFBFBD>kernel_version<EFBFBD>10.0.26100.7623<EFBFBD>version<EFBFBD>10.0.26200<EFBFBD>build<EFBFBD>7623<EFBFBD>trace<EFBFBD><EFBFBD>trace_id<EFBFBD> fc453570d1f4499d4a1f7acbf7781a13<31>span_id<69>e6025705ee5f416d<36>sample_rand<6E>?<3F>G5\"<22>

View File

@@ -0,0 +1 @@
<EFBFBD><EFBFBD>timestamp<EFBFBD>2026-01-24T09:07:31.634Z<EFBFBD>message<EFBFBD>Bloc State Change<67>category<72>DevicesCubit<69>level<65>info<66><6F>timestamp<6D>2026-01-24T09:07:33.913Z<EFBFBD>message<EFBFBD>Bloc State Change<67>category<72>DevicesCubit<69>level<65>info<66><6F>timestamp<6D>2026-01-24T09:07:42.606Z<EFBFBD>message<EFBFBD>Bloc State Change<67>category<72>RemoteControlCubit<69>level<65>info<66><6F>timestamp<6D>2026-01-24T09:07:52.514Z<EFBFBD>message<EFBFBD>Bloc State Change<67>category<72>DevicesCubit<69>level<65>info<66><6F>timestamp<6D>2026-01-24T09:07:54.536Z<EFBFBD>message<EFBFBD>Bloc State Change<67>category<72>DevicesCubit<69>level<65>info

View File

@@ -0,0 +1 @@
<EFBFBD><EFBFBD>event_id<EFBFBD>$717898bc-f383-4998-6fc7-a8de3b0a83d3<64>timestamp<6D>2026-01-26T14:14:12.258950Z<EFBFBD>platform<EFBFBD>native<EFBFBD>level<EFBFBD>fatal<EFBFBD>release<EFBFBD>maibu_satabot_v2@1.0.0+1<>dist<73>1<EFBFBD>environment<6E>dev<65>sdk<64><6B>name<6D>sentry.native.flutter<65>version<6F>0.12.3<EFBFBD>packages<EFBFBD><EFBFBD><EFBFBD>name<EFBFBD>github:getsentry/sentry-native<76>version<6F>0.12.3<EFBFBD>integrations<EFBFBD><EFBFBD>crashpad<EFBFBD>tags<EFBFBD><EFBFBD>extra<EFBFBD><EFBFBD>contexts<EFBFBD><EFBFBD>os<EFBFBD><EFBFBD>name<EFBFBD>Windows<EFBFBD>kernel_version<EFBFBD>10.0.26100.7623<EFBFBD>version<EFBFBD>10.0.26200<EFBFBD>build<EFBFBD>7623<EFBFBD>trace<EFBFBD><EFBFBD>trace_id<EFBFBD> fc453570d1f4499d4a1f7acbf7781a13<31>span_id<69>e6025705ee5f416d<36>sample_rand<6E>?<3F>G5\"<22>

View File

@@ -1 +1 @@
2026-01-21T02:19:04.070843Z
2026-01-26T14:14:12.259855Z

Binary file not shown.

Binary file not shown.

View File

@@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<application

View File

@@ -1,16 +0,0 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../domain/entities/user_entity.dart';
import 'app_user_state.dart';
class AppUserCubit extends Cubit<AppUserState> {
AppUserCubit() : super(const AppUserState());
void setAuth(UserEntity user) {
emit(state.copyWith(user));
}
void clearAuth() {
emit(const AppUserState());
}
}

View File

@@ -1,20 +0,0 @@
import 'package:equatable/equatable.dart';
import '../domain/entities/user_entity.dart';
class AppUserState extends Equatable {
final UserEntity? user; // 直接存实体,包含 userId, username,token 等所有信息
const AppUserState({this.user});
// 辅助属性:判断是否登录
bool get isLoggedIn => user != null;
AppUserState copyWith(UserEntity? user) {
return AppUserState(user: user ?? this.user);
}
// 必须重写 props,UI 才会只在数据真正变化时刷新
@override
List<Object?> get props => [user];
}

View File

@@ -1,4 +1,5 @@
class TCPConsts {
static const String TCP_IP = "1.95.137.212";
// static const String TCP_IP = "1.95.137.212";
static const String TCP_IP = "192.210.172.250";
static const int TCP_PORT = 9001;
}

View File

@@ -1,8 +1,9 @@
import '../../features/devices/domain/entities/device_entity.dart';
abstract class BaseModel {
BaseModel.fromJson(Map<String, dynamic> json);
abstract class BaseModel<E> {
/// 将 Model 转换为对应的 Domain Entity
E toEntity();
/// 将 Model 转换为 JSON(用于持久化或日志)
Map<String, dynamic> toJson();
toEntity();
BaseModel.fromEntity(DeviceEntity entity);
}

View File

@@ -1,8 +1,10 @@
import 'package:dio/dio.dart';
import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
import 'package:maibu_satabot_v2/core/consts/tcp_consts.dart';
import 'package:maibu_satabot_v2/core/infrastructure/logging/sentry_logger_impl.dart';
import 'package:maibu_satabot_v2/core/logging/i_logger_service.dart';
import 'package:maibu_satabot_v2/core/network/tcp/tcp_connection.dart';
import 'package:maibu_satabot_v2/features/devices/data/datasources/impl/device_http_datasource_impl.dart';
import 'package:maibu_satabot_v2/features/devices/domain/repositories/device_repository.dart';
import 'package:maibu_satabot_v2/features/devices/domain/usecases/get_user_device_usecase.dart';
@@ -12,7 +14,7 @@ import 'package:maibu_satabot_v2/features/remote_control/domain/repositories/rem
import 'package:maibu_satabot_v2/features/remote_control/domain/usecase/diff_steer_usecase.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../features/auth/data/datasources/auth_http_datasource.dart';
import '../../features/auth/data/datasources/i_auth_http_datasource.dart';
import '../../features/auth/data/datasources/impl/auth_http_datasource_impl.dart';
import '../../features/auth/data/repositories/auth_repository_impl.dart';
import '../../features/auth/domain/repositories/auth_repository.dart';
@@ -20,16 +22,15 @@ import '../../features/auth/domain/usecases/login_usecase.dart';
import '../../features/auth/presentation/bloc/auth_cubit.dart';
import '../../features/auth/presentation/bloc/login_bloc.dart';
import '../../features/auth/presentation/bloc/login_cubit.dart';
import '../../features/devices/data/datasources/device_http_datasource.dart';
import '../../features/connectivity/presentation/bloc/connectivity_cubit.dart';
import '../../features/devices/data/datasources/i_device_http_datasource.dart';
import '../../features/devices/data/repositories/device_repository_impl.dart';
import '../../features/remote_control/presentation/bloc/remote_control_cubit.dart';
import '../app/app_user_cubit.dart';
import '../network/dio_client.dart';
import '../network/net_message_dispatcher.dart';
import '../network/tcp/tcp_client.dart';
import '../../features/auth/presentation/bloc/app_user_cubit.dart';
import '../network/http/dio_client.dart';
import '../router/app_router.dart';
import '../storage/impl/user_storage_impl.dart';
import '../storage/user_storage.dart';
import '../../features/auth/data/datasources/impl/auth_local_datasource_impl.dart';
import '../../features/auth/data/datasources/i_auth_local_datasource.dart';
final sl = GetIt.instance;
@@ -40,10 +41,10 @@ Future<void> init() async {
sl.registerLazySingleton<Dio>(() => DioClient.create());
/// 1.1.2 TcpClient:TCP客户端
sl.registerLazySingleton(() => TcpClient());
sl.registerLazySingleton(() => TcpConnection(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT));
/// 1.1.3 NetMessageDispatcher:消息调度器,并将 TcpClient 注入给它
sl.registerLazySingleton(() => NetMessageDispatcher(sl()));
sl.registerLazySingleton(() => ConnectivityCubit());
/// 1.2 --- 本地存储 (LocalStorage) ---
/// 1.2.1 SharedPreferences:本地存储库
@@ -51,19 +52,19 @@ Future<void> init() async {
sl.registerLazySingleton<SharedPreferences>(() => sharedPrefs);
/// 1.2.2 UserStorage:用户存储库
sl.registerLazySingleton<UserStorage>(
sl.registerLazySingleton<IAuthLocalDataSource>(
// 必须先注册上面本地存储库的实例
() => UserStorageImpl(sl<SharedPreferences>()),
() => AuthLocalDatasourceImpl(sl<SharedPreferences>()),
);
/// 1.3 Log工具Sentry
sl.registerLazySingleton<ILoggerService>(() => SentryLoggerImpl());
/// 2. 数据源 (DataSource)
sl.registerLazySingleton<AuthHttpDataSource>(
sl.registerLazySingleton<IAuthHttpDataSource>(
() => AuthHttpDataSourceImpl(sl()),
);
sl.registerLazySingleton<DeviceHttpDatasource>(
sl.registerLazySingleton<IDeviceHttpDatasource>(
() => DeviceHttpDatasourceImpl(sl()),
);
@@ -76,7 +77,10 @@ Future<void> init() async {
);
sl.registerLazySingleton<DeviceRepository>(() => DeviceRepositoryImpl(sl()));
sl.registerLazySingleton<RemoteControlRepository>(
() => RemoteControlRepositoryImpl(sl(), sl()),
() => RemoteControlRepositoryImpl(
sl<ConnectivityCubit>(),
sl<DiffSteerUseCase>(),
),
);
/// 4. 用例 (UseCase)
@@ -85,18 +89,17 @@ Future<void> init() async {
sl.registerLazySingleton(() => DiffSteerUseCase());
/// 5. 状态管理 (Cubit/Bloc)
sl.registerLazySingleton(() => AppUserCubit()); // AuthCubit 依赖它,必须先注册
sl.registerLazySingleton(() => AppUserCubit(sl())); // AuthCubit 依赖它,必须先注册
sl.registerLazySingleton(() => DevicesCubit(sl(), sl()));
sl.registerFactory(() => RemoteControlCubit(sl()));
/// 6. 认证 (Auth)
// --- 关键修改点 1: AuthCubit 必须在 GoRouter 之前注册,并传入参数 ---
sl.registerLazySingleton(
() => AuthCubit(
sl<UserStorage>(),
sl<TcpClient>(),
sl<AppUserCubit>(),
sl<NetMessageDispatcher>(),
() => AuthCubit(
storage: sl<IAuthLocalDataSource>(), // 对应构造函数第一个参数
appCubit: sl<AppUserCubit>(), // 对应第二个参数
connectivityCubit: sl<ConnectivityCubit>(), // 对应第三个参数
),
);

View File

@@ -2,3 +2,7 @@ class Failure {
final String message;
Failure(this.message);
}
class NetworkFailure extends Failure {
NetworkFailure(super.message);
}

View File

@@ -11,26 +11,31 @@ import 'package:intl/intl.dart';
class SentryLoggerImpl implements ILoggerService {
File? _localLogFile;
/// --- 临时控制开关 ---
/// true: 上传到 Sentry + 本地存储
/// false: 只进行本地存储 + 控制台打印
bool get _isRemoteEnabled => false;
@override
Future<void> init() async {
await SentryFlutter.init((options) {
options.dsn = EnvConfig.sentryDsn;
options.environment = EnvConfig.environment;
// 生产环境只采样 20% 的性能数据,测试环境全开
options.tracesSampleRate = EnvConfig.isProduction ? 0.2 : 1.0;
});
await _collectDeviceInfo();
// 只有在开启远程上传时才初始化 Sentry SDK
if (_isRemoteEnabled) {
await SentryFlutter.init((options) {
options.dsn = EnvConfig.sentryDsn;
options.environment = EnvConfig.environment;
options.tracesSampleRate = EnvConfig.isProduction ? 0.2 : 1.0;
});
await _collectDeviceInfo();
}
// 无论是否上传服务器,始终准备本地日志文件
await _prepareLocalFile();
}
// 准备本地文件路径
Future<void> _prepareLocalFile() async {
try {
final directory = await getApplicationDocumentsDirectory();
// 按天生成文件名,例如: 2024-05-20_logs.txt
final fileName =
"${DateFormat('yyyy-MM-dd').format(DateTime.now())}_logs.txt";
final fileName = "${DateFormat('yyyy-MM-dd').format(DateTime.now())}_logs.txt";
_localLogFile = File('${directory.path}/$fileName');
if (!await _localLogFile!.exists()) {
@@ -42,6 +47,8 @@ class SentryLoggerImpl implements ILoggerService {
}
Future<void> _collectDeviceInfo() async {
if (!_isRemoteEnabled) return;
final deviceInfo = DeviceInfoPlugin();
Sentry.configureScope((scope) async {
if (Platform.isAndroid) {
@@ -58,28 +65,45 @@ class SentryLoggerImpl implements ILoggerService {
@override
void captureException(dynamic exception, {dynamic stackTrace}) {
Sentry.captureException(exception, stackTrace: stackTrace);
final timestamp = DateTime.now().toIso8601String();
final logEntry = "[$timestamp] [ERROR] $exception\n$stackTrace\n";
// 1. 只有开启时上传
if (_isRemoteEnabled) {
Sentry.captureException(exception, stackTrace: stackTrace);
}
// 2. 始终写入本地
_writeToLocalFile(logEntry);
// 3. 始终控制台打印
if (kDebugMode) {
debugPrint(logEntry);
}
}
@override
void log(
String message, {
bool isWarning = false,
Map<String, dynamic>? data,
}) {
String message, {
bool isWarning = false,
Map<String, dynamic>? data,
}) {
final timestamp = DateTime.now().toIso8601String();
final level = isWarning ? 'WARNING' : 'INFO';
final logEntry = "[$timestamp] [$level] $message ${data ?? ''}\n";
// 1.发送日志到 Sentry
Sentry.captureMessage(
message,
level: isWarning ? SentryLevel.warning : SentryLevel.info,
);
// 1. 只有开启时上传到 Sentry
if (_isRemoteEnabled) {
Sentry.captureMessage(
message,
level: isWarning ? SentryLevel.warning : SentryLevel.info,
);
}
// 2.写入本地文件
// 2. 始终写入本地文件
_writeToLocalFile(logEntry);
// 3. 始终打印到控制台
if (kDebugMode) {
debugPrint(logEntry);
}
@@ -87,25 +111,29 @@ class SentryLoggerImpl implements ILoggerService {
Future<void> _writeToLocalFile(String entry) async {
try {
// 使用追加模式写入
await _localLogFile?.writeAsString(entry, mode: FileMode.append);
} catch (e) {
// 本地写入失败不应崩溃
// 这里的错误不再抛出,避免日志系统本身崩溃导致主业务中断
}
}
// 增加一个获取本地日志文件路径的方法,方便你以后做“导出日志”功能
Future<String?> getLocalLogPath() async {
return _localLogFile?.path;
}
@override
void addBreadcrumb(String message, {String? category}) {
Sentry.addBreadcrumb(Breadcrumb(message: message, category: category));
// 面包屑主要用于 Sentry 报错时的上下文,如果关闭了远程,记录本地即可(可选)
if (_isRemoteEnabled) {
Sentry.addBreadcrumb(Breadcrumb(message: message, category: category));
}
_writeToLocalFile("[BREADCRUMB] [$category] $message\n");
}
@override
void setUser(String userId) {
Sentry.configureScope((scope) => scope.setUser(SentryUser(id: userId)));
if (_isRemoteEnabled) {
Sentry.configureScope((scope) => scope.setUser(SentryUser(id: userId)));
}
}
}
}

View File

@@ -0,0 +1,14 @@
import 'dart:async';
import 'package:fpdart/fpdart.dart';
import '../../error/failure.dart';
enum ConnectionStatus { connected, disconnected, connecting }
abstract class IConnection {
Stream<List<int>> get rawStream;
Stream<ConnectionStatus> get statusStream;
Future<Either<Failure, Unit>> connect();
Future<void> disconnect();
Future<Either<Failure, Unit>> send(List<int> data);
}

View File

@@ -0,0 +1,3 @@
class BleClient{
}

View File

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

View File

@@ -1,8 +1,8 @@
import 'package:dio/dio.dart';
import 'package:maibu_satabot_v2/core/consts/http_api_consts.dart';
import '../app/app_user_cubit.dart';
import '../di/injection.dart';
import '../../../features/auth/presentation/bloc/app_user_cubit.dart';
import '../../di/injection.dart';
class DioClient {
static Dio create() {

View File

@@ -0,0 +1,17 @@
class ConnectionConfig {
final String address; // IP 地址或蓝牙 MAC/UUID
final int? port; // TCP 专用端口
ConnectionConfig({
required this.address,
this.port,
});
// 方便快捷创建 TCP 配置
factory ConnectionConfig.tcp(String ip, int port) =>
ConnectionConfig(address: ip, port: port);
// 方便快捷创建 BLE 配置
factory ConnectionConfig.ble(String mac) =>
ConnectionConfig(address: mac);
}

View File

@@ -1,26 +0,0 @@
import 'dart:convert';
import 'protocol_decoder.dart';
import 'tcp/tcp_client.dart';
// 注意:这里需要导入你具体的 UserModel
// import '../../features/auth/data/models/user_model.dart';
class NetMessageDispatcher {
final TcpClient tcpClient;
NetMessageDispatcher(this.tcpClient);
/// 过滤特定指令的流
Stream<RawPacket> onCommand(int commandCode) {
return tcpClient.packetStream.where((p) => p.command == commandCode);
}
/// 示例解析方法:将 0x02 指令解析为 String
Stream<String> onStringMessage() {
return onCommand(0x02).map((p) => utf8.decode(p.payload));
}
/// 示例解析方法:将 0x12 指令解析为 JSON 并转为模型
Stream<Map<String, dynamic>> onJsonMessage(int cmd) {
return onCommand(cmd).map((p) => jsonDecode(utf8.decode(p.payload)));
}
}

View File

@@ -1,59 +0,0 @@
import 'dart:typed_data';
class RawPacket {
final int command; // 第三位指令位
final Uint8List payload; // 数据位
RawPacket({required this.command, required this.payload});
}
class ProtocolDecoder {
static const head = [0xAB, 0xAA];
static const tail = [0xAA, 0xAB];
final List<int> _buffer = [];
List<RawPacket> decode(List<int> newData) {
_buffer.addAll(newData);
List<RawPacket> packets = [];
while (_buffer.length >= 5) {
// 最小包长度:头2+指令1+尾2 = 5,加1位数据=6,但是心跳包是AB AA FF AA AB
// 1. 找头
int headIdx = -1;
for (int i = 0; i < _buffer.length - 1; i++) {
if (_buffer[i] == head[0] && _buffer[i + 1] == head[1]) {
headIdx = i;
break;
}
}
if (headIdx == -1) {
_buffer.clear(); // 没头,全是垃圾数据
break;
}
// 2. 找尾
int tailIdx = -1;
for (int i = headIdx + 2; i < _buffer.length - 1; i++) {
if (_buffer[i] == tail[0] && _buffer[i + 1] == tail[1]) {
tailIdx = i;
break;
}
}
if (tailIdx != -1) {
// 3. 提取
int cmd = _buffer[headIdx + 2];
Uint8List payload = Uint8List.fromList(
_buffer.sublist(headIdx + 3, tailIdx),
);
packets.add(RawPacket(command: cmd, payload: payload));
// 4. 移除已处理部分
_buffer.removeRange(0, tailIdx + 2);
} else {
break; // 还没传完
}
}
return packets;
}
}

View File

@@ -1,66 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/cupertino.dart';
import 'package:maibu_satabot_v2/core/network/protocol_decoder.dart';
class TcpClient {
Socket? _socket;
final _decoder = ProtocolDecoder();
// 使用 StreamController 将原始数据流暴露给外部,以便多个 DataSource 监听
final _controller = StreamController<RawPacket>.broadcast();
Stream<RawPacket> get packetStream => _controller.stream;
// 通过参数配置,不硬编码
Future<void> connect({required String host, required int port}) async {
try {
_socket = await Socket.connect(
host,
port,
timeout: const Duration(seconds: 5),
);
_socket!.listen(
(data) {
var packets = _decoder.decode(data);
for (var packet in packets) {
_controller.add(packet);
}
},
onDone: disconnect,
onError: (e) => disconnect(),
);
} catch (e) {
rethrow; // 向上抛出连接异常
}
}
void send(Map<String, dynamic> data) {
if (_socket == null) throw Exception("Socket not connected");
_socket!.write(jsonEncode(data));
}
void sendRaw(int cmd, List<int> payload) {
if (_socket == null) return;
final builder = BytesBuilder()
..addByte(0xAB)
..addByte(0xAA)
..addByte(cmd)
..add(payload)
..add([0x00, 0x00]) // CRC
..addByte(0xAA)
..addByte(0xAB);
_socket!.add(builder.takeBytes());
}
void disconnect() {
_socket?.destroy();
_socket = null;
_controller.close();
debugPrint("TCP Disconnected");
}
}

View File

@@ -0,0 +1,53 @@
import 'dart:io';
import 'dart:async';
import 'package:fpdart/fpdart.dart';
import '../../error/failure.dart';
import '../base/i_connection.dart';
class TcpConnection implements IConnection {
final String host;
final int port;
Socket? _socket;
final _statusController = StreamController<ConnectionStatus>.broadcast();
TcpConnection({required this.host, required this.port});
@override
Stream<List<int>> get rawStream => _socket ?? const Stream.empty();
@override
Stream<ConnectionStatus> get statusStream => _statusController.stream;
@override
Future<Either<Failure, Unit>> connect() async {
try {
_statusController.add(ConnectionStatus.connecting);
_socket = await Socket.connect(host, port, timeout: const Duration(seconds: 5));
_statusController.add(ConnectionStatus.connected);
_socket!.done.then((_) => _statusController.add(ConnectionStatus.disconnected));
return right(unit);
} catch (e) {
_statusController.add(ConnectionStatus.disconnected);
return left(NetworkFailure(e.toString()));
}
}
@override
Future<Either<Failure, Unit>> send(List<int> data) async {
if (_socket == null) return left(NetworkFailure("Socket未连接"));
try {
_socket!.add(data);
await _socket!.flush(); // 立即刷出
return right(unit);
} catch (e) {
return left(NetworkFailure(e.toString()));
}
}
@override
Future<void> disconnect() async {
await _socket?.close();
_socket = null;
_statusController.add(ConnectionStatus.disconnected);
}
}

View File

@@ -1,28 +0,0 @@
import 'dart:typed_data';
/// 极简化的 Codec:只负责中间那段变化的“负载数据”
class MachineProtocolCodec {
/// 仅封装 0x00 指令的数据部分 (Byte 3 到 Byte 10,共 8 字节)
static 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

@@ -0,0 +1,12 @@
import 'package:equatable/equatable.dart';
class PacketEntity extends Equatable{
final int cmdType;
final List<int> payload;
final List<int>? crc; // 心跳包可能为 null
const PacketEntity({required this.cmdType, required this.payload, this.crc});
@override
List<Object?> get props => [cmdType, payload, crc];
}

View File

@@ -0,0 +1,83 @@
import 'dart:async';
import 'dart:typed_data'; // 引入 Uint8List
import 'protocol_constants.dart';
import 'packet_entity.dart';
class PacketParserTransformer extends StreamTransformerBase<Uint8List, PacketEntity> {
@override
Stream<PacketEntity> bind(Stream<Uint8List> stream) {
final List<int> buffer = [];
final StreamController<PacketEntity> controller = StreamController<PacketEntity>();
stream.listen(
(data) {
// 将接收到的数据追加到 buffer
buffer.addAll(data);
while (buffer.length >= 5) { // 最小包长(心跳) AB AA FF AA AB
// 找 Header
int headIdx = -1;
for (int i = 0; i < buffer.length - 1; i++) {
if (buffer[i] == ProtocolConstants.header1 &&
buffer[i + 1] == ProtocolConstants.header2) {
headIdx = i;
break;
}
}
if (headIdx == -1) {
// 如果没有找到包头,丢弃已接收的数据,继续等待数据
buffer.clear(); // 直接清空缓冲区,可以避免部分无效数据的积累
break;
}
if (headIdx > 0) {
// 如果包头不是从缓冲区的开头开始,移除头部无效数据
buffer.removeRange(0, headIdx);
}
// 找 Footer
int footIdx = -1;
for (int i = 2; i < buffer.length - 1; i++) {
if (buffer[i] == ProtocolConstants.endFlag1 &&
buffer[i + 1] == ProtocolConstants.endFlag2) {
footIdx = i;
break;
}
}
if (footIdx != -1) {
final packetBytes = buffer.sublist(0, footIdx + 2); // 处理数据包
final cmd = packetBytes[2]; // 获取命令字节
if (cmd == ProtocolConstants.cmdHeartbeat) {
controller.add(PacketEntity(cmdType: cmd, payload: [])); // 心跳包没有负载
} else if (packetBytes.length >= 7) {
// 带 CRC 的包
final payload = packetBytes.sublist(3, packetBytes.length - 4);
final crc = packetBytes.sublist(packetBytes.length - 4, packetBytes.length - 2);
controller.add(PacketEntity(cmdType: cmd, payload: payload, crc: crc)); // 解析命令和负载
}
// 完成当前数据包的解析,移除已处理的包
buffer.removeRange(0, footIdx + 2);
} else {
break; // 数据不完整,等待更多数据
}
}
},
onError: (error) {
print("Stream error: $error");
controller.addError(error); // 如果发生错误,报告错误
},
onDone: () {
print("Stream closed.");
controller.close(); // 流关闭时,关闭控制器
},
);
return controller.stream;
}
}

View File

@@ -1,4 +1,4 @@
class MachineProtocolConstants {
class ProtocolConstants {
// 固定字节
static const int header1 = 0xAB;
static const int header2 = 0xAA;

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

@@ -1,6 +1,6 @@
import 'package:maibu_satabot_v2/core/domain/entities/user_entity.dart';
import '../../domain/entities/user_entity.dart';
abstract class UserStorage {
abstract class IAuthLocalDataSource {
Future<void> saveUser(UserEntity user);
Future<UserEntity?> getUser();
Future<void> deleteUser();

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

@@ -1,16 +1,16 @@
import 'dart:convert';
import 'package:maibu_satabot_v2/core/domain/entities/user_entity.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../features/auth/data/models/user_model.dart';
import '../user_storage.dart';
import '../../models/user_model.dart';
import '../../../domain/entities/user_entity.dart';
import '../i_auth_local_datasource.dart';
class UserStorageImpl implements UserStorage {
class AuthLocalDatasourceImpl implements IAuthLocalDataSource {
final SharedPreferences _prefs;
static const _userKey = 'auth_user';
UserStorageImpl(this._prefs);
AuthLocalDatasourceImpl(this._prefs);
@override
Future<void> saveUser(UserEntity user) async {

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

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

View File

@@ -4,7 +4,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.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/core/infrastructure/logging/app_bloc_observer.dart';
import 'package:maibu_satabot_v2/core/logging/i_logger_service.dart';
import 'package:maibu_satabot_v2/core/theme/AppTheme.dart';

View File

@@ -751,6 +751,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.5.0"
rxdart:
dependency: "direct main"
description:
name: rxdart
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.28.0"
sentry:
dependency: transitive
description:

View File

@@ -51,6 +51,9 @@ dependencies:
flutter_bloc: ^9.1.1
bloc: ^9.2.0
# ===== 状态管理(RxDart)=====
rxdart: ^0.28.0
# ===== 依赖注入 =====
get_it: ^9.2.0
@@ -98,6 +101,8 @@ dependencies:
# ===== 文件存储 =====
path_provider: ^2.1.5
intl: ^0.20.2
dev_dependencies: