#连接库初步完成
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>(), // 对应第三个参数
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
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,
|
||||
];
|
||||
}
|
||||
@@ -2,3 +2,7 @@ class Failure {
|
||||
final String message;
|
||||
Failure(this.message);
|
||||
}
|
||||
|
||||
class NetworkFailure extends Failure {
|
||||
NetworkFailure(super.message);
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
lib/core/network/base/i_connection.dart
Normal file
14
lib/core/network/base/i_connection.dart
Normal 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);
|
||||
}
|
||||
3
lib/core/network/ble/ble_client.dart
Normal file
3
lib/core/network/ble/ble_client.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
class BleClient{
|
||||
|
||||
}
|
||||
1
lib/core/network/enums/control_mode.dart
Normal file
1
lib/core/network/enums/control_mode.dart
Normal file
@@ -0,0 +1 @@
|
||||
enum ControlMode { TCP, BLE, LOCAL, NONE, OTHER }
|
||||
@@ -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() {
|
||||
17
lib/core/network/models/connection_config.dart
Normal file
17
lib/core/network/models/connection_config.dart
Normal 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);
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
53
lib/core/network/tcp/tcp_connection.dart
Normal file
53
lib/core/network/tcp/tcp_connection.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
12
lib/core/protocol/packet_entity.dart
Normal file
12
lib/core/protocol/packet_entity.dart
Normal 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];
|
||||
}
|
||||
83
lib/core/protocol/packet_parser_transformer.dart
Normal file
83
lib/core/protocol/packet_parser_transformer.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
class MachineProtocolConstants {
|
||||
class ProtocolConstants {
|
||||
// 固定字节
|
||||
static const int header1 = 0xAB;
|
||||
static const int header2 = 0xAA;
|
||||
@@ -1,41 +0,0 @@
|
||||
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';
|
||||
|
||||
class UserStorageImpl implements UserStorage {
|
||||
final SharedPreferences _prefs;
|
||||
static const _userKey = 'auth_user';
|
||||
|
||||
UserStorageImpl(this._prefs);
|
||||
|
||||
@override
|
||||
Future<void> saveUser(UserEntity user) async {
|
||||
// 1. 将对象转为 Map -> 2. 将 Map 转为 JSON 字符串
|
||||
final model = UserModel.fromEntity(user);
|
||||
await _prefs.setString(_userKey, json.encode(model.toJson()));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<UserEntity?> getUser() async {
|
||||
final jsonString = _prefs.getString(_userKey);
|
||||
if (jsonString == null) return null;
|
||||
|
||||
// 将 JSON 转回 Model,再转成 Entity 返回给业务层
|
||||
final model = UserModel.fromJson(json.decode(jsonString));
|
||||
return model.toEntity();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteUser() async {
|
||||
await _prefs.remove(_userKey);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> hasUser() async {
|
||||
return _prefs.containsKey(_userKey);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import 'package:maibu_satabot_v2/core/domain/entities/user_entity.dart';
|
||||
|
||||
abstract class UserStorage {
|
||||
Future<void> saveUser(UserEntity user);
|
||||
Future<UserEntity?> getUser();
|
||||
Future<void> deleteUser();
|
||||
Future<bool> hasUser();
|
||||
}
|
||||
Reference in New Issue
Block a user