fix: 增加获取设备公钥、私钥接口
This commit is contained in:
parent
799479447c
commit
a3ad89d0d4
@ -13,24 +13,23 @@ class ApiResponse<T> {
|
||||
|
||||
// ✅ 新增:从 JSON 创建 ApiResponse
|
||||
factory ApiResponse.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
T Function(dynamic)? dataFromJson, // 可为空,有些接口 data 是 null
|
||||
) {
|
||||
Map<String, dynamic> json,
|
||||
T Function(dynamic)? dataFromJson, // 可为空,有些接口 data 是 null
|
||||
) {
|
||||
final dataJson = json['data'];
|
||||
final T? parsedData = dataJson != null && dataFromJson != null
|
||||
? dataFromJson(dataJson)
|
||||
: null;
|
||||
final T? parsedData = dataJson != null && dataFromJson != null ? dataFromJson(dataJson) : null;
|
||||
|
||||
return ApiResponse<T>(
|
||||
errorCode: json['errorCode'],
|
||||
errorMsg: json['errorMsg'],
|
||||
errorCode: json['errorCode'] == null ? json['errcode'] : json['errorCode'],
|
||||
errorMsg: json['errorMsg'] == null ? json['errmsg'] : json['errorMsg'],
|
||||
description: json['description'],
|
||||
data: parsedData,
|
||||
);
|
||||
}
|
||||
|
||||
// 成功工厂构造
|
||||
factory ApiResponse.success(T data, {
|
||||
factory ApiResponse.success(
|
||||
T data, {
|
||||
String? errorMsg,
|
||||
int? errorCode,
|
||||
String? description,
|
||||
@ -45,11 +44,11 @@ class ApiResponse<T> {
|
||||
|
||||
// 失败工厂构造
|
||||
factory ApiResponse.error(
|
||||
String errorMsg, {
|
||||
int? errorCode,
|
||||
T? data,
|
||||
String? description,
|
||||
}) {
|
||||
String errorMsg, {
|
||||
int? errorCode,
|
||||
T? data,
|
||||
String? description,
|
||||
}) {
|
||||
return ApiResponse<T>(
|
||||
description: description,
|
||||
errorMsg: errorMsg,
|
||||
@ -62,9 +61,9 @@ class ApiResponse<T> {
|
||||
return errorCode == 0;
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ApiResponse(description: $description, errorMsg: $errorMsg, data: $data, errorCode: $errorCode)';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -12,8 +12,6 @@ class BaseApiService {
|
||||
dio.options.baseUrl = F.apiHost;
|
||||
dio.options.connectTimeout = const Duration(seconds: 30);
|
||||
dio.options.receiveTimeout = const Duration(seconds: 30);
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// 统一请求方法
|
||||
@ -28,8 +26,7 @@ class BaseApiService {
|
||||
// 🔍 打印请求信息(更详细控制)
|
||||
if (kDebugMode) {
|
||||
final uri = Uri.parse('${dio.options.baseUrl}$path');
|
||||
final queryString =
|
||||
queryParameters != null ? '?${uri.queryParameters.toString()}' : '';
|
||||
final queryString = queryParameters != null ? '?${uri.queryParameters.toString()}' : '';
|
||||
debugPrint('🟦 API Request: $method ${uri.toString()}$queryString');
|
||||
if (data != null) {
|
||||
debugPrint('🟦 Request Body: $data');
|
||||
@ -43,20 +40,16 @@ class BaseApiService {
|
||||
response = await dio.get(path, queryParameters: queryParameters);
|
||||
break;
|
||||
case HttpConstant.post:
|
||||
response = await dio.post(path,
|
||||
data: data, queryParameters: queryParameters);
|
||||
response = await dio.post(path, data: data, queryParameters: queryParameters);
|
||||
break;
|
||||
case HttpConstant.put:
|
||||
response =
|
||||
await dio.put(path, data: data, queryParameters: queryParameters);
|
||||
response = await dio.put(path, data: data, queryParameters: queryParameters);
|
||||
break;
|
||||
case HttpConstant.delete:
|
||||
response = await dio.delete(path,
|
||||
data: data, queryParameters: queryParameters);
|
||||
response = await dio.delete(path, data: data, queryParameters: queryParameters);
|
||||
break;
|
||||
case HttpConstant.patch:
|
||||
response = await dio.patch(path,
|
||||
data: data, queryParameters: queryParameters);
|
||||
response = await dio.patch(path, data: data, queryParameters: queryParameters);
|
||||
break;
|
||||
default:
|
||||
return ApiResponse.error('Unsupported method: $method');
|
||||
@ -64,8 +57,7 @@ class BaseApiService {
|
||||
|
||||
// ✅ 打印响应
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'🟩 API Response [${response.statusCode}] ${response.requestOptions.path}');
|
||||
debugPrint('🟩 API Response [${response.statusCode}] ${response.requestOptions.path}');
|
||||
debugPrint('🟩 Response Data: ${response.data}');
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
class StarCloudCreateUserRequest {
|
||||
final String clientId;
|
||||
final String clientSecret;
|
||||
|
||||
StarCloudCreateUserRequest({
|
||||
required this.clientId,
|
||||
required this.clientSecret,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'clientId': clientId,
|
||||
'clientSecret': clientSecret,
|
||||
};
|
||||
}
|
||||
}
|
||||
21
lib/api/model/starcloud/request/starcloud_login_request.dart
Normal file
21
lib/api/model/starcloud/request/starcloud_login_request.dart
Normal file
@ -0,0 +1,21 @@
|
||||
class StarCloudLoginRequest {
|
||||
final String clientId;
|
||||
final String clientSecret;
|
||||
final String username;
|
||||
final String password;
|
||||
StarCloudLoginRequest({
|
||||
required this.clientId,
|
||||
required this.clientSecret,
|
||||
required this.username,
|
||||
required this.password,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'clientId': clientId,
|
||||
'clientSecret': clientSecret,
|
||||
'username': username,
|
||||
'password': password,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
class StarCloudCreateUserResponse {
|
||||
final String username;
|
||||
final String password;
|
||||
final int uid;
|
||||
|
||||
StarCloudCreateUserResponse({
|
||||
required this.username,
|
||||
required this.password,
|
||||
required this.uid,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'username': username,
|
||||
'password': password,
|
||||
'uid': uid,
|
||||
};
|
||||
}
|
||||
|
||||
factory StarCloudCreateUserResponse.fromJson(Map<String, dynamic> json) {
|
||||
return StarCloudCreateUserResponse(
|
||||
username: json['username'] ?? '',
|
||||
password: json['password'] ?? '',
|
||||
uid: json['uid'] ?? -1,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'StarCloudCreateUserResponse{username: $username, password: $password, uid: $uid}';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
class StarCloudLoginResponse {
|
||||
final String accessToken;
|
||||
final String uid;
|
||||
final String refreshToken;
|
||||
final String expiresIn;
|
||||
|
||||
StarCloudLoginResponse({
|
||||
required this.accessToken,
|
||||
required this.uid,
|
||||
required this.refreshToken,
|
||||
required this.expiresIn,
|
||||
});
|
||||
|
||||
factory StarCloudLoginResponse.fromJson(Map<String, dynamic> json) {
|
||||
return StarCloudLoginResponse(
|
||||
accessToken: json['access_token'] ?? '',
|
||||
uid: json['uid'] ?? '',
|
||||
refreshToken: json['refresh_token'] ?? '',
|
||||
expiresIn: json['expires_in'] ?? '',
|
||||
);
|
||||
}
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'accessToken': accessToken,
|
||||
'uid': uid,
|
||||
'refreshToken': refreshToken,
|
||||
'expiresIn': expiresIn,
|
||||
};
|
||||
}
|
||||
}
|
||||
107
lib/api/starcloud/starcloud_api_path.dart
Normal file
107
lib/api/starcloud/starcloud_api_path.dart
Normal file
@ -0,0 +1,107 @@
|
||||
class StarCloudApiPath {
|
||||
static const String createUser = '/createUser'; // 创建账号
|
||||
static const String getOauthToken = '/oauth2/token'; // 获取访问令牌
|
||||
static const String refreshOauthToken = '/oauth2/token'; // 刷新访问令牌
|
||||
static const String lockList = '/v1/lock/list'; // 获取名下锁列表
|
||||
static const String lockDetail = '/v1/lock/detail'; // 获取锁详情
|
||||
static const String lockInitialize = '/v1/lock/initialize';
|
||||
static const String getLockNetToken = '/v1/lock/getLockNetToken'; // 获取联网token
|
||||
static const String lockReset = '/v1/lock/delete'; // 重置锁
|
||||
static const String getLockSettingData =
|
||||
'/v1/lock/getLockSettingData'; // 获取锁应用设置
|
||||
static const String changeAdminKeyboardPwdURL =
|
||||
'/v1/lock/changeAdminKeyboardPwd'; // 更新管理员密码
|
||||
static const String checkKeyboardpwdName =
|
||||
'/v1/keyboardPwd/checkKeyboardpwdName'; // 检测密码是否重复
|
||||
|
||||
// 卡
|
||||
static const String cardList = '/v1/identityCard/list';
|
||||
static const String addCard = '/v1/identityCard/add';
|
||||
static const String editCard = '/v1/identityCard/update';
|
||||
static const String resetCard = '/v1/identityCard/clear';
|
||||
static const String deleteCard = '/v1/identityCard/delete';
|
||||
|
||||
// 密码
|
||||
static const String passwordList = '/v1/keyboardPwd/listKeyboardPwd';
|
||||
static const String addCustoomPassword = '/v1/keyboardPwd/add';
|
||||
static const String addOffLinePassword = '/v1/keyboardPwd/get';
|
||||
static const String updatePassword = '/v1/keyboardPwd/update';
|
||||
static const String deletePassword = '/v1/keyboardPwd/delete';
|
||||
static const String resetPassword = '/v1/keyboardPwd/clear';
|
||||
|
||||
static const String fingerprintList = '/v1/fingerprint/list';
|
||||
static const String addFingerprint = '/v1/fingerprint/add';
|
||||
static const String editFingerprint = '/v1/fingerprint/update';
|
||||
static const String deleteFingerprint = '/v1/fingerprint/delete';
|
||||
static const String resetFingerprint = '/v1/fingerprint/clear';
|
||||
|
||||
static const String faceList = '/v1/face/list';
|
||||
static const String addFace = '/v1/face/add';
|
||||
static const String editFace = '/v1/face/update';
|
||||
static const String deleteFace = '/v1/face/delete';
|
||||
static const String resetFace = '/v1/face/clear';
|
||||
|
||||
//电表
|
||||
static const String electricMeterList = '/v1/elec/list';
|
||||
static const String electricMeterAdd = '/v1/elec/add';
|
||||
static const String electricMeterRefresh = '/v1/elec/refreshElecInfo';
|
||||
static const String electricMeterClose = '/v1/elec/closeElec';
|
||||
static const String electricMeterOpen = '/v1/elec/openElec';
|
||||
static const String electricMeterGetUseEleRecord = '/v1/elec/getUseEleRecord';
|
||||
static const String electricMeterSet = '/v1/elec/updateElecSetting';
|
||||
static const String electricMeterDelet = '/v1/elec/delete';
|
||||
static const String electricMeterDetail = '/v1/elec/detail';
|
||||
|
||||
// 热水表
|
||||
static const String hotWaterMeterList = '/v1/hotWater/list';
|
||||
static const String hotWaterMeterAdd = '/v1/hotWater/add';
|
||||
static const String hotWaterMeterRefresh = '/v1/hotWater/refreshWaterInfo';
|
||||
static const String hotWaterMeterClose = '/v1/hotWater/closeWater';
|
||||
static const String hotWaterMeterOpen = '/v1/hotWater/openWater';
|
||||
static const String hotWaterMeterRecord = '/v1/hotWater/getUseTonsRecord';
|
||||
static const String hotWaterMeterReadRecord = '/v1/hotWater/getReadRecord';
|
||||
static const String hotWaterMeterDelet = '/v1/hotWater/delete';
|
||||
static const String hotWaterMeterSet = '/v1/hotWater/updateWaterSetting';
|
||||
|
||||
// 冷水表
|
||||
static const String coldWaterMeterList = '/v1/coldWater/list';
|
||||
static const String coldWaterMeterAdd = '/v1/coldWater/add';
|
||||
static const String coldWaterMeterRefresh = '/v1/coldWater/refreshWaterInfo';
|
||||
static const String coldWaterMeterClose = '/v1/coldWater/closeWater';
|
||||
static const String coldWaterMeterOpen = '/v1/coldWater/openWater';
|
||||
static const String coldWaterMeterRecord = '/v1/coldWater/getUseTonsRecord';
|
||||
static const String coldWaterMeterReadRecord = '/v1/coldWater/getReadRecord';
|
||||
static const String coldWaterMeterDelet = '/v1/coldWater/delete';
|
||||
static const String coldWaterMeterSet = '/v1/coldWater/updateWaterSetting';
|
||||
|
||||
//电子钥匙
|
||||
static const String electricKeysList = '/v1/key/listKey';
|
||||
static const String electricKeySender = '/v1/key/send';
|
||||
static const String electricKeySendBatch = '/v1/key/sendBatch';
|
||||
static const String electricKeyEdit = '/v1/key/update';
|
||||
static const String electricKeyDelet = '/v1/key/delete';
|
||||
static const String electricKeyClear = '/v1/key/clear';
|
||||
static const String electricKeyFreeze = '/v1/key/freeze'; //冻结
|
||||
static const String electricKeyUnfreeze = '/v1/key/unfreeze'; //解冻
|
||||
static const String electricUpdateLockUserNo =
|
||||
'/v1/key/updateLockUserNo'; //电子钥匙-更新钥匙UserNo
|
||||
|
||||
// 操作记录
|
||||
static const String recordList = '/v1/lockRecord/records';
|
||||
static const String getLastRecordTime = '/v1/lockRecord/getLastRecordTime';
|
||||
static const String uploadLockRecord = '/v1/lockRecord/upload';
|
||||
|
||||
static const String getServerTime = '/v1/lock/queryDate';
|
||||
static const String updateLockSetting =
|
||||
'/v1/lockSetting/updateLockSetting'; // 更新锁设置
|
||||
static const String readeLockSetting =
|
||||
'/v1/lockSetting/getLockSetting'; // 读取锁设置
|
||||
static const String getGatewayConfig =
|
||||
'/v1/gateway/getGatewayConfig';
|
||||
static const String getDeviceNetworkInfo =
|
||||
'/v1/deviceNetwork/getNetworkInfo';
|
||||
static const String updateDeviceNetworkInfo =
|
||||
'/v1/deviceNetwork/setting';
|
||||
static const String remoteUnlocking =
|
||||
'/v1/gateway/unlock';
|
||||
}
|
||||
42
lib/api/starcloud/starcloud_api_service.dart
Normal file
42
lib/api/starcloud/starcloud_api_service.dart
Normal file
@ -0,0 +1,42 @@
|
||||
import 'package:starwork_flutter/api/api_response.dart';
|
||||
import 'package:starwork_flutter/api/base_api_service.dart';
|
||||
import 'package:starwork_flutter/api/model/starcloud/request/starcloud_create_user_request.dart';
|
||||
import 'package:starwork_flutter/api/model/starcloud/request/starcloud_login_request.dart';
|
||||
import 'package:starwork_flutter/api/model/starcloud/response/starcloud_create_user_response.dart';
|
||||
import 'package:starwork_flutter/api/model/starcloud/response/starcloud_login_response.dart';
|
||||
import 'package:starwork_flutter/api/model/user/response/token_response.dart';
|
||||
import 'package:starwork_flutter/api/starcloud/starcloud_api_path.dart';
|
||||
import 'package:starwork_flutter/api/starcloud/starcloud_base_api_service.dart';
|
||||
import 'package:starwork_flutter/common/constant/http_constant.dart';
|
||||
|
||||
class StarCloudApiService {
|
||||
final StarCloudBaseApiService _api;
|
||||
|
||||
StarCloudApiService(this._api); // 通过构造函数注入
|
||||
|
||||
// 创建用户
|
||||
Future<ApiResponse<StarCloudCreateUserResponse>> createUser({
|
||||
required StarCloudCreateUserRequest request,
|
||||
}) {
|
||||
return _api.makeRequest(
|
||||
// 通过实例调用
|
||||
path: StarCloudApiPath.createUser,
|
||||
method: HttpConstant.post,
|
||||
data: request.toJson(),
|
||||
fromJson: (data) => StarCloudCreateUserResponse.fromJson(data),
|
||||
);
|
||||
}
|
||||
|
||||
// 登录星云
|
||||
Future<ApiResponse<StarCloudLoginResponse>> login({
|
||||
required StarCloudLoginRequest request,
|
||||
}) {
|
||||
return _api.makeRequest(
|
||||
// 通过实例调用
|
||||
path: StarCloudApiPath.createUser,
|
||||
method: HttpConstant.post,
|
||||
data: request.toJson(),
|
||||
fromJson: (data) => StarCloudLoginResponse.fromJson(data),
|
||||
);
|
||||
}
|
||||
}
|
||||
120
lib/api/starcloud/starcloud_base_api_service.dart
Normal file
120
lib/api/starcloud/starcloud_base_api_service.dart
Normal file
@ -0,0 +1,120 @@
|
||||
import 'package:dio/dio.dart' as dioAlias;
|
||||
|
||||
import 'package:starwork_flutter/api/api_response.dart';
|
||||
import 'package:starwork_flutter/common/constant/http_constant.dart';
|
||||
import 'package:starwork_flutter/flavors.dart';
|
||||
import 'package:flutter/foundation.dart'; // 用于 debugPrint
|
||||
|
||||
class StarCloudBaseApiService {
|
||||
final dioAlias.Dio dio = dioAlias.Dio();
|
||||
|
||||
StarCloudBaseApiService() {
|
||||
dio.options.baseUrl = F.starCloudUrl;
|
||||
dio.options.connectTimeout = const Duration(seconds: 30);
|
||||
dio.options.receiveTimeout = const Duration(seconds: 30);
|
||||
}
|
||||
|
||||
/// 统一请求方法
|
||||
Future<ApiResponse<T>> makeRequest<T>({
|
||||
required String path,
|
||||
String method = HttpConstant.post,
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
required T Function(dynamic) fromJson,
|
||||
}) async {
|
||||
try {
|
||||
// 🔍 打印请求信息(更详细控制)
|
||||
if (kDebugMode) {
|
||||
final uri = Uri.parse('${dio.options.baseUrl}$path');
|
||||
final queryString = queryParameters != null ? '?${uri.queryParameters.toString()}' : '';
|
||||
debugPrint('🟦 API Request: $method ${uri.toString()}$queryString');
|
||||
if (data != null) {
|
||||
debugPrint('🟦 Request Body: $data');
|
||||
}
|
||||
}
|
||||
|
||||
dioAlias.Response response;
|
||||
|
||||
switch (method.toUpperCase()) {
|
||||
case HttpConstant.get:
|
||||
response = await dio.get(path, queryParameters: queryParameters);
|
||||
break;
|
||||
case HttpConstant.post:
|
||||
response = await dio.post(path, data: data, queryParameters: queryParameters);
|
||||
break;
|
||||
case HttpConstant.put:
|
||||
response = await dio.put(path, data: data, queryParameters: queryParameters);
|
||||
break;
|
||||
case HttpConstant.delete:
|
||||
response = await dio.delete(path, data: data, queryParameters: queryParameters);
|
||||
break;
|
||||
case HttpConstant.patch:
|
||||
response = await dio.patch(path, data: data, queryParameters: queryParameters);
|
||||
break;
|
||||
default:
|
||||
return ApiResponse.error('Unsupported method: $method');
|
||||
}
|
||||
|
||||
// ✅ 打印响应
|
||||
if (kDebugMode) {
|
||||
debugPrint('🟩 API Response [${response.statusCode}] ${response.requestOptions.path}');
|
||||
debugPrint('🟩 Response Data: ${response.data}');
|
||||
}
|
||||
|
||||
if (response.statusCode! >= 200 && response.statusCode! < 300) {
|
||||
final jsonMap = response.data as Map<String, dynamic>;
|
||||
|
||||
// ✅ 直接用 ApiResponse.fromJson 解析整个响应
|
||||
final apiResponse = ApiResponse<T>.fromJson(jsonMap, fromJson);
|
||||
|
||||
// ✅ 判断业务是否成功(根据 errorCode)
|
||||
if (apiResponse.errorCode == 0) {
|
||||
return apiResponse; // 直接返回,data 已解析
|
||||
} else {
|
||||
// 业务失败,但 data 可能有部分信息
|
||||
return ApiResponse.error(
|
||||
apiResponse.errorMsg ?? 'Request failed',
|
||||
errorCode: apiResponse.errorCode,
|
||||
data: apiResponse.data,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return ApiResponse.error(
|
||||
response.statusMessage ?? 'Request failed',
|
||||
errorCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
} on dioAlias.DioException catch (e) {
|
||||
// ❌ 打印错误
|
||||
if (kDebugMode) {
|
||||
debugPrint('🟥 API Error: ${e.type} - ${e.message}');
|
||||
if (e.response != null) {
|
||||
debugPrint('🟥 Error Response: ${e.response?.data}');
|
||||
debugPrint('🟥 Status Code: ${e.response?.statusCode}');
|
||||
}
|
||||
}
|
||||
|
||||
String message = 'Unknown error';
|
||||
int? statusCode = e.response?.statusCode;
|
||||
|
||||
if (e.type == dioAlias.DioExceptionType.connectionTimeout) {
|
||||
message = 'Connection timeout';
|
||||
} else if (e.type == dioAlias.DioExceptionType.receiveTimeout) {
|
||||
message = 'Server timeout';
|
||||
} else if (e.type == dioAlias.DioExceptionType.badResponse) {
|
||||
message = e.response?.statusMessage ?? 'Bad response';
|
||||
} else if (e.type == dioAlias.DioExceptionType.connectionError) {
|
||||
message = 'Network not available';
|
||||
} else {
|
||||
message = e.message ?? 'Something went wrong';
|
||||
}
|
||||
|
||||
return ApiResponse.error(message, errorCode: statusCode);
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('🟥 Unexpected Error: $e');
|
||||
}
|
||||
return ApiResponse.error('Unexpected error: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,8 @@ import 'package:get/get.dart';
|
||||
import 'package:starwork_flutter/api/base_api_service.dart';
|
||||
import 'package:starwork_flutter/api/service/common_api_service.dart';
|
||||
import 'package:starwork_flutter/api/service/user_api_service.dart';
|
||||
import 'package:starwork_flutter/api/starcloud/starcloud_api_service.dart';
|
||||
import 'package:starwork_flutter/api/starcloud/starcloud_base_api_service.dart';
|
||||
import 'package:starwork_flutter/common/utils/shared_preferences_utils.dart';
|
||||
import 'package:starwork_flutter/flavors.dart';
|
||||
import 'package:starwork_flutter/views/login/login_controller.dart';
|
||||
@ -20,10 +22,12 @@ class AppInitialization {
|
||||
setSystemStatusBar();
|
||||
await SharedPreferencesUtils.init();
|
||||
initEasyLoading();
|
||||
|
||||
|
||||
Get.put(BaseApiService());
|
||||
Get.put(StarCloudBaseApiService());
|
||||
Get.put(CommonApiService(Get.find<BaseApiService>()));
|
||||
Get.put(UserApiService(Get.find<BaseApiService>()));
|
||||
Get.put(StarCloudApiService(Get.find<StarCloudBaseApiService>()));
|
||||
Get.put(LoginController());
|
||||
Get.put(MainController());
|
||||
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
|
||||
class BleConfig {
|
||||
// 私有构造函数
|
||||
BleConfig._() {
|
||||
// ✅ 这里就是单例初始化的地方
|
||||
// 只会执行一次(第一次获取实例时)
|
||||
}
|
||||
|
||||
// 静态实例
|
||||
static final BleConfig _instance = BleConfig._();
|
||||
static Guid serviceId = Guid('fff0');
|
||||
|
||||
// 工厂构造函数,提供全局访问点
|
||||
factory BleConfig() => _instance;
|
||||
// 用来订阅的特征id
|
||||
static Guid characteristicIdSubscription = Guid('fff1');
|
||||
|
||||
// 用来写入的特征id
|
||||
static Guid characteristicIdWrite = Guid('fff2');
|
||||
}
|
||||
|
||||
@ -2,9 +2,34 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import 'package:starwork_flutter/base/app_logger.dart';
|
||||
import 'package:starwork_flutter/ble/ble_config.dart';
|
||||
import 'package:starwork_flutter/ble/command/base/base_ble_command.dart';
|
||||
import 'package:starwork_flutter/ble/command/ble_command_manager.dart';
|
||||
import 'package:starwork_flutter/ble/command/request/ble_cmd_get_public_key.dart';
|
||||
import 'package:starwork_flutter/ble/command/response/ble_cmd_get_public_key_parser.dart';
|
||||
import 'package:starwork_flutter/ble/model/scan_device_info.dart';
|
||||
import 'package:starwork_flutter/common/constant/device_type.dart';
|
||||
|
||||
/// ✨✨✨ 待发送命令类 ✨✨✨
|
||||
class _PendingCommand<T> {
|
||||
final BaseBleCommand<T> command;
|
||||
final String targetDeviceId;
|
||||
final String? targetDeviceName;
|
||||
final Duration timeout;
|
||||
final Completer<T?> completer;
|
||||
final DateTime createdAt;
|
||||
final int commandId; // 添加命令ID字段
|
||||
|
||||
_PendingCommand({
|
||||
required this.command,
|
||||
required this.targetDeviceId,
|
||||
this.targetDeviceName,
|
||||
required this.timeout,
|
||||
required this.completer,
|
||||
required this.commandId, // 添加命令ID参数
|
||||
}) : createdAt = DateTime.now();
|
||||
}
|
||||
|
||||
class BleService {
|
||||
// 私有构造函数
|
||||
BleService._() {
|
||||
@ -22,15 +47,57 @@ class BleService {
|
||||
/// 用来存储搜索到的设备,并用于去重和过滤
|
||||
final Map<String, ScanResult> _discoveredDevices = {};
|
||||
|
||||
// 使用命令管理器处理数据包
|
||||
final BleCommandManager bleCommandManager = BleCommandManager();
|
||||
|
||||
/// mtu大小
|
||||
int _mtu = 23;
|
||||
|
||||
/// 用来监听蓝牙适配器状态的订阅流
|
||||
StreamSubscription<BluetoothAdapterState>? _adapterStateSubscription;
|
||||
|
||||
/// 用来监听搜索到的设备的订阅流
|
||||
StreamSubscription<List<ScanResult>>? _scanResultSubscription;
|
||||
|
||||
/// 用来监听连接设备时的连接状态
|
||||
StreamSubscription<BluetoothConnectionState>? _connectionStateSubscription;
|
||||
|
||||
/// 用来监听mtu的变化
|
||||
StreamSubscription<int>? _mtuChangeSubscription;
|
||||
|
||||
/// 用来监听订阅服务的数据
|
||||
StreamSubscription<List<int>>? _characteristicDataSubscription;
|
||||
|
||||
/// 当前连接的设备
|
||||
BluetoothDevice? _connectedDevice;
|
||||
|
||||
/// 当前的写入特征值
|
||||
BluetoothCharacteristic? _writeCharacteristic;
|
||||
|
||||
/// 当前的订阅特征值
|
||||
BluetoothCharacteristic? _subscriptionCharacteristic;
|
||||
|
||||
/// ✨✨✨ 命令响应等待器映射 ✨✨✨
|
||||
final Map<String, Completer<dynamic>> _commandResponseWaiters = {};
|
||||
|
||||
/// ✨✨✨ 命令超时定时器映射 ✨✨✨
|
||||
final Map<String, Timer> _commandTimeouts = {};
|
||||
|
||||
/// ✨✨✨ 命令ID到命令键的映射 ✨✨✨
|
||||
final Map<int, String> _commandIdToKeyMap = {};
|
||||
|
||||
/// 待发送命令队列 (用于自动连接后发送)
|
||||
final List<_PendingCommand<dynamic>> _pendingCommands = [];
|
||||
|
||||
/// 搜索状态监控定时器
|
||||
Timer? _scanningMonitorTimer;
|
||||
|
||||
// 内部维护的蓝牙状态
|
||||
BluetoothAdapterState _bluetoothAdapterState = BluetoothAdapterState.unknown;
|
||||
|
||||
// 内部维护的设备连接状态
|
||||
BluetoothConnectionState _bluetoothConnectionState = BluetoothConnectionState.disconnected;
|
||||
|
||||
/// 提供外部获取蓝牙适配器方法
|
||||
BluetoothAdapterState get bluetoothAdapterState => _bluetoothAdapterState;
|
||||
|
||||
@ -53,90 +120,308 @@ class BleService {
|
||||
/// 开启蓝牙搜索
|
||||
void enableBluetoothSearch({
|
||||
DeviceType deviceType = DeviceType.all,
|
||||
Duration searchTime = const Duration(seconds: 30),
|
||||
Duration searchTime = const Duration(seconds: 15),
|
||||
required void Function(ScanDeviceInfo device) onDeviceFound,
|
||||
}) async {
|
||||
// 如果正在搜索中,直接返回
|
||||
var isScanningNow = FlutterBluePlus.isScanningNow;
|
||||
if (isScanningNow) {
|
||||
AppLogger.warn('正处于搜索状态,请勿重复搜索');
|
||||
return;
|
||||
// 检查蓝牙适配器状态
|
||||
AppLogger.highlight('🔍 当前蓝牙适配器状态: $_bluetoothAdapterState');
|
||||
|
||||
// 如果状态是unknown,等待一下状态更新
|
||||
if (_bluetoothAdapterState == BluetoothAdapterState.unknown) {
|
||||
AppLogger.highlight('⏳ 等待蓝牙适配器状态更新...');
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
AppLogger.highlight('🔍 等待后蓝牙适配器状态: $_bluetoothAdapterState');
|
||||
}
|
||||
|
||||
if (_bluetoothConnectionState == BluetoothConnectionState.connected) {
|
||||
// 搜索时断开已有连接
|
||||
List<BluetoothDevice> devs = FlutterBluePlus.connectedDevices;
|
||||
for (var d in devs) {
|
||||
d.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
if (_bluetoothAdapterState == BluetoothAdapterState.on) {
|
||||
FlutterBluePlus.startScan(timeout: searchTime);
|
||||
AppLogger.highlight('🚀 开始启动蓝牙扫描,搜索时长: ${searchTime.inSeconds}秒');
|
||||
|
||||
/// 取消旧的订阅,防止重复
|
||||
_scanResultSubscription?.cancel();
|
||||
_discoveredDevices.clear();
|
||||
try {
|
||||
FlutterBluePlus.startScan(timeout: searchTime);
|
||||
|
||||
/// 监听搜索到的设备
|
||||
_scanResultSubscription = FlutterBluePlus.onScanResults.listen(
|
||||
(List<ScanResult> results) {
|
||||
for (var result in results) {
|
||||
var device = result.device;
|
||||
final deviceId = device.remoteId.toString();
|
||||
final platformName = device.platformName;
|
||||
var serviceUuids = result.advertisementData.serviceUuids;
|
||||
/// 取消旧的订阅,防止重复
|
||||
_scanResultSubscription?.cancel();
|
||||
_discoveredDevices.clear();
|
||||
|
||||
// ✅ 只有新设备才回调
|
||||
if (!_discoveredDevices.containsKey(deviceId) && platformName.isNotEmpty) {
|
||||
_discoveredDevices[deviceId] = result;
|
||||
bool pairStatus = false;
|
||||
bool hasNewEvent = false;
|
||||
for (var uuid in serviceUuids) {
|
||||
String uuidStr = uuid.toString().replaceAll('-', '');
|
||||
if (uuidStr.length == 8) {
|
||||
var pairStatusStr = uuidStr.substring(4, 6);
|
||||
var hasNewEventStr = uuidStr.substring(6, 8);
|
||||
pairStatus = pairStatusStr == '01';
|
||||
hasNewEvent = hasNewEventStr == '01';
|
||||
var scanDeviceInfo = ScanDeviceInfo(
|
||||
isBinding: pairStatus,
|
||||
advName: device.advName,
|
||||
rawDeviceInfo: result,
|
||||
hasNewEvent: hasNewEvent,
|
||||
);
|
||||
onDeviceFound.call(scanDeviceInfo);
|
||||
} else if (uuidStr.length == 32) {
|
||||
var pairStatusStr = uuidStr.substring(26, 28);
|
||||
pairStatus = pairStatusStr == '00'; // 第4、5位(索引3和4)
|
||||
int statusValue = int.parse(pairStatusStr, radix: 16);
|
||||
// 提取 byte0(配对状态:第1位)
|
||||
int byte0 = (statusValue >> 0) & 0x01; // 取最低位
|
||||
// 提取 byte1(事件状态:第2位)
|
||||
int byte1 = (statusValue >> 1) & 0x01; // 取次低位
|
||||
// 判断是否未配对
|
||||
pairStatus = (byte0 == 1);
|
||||
// 判断是否有新事件
|
||||
hasNewEvent = (byte1 == 1);
|
||||
var scanDeviceInfo = ScanDeviceInfo(
|
||||
isBinding: pairStatus,
|
||||
advName: device.advName,
|
||||
rawDeviceInfo: result,
|
||||
hasNewEvent: hasNewEvent,
|
||||
);
|
||||
onDeviceFound.call(scanDeviceInfo);
|
||||
/// 监听搜索到的设备
|
||||
_scanResultSubscription = FlutterBluePlus.onScanResults.listen(
|
||||
(List<ScanResult> results) {
|
||||
for (var result in results) {
|
||||
var device = result.device;
|
||||
final deviceId = device.remoteId.toString();
|
||||
final platformName = device.platformName;
|
||||
var serviceUuids = result.advertisementData.serviceUuids;
|
||||
|
||||
// ✅ 只有新设备才回调
|
||||
if (!_discoveredDevices.containsKey(deviceId) && platformName.isNotEmpty) {
|
||||
_discoveredDevices[deviceId] = result;
|
||||
bool pairStatus = false;
|
||||
bool hasNewEvent = false;
|
||||
for (var uuid in serviceUuids) {
|
||||
String uuidStr = uuid.toString().replaceAll('-', '');
|
||||
if (uuidStr.length == 8) {
|
||||
var pairStatusStr = uuidStr.substring(4, 6);
|
||||
var hasNewEventStr = uuidStr.substring(6, 8);
|
||||
pairStatus = pairStatusStr == '01';
|
||||
hasNewEvent = hasNewEventStr == '01';
|
||||
var scanDeviceInfo = ScanDeviceInfo(
|
||||
isBinding: pairStatus,
|
||||
advName: device.platformName,
|
||||
rawDeviceInfo: result,
|
||||
hasNewEvent: hasNewEvent,
|
||||
);
|
||||
onDeviceFound.call(scanDeviceInfo);
|
||||
} else if (uuidStr.length == 32) {
|
||||
var pairStatusStr = uuidStr.substring(26, 28);
|
||||
pairStatus = pairStatusStr == '00'; // 第4、5位(索引3和4)
|
||||
int statusValue = int.parse(pairStatusStr, radix: 16);
|
||||
// 提取 byte0(配对状态:第1位)
|
||||
int byte0 = (statusValue >> 0) & 0x01; // 取最低位
|
||||
// 提取 byte1(事件状态:第2位)
|
||||
int byte1 = (statusValue >> 1) & 0x01; // 取次低位
|
||||
// 判断是否未配对
|
||||
pairStatus = (byte0 == 1);
|
||||
// 判断是否有新事件
|
||||
hasNewEvent = (byte1 == 1);
|
||||
var scanDeviceInfo = ScanDeviceInfo(
|
||||
isBinding: pairStatus,
|
||||
advName: device.platformName,
|
||||
rawDeviceInfo: result,
|
||||
hasNewEvent: hasNewEvent,
|
||||
);
|
||||
onDeviceFound.call(scanDeviceInfo);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 可选:更新 RSSI
|
||||
_discoveredDevices[deviceId] = result;
|
||||
}
|
||||
} else {
|
||||
// 可选:更新 RSSI
|
||||
_discoveredDevices[deviceId] = result;
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: (e) => AppLogger.error('搜索设备时遇到错误:' + e),
|
||||
);
|
||||
},
|
||||
onError: (e) => AppLogger.error('搜索设备时遇到错误:' + e),
|
||||
);
|
||||
|
||||
AppLogger.highlight('✅ 蓝牙搜索已成功启动');
|
||||
|
||||
// 监听搜索状态变化
|
||||
_monitorScanningState();
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error('启动蓝牙搜索失败', error: e, stackTrace: stackTrace);
|
||||
}
|
||||
} else {
|
||||
AppLogger.error('❌ 蓝牙适配器未开启,当前状态: $_bluetoothAdapterState');
|
||||
}
|
||||
}
|
||||
|
||||
/// 监控搜索状态变化
|
||||
void _monitorScanningState() {
|
||||
// 取消之前的定时器
|
||||
_scanningMonitorTimer?.cancel();
|
||||
|
||||
_scanningMonitorTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
bool currentlyScanning = FlutterBluePlus.isScanningNow;
|
||||
|
||||
if (!currentlyScanning) {
|
||||
AppLogger.highlight('🔍 搜索已停止 (监控器检测)');
|
||||
timer.cancel();
|
||||
_scanningMonitorTimer = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 停止扫描
|
||||
void stopBluetoothSearch() async {
|
||||
var isScanningNow = FlutterBluePlus.isScanningNow;
|
||||
if (isScanningNow) {
|
||||
FlutterBluePlus.stopScan();
|
||||
}
|
||||
|
||||
/// 清空搜索到的设备
|
||||
_discoveredDevices.clear();
|
||||
// 取消搜索状态监控定时器
|
||||
_scanningMonitorTimer?.cancel();
|
||||
_scanningMonitorTimer = null;
|
||||
|
||||
/// 清空搜索到的设备
|
||||
_discoveredDevices.clear();
|
||||
|
||||
AppLogger.highlight('🛑 蓝牙搜索已停止');
|
||||
}
|
||||
|
||||
void seedData({
|
||||
required BaseBleCommand command,
|
||||
}) async {
|
||||
// 搜索时断开已有连接
|
||||
List<BluetoothDevice> devs = FlutterBluePlus.connectedDevices;
|
||||
for (var d in devs) {
|
||||
d.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/// 监听连接设备时的状态
|
||||
void _handleListenBluetoothConnectionState(BluetoothConnectionState state) {
|
||||
_bluetoothConnectionState = state;
|
||||
}
|
||||
|
||||
/// 监听mtu变化
|
||||
void _handleListenMtuChange(int mtu) {
|
||||
_mtu = mtu;
|
||||
BaseBleCommand.MAX_PACKET_DATA_SIZE = mtu;
|
||||
}
|
||||
|
||||
/// 监听订阅值变化
|
||||
void _handleListenCharacteristicData(List<int> data) {
|
||||
AppLogger.highlight('✨✨✨ 📨 收到订阅数据:$data (长度: ${data.length}) ✨✨✨');
|
||||
|
||||
// 解析数据
|
||||
if (data.isNotEmpty) {
|
||||
dynamic result = bleCommandManager.handleResponse(data);
|
||||
|
||||
if (result != null) {
|
||||
// 触发命令响应等待器
|
||||
_triggerCommandResponseWaiters(result);
|
||||
|
||||
// 如果是获取公钥的应答,可以进行类型转换
|
||||
// if (result is GetPublicKeyResponse) {
|
||||
// GetPublicKeyResponse response = result;
|
||||
// }
|
||||
} else {
|
||||
AppLogger.warn('⚠️ 数据包解析失败或不匹配任何命令');
|
||||
}
|
||||
} else {
|
||||
AppLogger.warn('✨✨✨ ⚠️ 收到空数据 ✨✨✨');
|
||||
}
|
||||
}
|
||||
|
||||
/// ✨✨✨ 触发命令响应等待器 ✨✨✨
|
||||
void _triggerCommandResponseWaiters(dynamic response) {
|
||||
// 遍历所有等待中的命令,找到匹配的进行响应
|
||||
List<String> completedKeys = [];
|
||||
|
||||
// 如果响应是GetPublicKeyResponse类型,尝试提取命令ID进行匹配
|
||||
|
||||
int commandId = response.commandId;
|
||||
String? commandKey = _commandIdToKeyMap[commandId];
|
||||
|
||||
if (commandKey != null) {
|
||||
// 精确匹配到特定命令
|
||||
Completer? completer = _commandResponseWaiters[commandKey];
|
||||
if (completer != null && !completer.isCompleted) {
|
||||
AppLogger.debug('🔔 精确匹配命令响应: 命令ID=0x${commandId.toRadixString(16).padLeft(4, '0')}, 键=$commandKey');
|
||||
completer.complete(response);
|
||||
completedKeys.add(commandKey);
|
||||
|
||||
// 清理已完成的等待器
|
||||
_cleanupCommandWaiter(commandKey);
|
||||
_commandIdToKeyMap.remove(commandId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有精确匹配,回退到原来的逻辑(完成所有未完成的等待器)
|
||||
_commandResponseWaiters.forEach((key, completer) {
|
||||
if (!completer.isCompleted) {
|
||||
AppLogger.debug('🔔 触发命令响应(回退模式): $key');
|
||||
completer.complete(response);
|
||||
completedKeys.add(key);
|
||||
}
|
||||
});
|
||||
|
||||
// 清理已完成的等待器
|
||||
for (String key in completedKeys) {
|
||||
_cleanupCommandWaiter(key);
|
||||
}
|
||||
}
|
||||
|
||||
/// ✨✨✨ 发送蓝牙命令 ✨✨✨
|
||||
Future<T?> sendCommand<T>({
|
||||
required BaseBleCommand<T> command,
|
||||
String? targetDeviceId,
|
||||
String? targetDeviceName,
|
||||
Duration timeout = const Duration(seconds: 10),
|
||||
bool autoConnectIfNeeded = true,
|
||||
Duration searchTimeoutIfNeeded = const Duration(seconds: 15),
|
||||
}) async {
|
||||
AppLogger.highlight('✨✨✨ 🚀 开始发送蓝牙命令: ${command.runtimeType} ✨✨✨');
|
||||
|
||||
try {
|
||||
// 1. 检查连接状态
|
||||
bool isConnected = await _ensureDeviceConnected(
|
||||
targetDeviceId: targetDeviceId,
|
||||
targetDeviceName: targetDeviceName,
|
||||
autoConnect: autoConnectIfNeeded,
|
||||
searchTimeout: searchTimeoutIfNeeded,
|
||||
);
|
||||
|
||||
if (!isConnected) {
|
||||
throw Exception('设备未连接,无法发送命令');
|
||||
}
|
||||
|
||||
// 2. 检查写入特征值
|
||||
if (_writeCharacteristic == null) {
|
||||
throw Exception('写入特征值未初始化');
|
||||
}
|
||||
|
||||
// 3. 构建命令数据包
|
||||
List<List<int>> packets = command.build();
|
||||
AppLogger.info('📦 命令数据包数量: ${packets.length}');
|
||||
|
||||
// 4. 设置命令响应等待器
|
||||
String commandKey = _generateCommandKey(command);
|
||||
Completer<T?> responseCompleter = Completer<T?>();
|
||||
_commandResponseWaiters[commandKey] = responseCompleter;
|
||||
|
||||
// 5. 如果命令有cmdId静态字段,将其与命令键关联
|
||||
try {
|
||||
// 对于特定类型的命令,手动检查cmdId字段
|
||||
if (command is BleCmdGetPublicKey) {
|
||||
int commandId = BleCmdGetPublicKey.cmdId;
|
||||
_commandIdToKeyMap[commandId] = commandKey;
|
||||
AppLogger.debug('📝 命令ID映射: 0x${commandId.toRadixString(16).padLeft(4, '0')} -> $commandKey');
|
||||
}
|
||||
// 可以为其他命令类型添加类似的检查
|
||||
} catch (e) {
|
||||
AppLogger.warn('⚠️ 无法获取命令ID: $e');
|
||||
}
|
||||
|
||||
// 6. 设置超时定时器
|
||||
Timer timeoutTimer = Timer(timeout, () {
|
||||
if (!responseCompleter.isCompleted) {
|
||||
_cleanupCommandWaiter(commandKey);
|
||||
responseCompleter.completeError(TimeoutException('命令响应超时', timeout));
|
||||
}
|
||||
});
|
||||
_commandTimeouts[commandKey] = timeoutTimer;
|
||||
|
||||
// 7. 发送数据包
|
||||
for (int i = 0; i < packets.length; i++) {
|
||||
List<int> packet = packets[i];
|
||||
AppLogger.debug('📤 发送第${i + 1}个数据包,数据包: (${packet.toString()},长度:${packet.length}字节)');
|
||||
|
||||
await _writeCharacteristic!.write(packet, withoutResponse: false);
|
||||
|
||||
// 在多包发送时稍微延迟
|
||||
// if (i < packets.length - 1) {
|
||||
// await Future.delayed(const Duration(milliseconds: 50));
|
||||
// }
|
||||
}
|
||||
|
||||
AppLogger.info('✅ 所有数据包发送完成,等待应答...');
|
||||
|
||||
// 8. 等待响应
|
||||
T? response = await responseCompleter.future;
|
||||
|
||||
AppLogger.highlight('✨✨✨ ✅ 命令发送成功,收到应答: $response ✨✨✨');
|
||||
return response;
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error('❌ 发送蓝牙命令失败', error: e, stackTrace: stackTrace);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@ -144,6 +429,273 @@ class BleService {
|
||||
/// 销毁蓝牙适配器监听
|
||||
_adapterStateSubscription?.cancel();
|
||||
_scanResultSubscription?.cancel();
|
||||
_connectionStateSubscription?.cancel();
|
||||
_mtuChangeSubscription?.cancel();
|
||||
_characteristicDataSubscription?.cancel();
|
||||
|
||||
/// 取消搜索状态监控定时器
|
||||
_scanningMonitorTimer?.cancel();
|
||||
_scanningMonitorTimer = null;
|
||||
|
||||
/// 清理命令等待器
|
||||
for (String key in _commandResponseWaiters.keys) {
|
||||
_cleanupCommandWaiter(key);
|
||||
}
|
||||
_commandResponseWaiters.clear();
|
||||
_commandTimeouts.clear();
|
||||
_commandIdToKeyMap.clear(); // 清空命令ID映射
|
||||
|
||||
/// 清理待发送命令
|
||||
for (_PendingCommand<dynamic> pendingCmd in _pendingCommands) {
|
||||
if (!pendingCmd.completer.isCompleted) {
|
||||
pendingCmd.completer.completeError(Exception('服务已关闭'));
|
||||
}
|
||||
}
|
||||
_pendingCommands.clear();
|
||||
|
||||
/// 重置连接状态
|
||||
_bluetoothAdapterState = BluetoothAdapterState.unknown;
|
||||
_connectedDevice = null;
|
||||
_writeCharacteristic = null;
|
||||
_subscriptionCharacteristic = null;
|
||||
|
||||
AppLogger.highlight('🗑️ BleService 资源已清理');
|
||||
}
|
||||
|
||||
/// ✨✨✨ 确保设备已连接 ✨✨✨
|
||||
Future<bool> _ensureDeviceConnected({
|
||||
String? targetDeviceId,
|
||||
String? targetDeviceName,
|
||||
bool autoConnect = true,
|
||||
Duration searchTimeout = const Duration(seconds: 15),
|
||||
}) async {
|
||||
// 1. 检查当前连接状态
|
||||
if (_connectedDevice != null && _bluetoothConnectionState == BluetoothConnectionState.connected) {
|
||||
// 如果指定了目标设备ID,检查是否匹配
|
||||
if (targetDeviceId != null && _connectedDevice!.remoteId.toString() != targetDeviceId) {
|
||||
AppLogger.info('🔄 当前连接的设备与目标不匹配,需要切换连接');
|
||||
await _connectedDevice!.disconnect();
|
||||
_connectedDevice = null;
|
||||
_writeCharacteristic = null;
|
||||
_subscriptionCharacteristic = null;
|
||||
} else {
|
||||
AppLogger.info('✅ 设备已连接,无需重新连接');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 如果不允许自动连接,直接返回失败
|
||||
if (!autoConnect) {
|
||||
AppLogger.warn('⚠️ 设备未连接且不允许自动连接');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. 尝试查找已连接的设备
|
||||
List<BluetoothDevice> connectedDevices = FlutterBluePlus.connectedDevices;
|
||||
if (targetDeviceId != null) {
|
||||
for (BluetoothDevice device in connectedDevices) {
|
||||
if (device.remoteId.toString() == targetDeviceId) {
|
||||
AppLogger.info('🔗 找到已连接的目标设备');
|
||||
return await _setupDeviceConnection(device);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 需要搜索并连接设备
|
||||
AppLogger.info('🔍 开始搜索目标设备...');
|
||||
BluetoothDevice? foundDevice = await _searchForTargetDevice(
|
||||
targetDeviceId: targetDeviceId,
|
||||
targetDeviceName: targetDeviceName,
|
||||
searchTimeout: searchTimeout,
|
||||
);
|
||||
|
||||
if (foundDevice == null) {
|
||||
AppLogger.error('❌ 未找到目标设备');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 5. 连接设备
|
||||
AppLogger.info('🔗 开始连接设备: ${foundDevice.platformName}');
|
||||
return await _connectToDevice(foundDevice);
|
||||
}
|
||||
|
||||
/// ✨✨✨ 搜索目标设备 ✨✨✨
|
||||
Future<BluetoothDevice?> _searchForTargetDevice({
|
||||
String? targetDeviceId,
|
||||
String? targetDeviceName,
|
||||
Duration searchTimeout = const Duration(seconds: 15),
|
||||
}) async {
|
||||
Completer<BluetoothDevice?> searchCompleter = Completer<BluetoothDevice?>();
|
||||
|
||||
// 启动搜索
|
||||
enableBluetoothSearch(
|
||||
searchTime: searchTimeout,
|
||||
onDeviceFound: (ScanDeviceInfo deviceInfo) {
|
||||
BluetoothDevice device = deviceInfo.rawDeviceInfo.device;
|
||||
String deviceId = device.remoteId.toString();
|
||||
String deviceName = device.platformName;
|
||||
|
||||
AppLogger.debug('🔍 发现设备: $deviceName ($deviceId)');
|
||||
|
||||
// 检查是否匹配目标设备
|
||||
bool isTargetDevice = false;
|
||||
|
||||
if (targetDeviceId != null && deviceId == targetDeviceId) {
|
||||
isTargetDevice = true;
|
||||
AppLogger.info('🎯 通过ID匹配到目标设备: $deviceName');
|
||||
} else if (targetDeviceName != null && deviceName.contains(targetDeviceName)) {
|
||||
isTargetDevice = true;
|
||||
AppLogger.info('🎯 通过名称匹配到目标设备: $deviceName');
|
||||
}
|
||||
|
||||
if (isTargetDevice && !searchCompleter.isCompleted) {
|
||||
searchCompleter.complete(device);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 设置超时
|
||||
Timer(searchTimeout, () {
|
||||
if (!searchCompleter.isCompleted) {
|
||||
searchCompleter.complete(null);
|
||||
}
|
||||
});
|
||||
|
||||
return await searchCompleter.future;
|
||||
}
|
||||
|
||||
/// ✨✨✨ 连接到设备 ✨✨✨
|
||||
Future<bool> _connectToDevice(BluetoothDevice device) async {
|
||||
try {
|
||||
AppLogger.info('🔗 正在连接设备: ${device.platformName}');
|
||||
|
||||
await device.connect(timeout: const Duration(seconds: 10));
|
||||
|
||||
// 设置连接设备
|
||||
return await _setupDeviceConnection(device);
|
||||
} catch (e) {
|
||||
AppLogger.error('❌ 连接设备失败: ${device.platformName}', error: e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// ✨✨✨ 设置设备连接 ✨✨✨
|
||||
Future<bool> _setupDeviceConnection(BluetoothDevice device) async {
|
||||
try {
|
||||
AppLogger.info('🔧 设置设备连接: ${device.platformName}');
|
||||
|
||||
// 更新内部状态
|
||||
_connectedDevice = device;
|
||||
|
||||
// 监听连接状态
|
||||
_connectionStateSubscription?.cancel();
|
||||
_connectionStateSubscription = device.connectionState.listen(_handleListenBluetoothConnectionState);
|
||||
|
||||
// 监听MTU变化
|
||||
_mtuChangeSubscription?.cancel();
|
||||
_mtuChangeSubscription = device.mtu.listen(_handleListenMtuChange);
|
||||
|
||||
// 发现服务
|
||||
List<BluetoothService> services = await device.discoverServices();
|
||||
AppLogger.info('🔍 发现服务数量: ${services.length}');
|
||||
|
||||
// 查找目标服务
|
||||
BluetoothService? targetService;
|
||||
for (BluetoothService service in services) {
|
||||
if (service.uuid == BleConfig.serviceId) {
|
||||
targetService = service;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetService == null) {
|
||||
throw Exception('未找到目标服务: ${BleConfig.serviceId}');
|
||||
}
|
||||
|
||||
AppLogger.info('✅ 找到目标服务: ${targetService.uuid}');
|
||||
|
||||
// 查找特征值
|
||||
BluetoothCharacteristic? writeChar;
|
||||
BluetoothCharacteristic? subscribeChar;
|
||||
|
||||
for (BluetoothCharacteristic characteristic in targetService.characteristics) {
|
||||
if (characteristic.uuid == BleConfig.characteristicIdWrite) {
|
||||
writeChar = characteristic;
|
||||
AppLogger.info('✅ 找到写入特征值: ${characteristic.uuid}');
|
||||
} else if (characteristic.uuid == BleConfig.characteristicIdSubscription) {
|
||||
subscribeChar = characteristic;
|
||||
AppLogger.info('✅ 找到订阅特征值: ${characteristic.uuid}');
|
||||
}
|
||||
}
|
||||
|
||||
if (writeChar == null || subscribeChar == null) {
|
||||
throw Exception('未找到所需的特征值');
|
||||
}
|
||||
|
||||
// 设置特征值
|
||||
_writeCharacteristic = writeChar;
|
||||
_subscriptionCharacteristic = subscribeChar;
|
||||
|
||||
// 订阅通知
|
||||
await subscribeChar.setNotifyValue(true);
|
||||
AppLogger.info('✅ 已订阅通知');
|
||||
|
||||
// 监听数据
|
||||
_characteristicDataSubscription?.cancel();
|
||||
_characteristicDataSubscription = subscribeChar.onValueReceived.listen(_handleListenCharacteristicData);
|
||||
|
||||
AppLogger.highlight('✨✨✨ ✅ 设备连接设置完成 ✨✨✨');
|
||||
|
||||
// 处理待发送的命令
|
||||
await _processPendingCommands();
|
||||
|
||||
return true;
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error('❌ 设置设备连接失败', error: e, stackTrace: stackTrace);
|
||||
_connectedDevice = null;
|
||||
_writeCharacteristic = null;
|
||||
_subscriptionCharacteristic = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// ✨✨✨ 处理待发送的命令 ✨✨✨
|
||||
Future<void> _processPendingCommands() async {
|
||||
if (_pendingCommands.isEmpty) return;
|
||||
|
||||
AppLogger.info('📦 处理待发送命令: ${_pendingCommands.length}个');
|
||||
|
||||
List<_PendingCommand<dynamic>> commandsToProcess = List.from(_pendingCommands);
|
||||
_pendingCommands.clear();
|
||||
|
||||
for (_PendingCommand<dynamic> pendingCmd in commandsToProcess) {
|
||||
try {
|
||||
AppLogger.info('🚀 发送待处理命令: ${pendingCmd.commandId}');
|
||||
dynamic result = await sendCommand(
|
||||
command: pendingCmd.command,
|
||||
targetDeviceId: pendingCmd.targetDeviceId,
|
||||
targetDeviceName: pendingCmd.targetDeviceName,
|
||||
timeout: pendingCmd.timeout,
|
||||
);
|
||||
pendingCmd.completer.complete(result);
|
||||
} catch (e) {
|
||||
pendingCmd.completer.completeError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ✨✨✨ 生成命令唯一标识 ✨✨✨
|
||||
String _generateCommandKey<T>(BaseBleCommand<T> command) {
|
||||
return '${command.runtimeType}_${DateTime.now().millisecondsSinceEpoch}';
|
||||
}
|
||||
|
||||
/// ✨✨✨ 清理命令等待器 ✨✨✨
|
||||
void _cleanupCommandWaiter(String commandKey) {
|
||||
// 从命令ID映射中移除
|
||||
_commandIdToKeyMap.removeWhere((commandId, key) => key == commandKey);
|
||||
|
||||
_commandResponseWaiters.remove(commandKey);
|
||||
Timer? timer = _commandTimeouts.remove(commandKey);
|
||||
timer?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
398
lib/ble/command/base/base_ble_command.dart
Normal file
398
lib/ble/command/base/base_ble_command.dart
Normal file
@ -0,0 +1,398 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:starwork_flutter/base/app_logger.dart';
|
||||
|
||||
/// ✨✨✨ 数据包解析结果类 ✨✨✨
|
||||
class ParsedPacket {
|
||||
final bool isValid;
|
||||
final int packetType;
|
||||
final int packetSequence;
|
||||
final int version;
|
||||
final int encryptType;
|
||||
final int encryptedDataLength;
|
||||
final int originalDataLength;
|
||||
final List<int> data;
|
||||
final int crc16;
|
||||
final String? errorMessage;
|
||||
|
||||
ParsedPacket({
|
||||
required this.isValid,
|
||||
required this.packetType,
|
||||
required this.packetSequence,
|
||||
required this.version,
|
||||
required this.encryptType,
|
||||
required this.encryptedDataLength,
|
||||
required this.originalDataLength,
|
||||
required this.data,
|
||||
required this.crc16,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
if (!isValid) {
|
||||
return 'ParsedPacket(invalid: $errorMessage)';
|
||||
}
|
||||
return 'ParsedPacket(type: 0x${packetType.toRadixString(16).padLeft(2, '0')}, ' +
|
||||
'seq: $packetSequence, version: ${version >> 4}, encrypt: $encryptType, ' +
|
||||
'dataLen: $originalDataLength, crc: 0x${crc16.toRadixString(16).padLeft(4, '0')})';
|
||||
}
|
||||
}
|
||||
|
||||
/// ✨✨✨ 蓝牙命令基类 - 提供数据包组装功能 ✨✨✨
|
||||
abstract class BaseBleCommand<T> {
|
||||
/// 包头固定值: 0XEF01EE02
|
||||
static const List<int> PACKET_HEADER = [0xEF, 0x01, 0xEE, 0x02];
|
||||
|
||||
/// 包类型
|
||||
static const int PACKET_TYPE_REQUEST = 0x01; // 请求包
|
||||
static const int PACKET_TYPE_RESPONSE = 0x11; // 应答包
|
||||
|
||||
/// 包版本 (高4位)
|
||||
static const int PACKET_VERSION = 0x10; // 版本1.0
|
||||
|
||||
/// 加密类型 (低4位)
|
||||
static const int ENCRYPT_TYPE_PLAIN = 0x00; // 明文
|
||||
static const int ENCRYPT_TYPE_AES128 = 0x01; // AES128
|
||||
static const int ENCRYPT_TYPE_SM4_PRESET = 0x02; // SM4(事先约定密钥)
|
||||
static const int ENCRYPT_TYPE_SM4_DEVICE = 0x03; // SM4(设备指定密钥)
|
||||
|
||||
/// 最大单包数据长度 (可根据MTU调整)
|
||||
static int MAX_PACKET_DATA_SIZE = 100;
|
||||
|
||||
/// 全局包序号计数器
|
||||
static int _globalPacketSequence = 1;
|
||||
|
||||
/// ✨✨✨ 获取下一个包序号 ✨✨✨
|
||||
static int getNextPacketSequence() {
|
||||
int sequence = _globalPacketSequence;
|
||||
_globalPacketSequence++;
|
||||
if (_globalPacketSequence > 0xFFFF) {
|
||||
_globalPacketSequence = 1; // 2字节范围内循环
|
||||
}
|
||||
return sequence;
|
||||
}
|
||||
|
||||
/// ✨✨✨ 组装包头 ✨✨✨
|
||||
/// [packetType] 包类型 (0x01请求包, 0x11应答包)
|
||||
/// [packetSequence] 包序号 (2字节)
|
||||
/// [encryptType] 加密类型 (0:明文, 1:AES128, 2:SM4事先约定, 3:SM4设备指定)
|
||||
/// [dataLength] 数据长度 (高16位:加密后长度, 低16位:原始长度)
|
||||
static List<int> buildPacketHeader({
|
||||
required int packetType,
|
||||
required int packetSequence,
|
||||
int encryptType = ENCRYPT_TYPE_PLAIN,
|
||||
required int encryptedDataLength,
|
||||
required int originalDataLength,
|
||||
}) {
|
||||
List<int> header = [];
|
||||
|
||||
// 1. 包头 (4字节)
|
||||
header.addAll(PACKET_HEADER);
|
||||
|
||||
// 2. 包类型 (1字节)
|
||||
header.add(packetType);
|
||||
|
||||
// 3. 包序号 (2字节,大端序)
|
||||
header.add((packetSequence >> 8) & 0xFF); // 高字节
|
||||
header.add(packetSequence & 0xFF); // 低字节
|
||||
|
||||
// 4. 包标识 (1字节: 高4位版本 + 低4位加密类型)
|
||||
int packetFlag = (PACKET_VERSION & 0xF0) | (encryptType & 0x0F);
|
||||
header.add(packetFlag);
|
||||
|
||||
// 5. 数据长度 (4字节,大端序: 高16位加密后长度 + 低16位原始长度)
|
||||
int combinedLength = (encryptedDataLength << 16) | originalDataLength;
|
||||
header.add((combinedLength >> 24) & 0xFF); // 最高字节
|
||||
header.add((combinedLength >> 16) & 0xFF); // 次高字节
|
||||
header.add((combinedLength >> 8) & 0xFF); // 次低字节
|
||||
header.add(combinedLength & 0xFF); // 最低字节
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// ✨✨✨ 组装包尾 (CRC16校验) ✨✨✨
|
||||
/// [packetData] 完整的数据包(包头+数据块)
|
||||
static List<int> buildPacketTail(List<int> packetData) {
|
||||
int crc16 = calculateCRC16Kermit(packetData);
|
||||
|
||||
// CRC16校验位 (2字节,大端序)
|
||||
List<int> tail = [
|
||||
(crc16 >> 8) & 0xFF, // 高字节
|
||||
crc16 & 0xFF, // 低字节
|
||||
];
|
||||
|
||||
return tail;
|
||||
}
|
||||
|
||||
/// ✨✨✨ 计算CRC16-KERMIT校验 ✨✨✨
|
||||
static int calculateCRC16Kermit(List<int> data) {
|
||||
const int polynomial = 0x1021; // CRC16-KERMIT多项式
|
||||
const int initialValue = 0x0000;
|
||||
|
||||
int crc = initialValue;
|
||||
|
||||
for (int byte in data) {
|
||||
crc ^= (byte << 8);
|
||||
for (int i = 0; i < 8; i++) {
|
||||
if ((crc & 0x8000) != 0) {
|
||||
crc = (crc << 1) ^ polynomial;
|
||||
} else {
|
||||
crc <<= 1;
|
||||
}
|
||||
crc &= 0xFFFF; // 保持16位
|
||||
}
|
||||
}
|
||||
|
||||
// KERMIT CRC需要交换字节序
|
||||
int result = ((crc & 0xFF) << 8) | ((crc >> 8) & 0xFF);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// ✨✨✨ 分包处理 - 将大数据分割成多个包 ✨✨✨
|
||||
/// [originalData] 原始数据
|
||||
/// [encryptType] 加密类型
|
||||
/// 返回多个完整的数据包
|
||||
static List<List<int>> splitIntoPackets({
|
||||
required List<int> originalData,
|
||||
int encryptType = ENCRYPT_TYPE_PLAIN,
|
||||
}) {
|
||||
List<List<int>> packets = [];
|
||||
|
||||
// 计算需要分成多少包
|
||||
int totalPackets = (originalData.length / MAX_PACKET_DATA_SIZE).ceil();
|
||||
|
||||
for (int i = 0; i < totalPackets; i++) {
|
||||
int startIndex = i * MAX_PACKET_DATA_SIZE;
|
||||
int endIndex = (startIndex + MAX_PACKET_DATA_SIZE > originalData.length) ? originalData.length : startIndex + MAX_PACKET_DATA_SIZE;
|
||||
|
||||
// 获取当前包的数据
|
||||
List<int> currentPacketData = originalData.sublist(startIndex, endIndex);
|
||||
int originalDataLength = currentPacketData.length;
|
||||
|
||||
// ✨✨✨ 根据加密类型计算加密后数据长度 ✨✨✨
|
||||
int encryptedDataLength = originalDataLength; // 默认情况下加密后长度与原始长度相同
|
||||
if (encryptType != ENCRYPT_TYPE_PLAIN) {
|
||||
// TODO: 根据具体加密算法计算加密后长度
|
||||
// 例如AES128加密通常会填充到16字节的倍数
|
||||
switch (encryptType) {
|
||||
case ENCRYPT_TYPE_AES128:
|
||||
// AES128加密填充到16字节边界
|
||||
encryptedDataLength = ((originalDataLength + 15) ~/ 16) * 16;
|
||||
break;
|
||||
case ENCRYPT_TYPE_SM4_PRESET:
|
||||
case ENCRYPT_TYPE_SM4_DEVICE:
|
||||
// SM4加密填充到16字节边界
|
||||
encryptedDataLength = ((originalDataLength + 15) ~/ 16) * 16;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取分包序号
|
||||
int packetSequence = getNextPacketSequence();
|
||||
|
||||
// 组装包头
|
||||
List<int> header = buildPacketHeader(
|
||||
packetType: PACKET_TYPE_REQUEST,
|
||||
packetSequence: packetSequence,
|
||||
encryptType: encryptType,
|
||||
encryptedDataLength: encryptedDataLength,
|
||||
originalDataLength: originalDataLength,
|
||||
);
|
||||
|
||||
// 组装完整包 (包头 + 数据)
|
||||
List<int> packetWithHeader = [];
|
||||
packetWithHeader.addAll(header);
|
||||
packetWithHeader.addAll(currentPacketData);
|
||||
|
||||
// 计算并添加包尾
|
||||
List<int> tail = buildPacketTail(packetWithHeader);
|
||||
|
||||
// 完整的数据包
|
||||
List<int> completePacket = [];
|
||||
completePacket.addAll(packetWithHeader);
|
||||
completePacket.addAll(tail);
|
||||
|
||||
packets.add(completePacket);
|
||||
}
|
||||
|
||||
return packets;
|
||||
}
|
||||
|
||||
/// ✨✨✨ 抽象方法 - 子类需要实现具体的数据构建逻辑 ✨✨✨
|
||||
List<int> buildData();
|
||||
|
||||
/// ✨✨✨ 获取当前命令的加密类型 - 子类可以重写此方法 ✨✨✨
|
||||
/// 默认返回明文类型,子类可以根据需要重写
|
||||
int getEncryptType() {
|
||||
return ENCRYPT_TYPE_PLAIN;
|
||||
}
|
||||
|
||||
/// ✨✨✨ 构建完整的命令包 ✨✨✨
|
||||
/// 使用子类定义的加密类型自动组装数据包
|
||||
List<List<int>> build() {
|
||||
// 获取子类实现的数据
|
||||
List<int> commandData = buildData();
|
||||
|
||||
// 使用子类定义的加密类型
|
||||
int encryptType = getEncryptType();
|
||||
|
||||
// 分包处理
|
||||
List<List<int>> packets = splitIntoPackets(
|
||||
originalData: commandData,
|
||||
encryptType: encryptType,
|
||||
);
|
||||
|
||||
return packets;
|
||||
}
|
||||
|
||||
/// ✨✨✨ 解析接收到的数据包 ✨✨✨
|
||||
/// [rawData] 接收到的原始数据包字节
|
||||
/// 返回解析结果,包含包的各个字段信息
|
||||
static ParsedPacket parsePacket(List<int> rawData) {
|
||||
try {
|
||||
// 1. 检查最小长度 (包头4 + 包类型1 + 包序号2 + 包标识1 + 数据长度4 + CRC2 = 14字节)
|
||||
if (rawData.length < 14) {
|
||||
return ParsedPacket(
|
||||
isValid: false,
|
||||
packetType: 0,
|
||||
packetSequence: 0,
|
||||
version: 0,
|
||||
encryptType: 0,
|
||||
encryptedDataLength: 0,
|
||||
originalDataLength: 0,
|
||||
data: [],
|
||||
crc16: 0,
|
||||
errorMessage: '数据包长度不足: ${rawData.length}字节 < 14字节',
|
||||
);
|
||||
}
|
||||
|
||||
int offset = 0;
|
||||
|
||||
// 2. 检查包头
|
||||
List<int> header = rawData.sublist(offset, offset + 4);
|
||||
offset += 4;
|
||||
if (!_isValidHeader(header)) {
|
||||
return ParsedPacket(
|
||||
isValid: false,
|
||||
packetType: 0,
|
||||
packetSequence: 0,
|
||||
version: 0,
|
||||
encryptType: 0,
|
||||
encryptedDataLength: 0,
|
||||
originalDataLength: 0,
|
||||
data: [],
|
||||
crc16: 0,
|
||||
errorMessage: '包头不正确: ${header.map((b) => '0x${b.toRadixString(16).padLeft(2, '0')}').join(' ')}',
|
||||
);
|
||||
}
|
||||
|
||||
// 3. 解析包类型
|
||||
int packetType = rawData[offset++];
|
||||
|
||||
// 4. 解析包序号 (大端序)
|
||||
int packetSequence = (rawData[offset] << 8) | rawData[offset + 1];
|
||||
offset += 2;
|
||||
|
||||
// 5. 解析包标识 (高4位版本 + 低4位加密类型)
|
||||
int packetFlag = rawData[offset++];
|
||||
int version = (packetFlag & 0xF0);
|
||||
int encryptType = (packetFlag & 0x0F);
|
||||
|
||||
// 6. 解析数据长度 (大端序: 高16位加密后长度 + 低16位原始长度)
|
||||
int combinedLength = (rawData[offset] << 24) | (rawData[offset + 1] << 16) | (rawData[offset + 2] << 8) | rawData[offset + 3];
|
||||
offset += 4;
|
||||
int encryptedDataLength = (combinedLength >> 16) & 0xFFFF;
|
||||
int originalDataLength = combinedLength & 0xFFFF;
|
||||
AppLogger.debug('📏 数据长度: 加密后=$encryptedDataLength, 原始=$originalDataLength');
|
||||
|
||||
// 7. 检查数据包完整性
|
||||
// ✨✨✨ 根据加密类型确定实际数据长度 ✨✨✨
|
||||
int actualDataLength = (encryptType == ENCRYPT_TYPE_PLAIN) ? originalDataLength : encryptedDataLength;
|
||||
int expectedTotalLength = 12 + actualDataLength + 2; // 包头到数据长度(12) + 数据块 + CRC(2)
|
||||
if (rawData.length != expectedTotalLength) {
|
||||
return ParsedPacket(
|
||||
isValid: false,
|
||||
packetType: packetType,
|
||||
packetSequence: packetSequence,
|
||||
version: version,
|
||||
encryptType: encryptType,
|
||||
encryptedDataLength: encryptedDataLength,
|
||||
originalDataLength: originalDataLength,
|
||||
data: [],
|
||||
crc16: 0,
|
||||
errorMessage: '数据包长度不匹配: 实际${rawData.length}字节 != 期望${expectedTotalLength}字节',
|
||||
);
|
||||
}
|
||||
|
||||
// 8. 提取数据块
|
||||
// ✨✨✨ 根据加密类型提取相应长度的数据 ✨✨✨
|
||||
List<int> data = rawData.sublist(offset, offset + actualDataLength);
|
||||
offset += actualDataLength;
|
||||
|
||||
// // 9. 提取CRC16校验位 (大端序)
|
||||
int crc16 = (rawData[offset] << 8) | rawData[offset + 1];
|
||||
|
||||
//
|
||||
// // 10. 验证CRC16校验
|
||||
// List<int> dataToCheck = rawData.sublist(0, rawData.length - 2); // 除去CRC的数据
|
||||
// int calculatedCRC = calculateCRC16Kermit(dataToCheck);
|
||||
// bool crcValid = (calculatedCRC == crc16);
|
||||
// AppLogger.debug('🔍 CRC16验证: 计算值=0x${calculatedCRC.toRadixString(16).padLeft(4, '0')}, 接收值=0x${crc16.toRadixString(16).padLeft(4, '0')}, ${crcValid ? '✅通过' : '❌失败'}');
|
||||
//
|
||||
// if (!crcValid) {
|
||||
// return ParsedPacket(
|
||||
// isValid: false,
|
||||
// packetType: packetType, packetSequence: packetSequence, version: version, encryptType: encryptType,
|
||||
// encryptedDataLength: encryptedDataLength, originalDataLength: originalDataLength, data: data,
|
||||
// crc16: crc16, errorMessage: 'CRC16校验失败: 计算值=0x${calculatedCRC.toRadixString(16).padLeft(4, '0')}, 接收值=0x${crc16.toRadixString(16).padLeft(4, '0')}',
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
return ParsedPacket(
|
||||
isValid: true,
|
||||
packetType: packetType,
|
||||
packetSequence: packetSequence,
|
||||
version: version,
|
||||
encryptType: encryptType,
|
||||
encryptedDataLength: encryptedDataLength,
|
||||
originalDataLength: originalDataLength,
|
||||
data: data,
|
||||
crc16: crc16,
|
||||
);
|
||||
} catch (e) {
|
||||
AppLogger.error('❌ 数据包解析异常', error: e);
|
||||
return ParsedPacket(
|
||||
isValid: false,
|
||||
packetType: 0,
|
||||
packetSequence: 0,
|
||||
version: 0,
|
||||
encryptType: 0,
|
||||
encryptedDataLength: 0,
|
||||
originalDataLength: 0,
|
||||
data: [],
|
||||
crc16: 0,
|
||||
errorMessage: '解析异常: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// ✨✨✨ 验证包头是否正确 ✨✨✨
|
||||
static bool _isValidHeader(List<int> header) {
|
||||
if (header.length != 4) return false;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (header[i] != PACKET_HEADER[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取固定长度的数组
|
||||
List<int> getFixedLengthList(List<int> data, int length) {
|
||||
for (int i = 0; i < length; i++) {
|
||||
data.add(0);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
191
lib/ble/command/base/base_ble_response_parser.dart
Normal file
191
lib/ble/command/base/base_ble_response_parser.dart
Normal file
@ -0,0 +1,191 @@
|
||||
import 'package:starwork_flutter/base/app_logger.dart';
|
||||
import 'package:starwork_flutter/ble/command/base/base_ble_command.dart';
|
||||
|
||||
/// ✨✨✨ 蓝牙命令应答解析基类 ✨✨✨
|
||||
abstract class BaseBleResponseParser {
|
||||
|
||||
/// 命令ID - 子类必须定义
|
||||
int get commandId;
|
||||
|
||||
/// 命令名称 - 子类可以重写,用于日志显示
|
||||
String get commandName => '未知命令(0x${commandId.toRadixString(16).padLeft(4, '0')})';
|
||||
|
||||
/// ✨✨✨ 解析命令应答数据的抽象方法 ✨✨✨
|
||||
/// [parsedPacket] 已解析的数据包基本信息
|
||||
/// [rawResponseData] 原始应答数据(仅数据块部分)
|
||||
/// 返回解析后的业务数据对象
|
||||
dynamic parseResponse(ParsedPacket parsedPacket, List<int> rawResponseData);
|
||||
|
||||
/// ✨✨✨ 验证应答数据是否匹配当前命令 ✨✨✨
|
||||
/// [parsedPacket] 已解析的数据包
|
||||
bool isMatchingResponse(ParsedPacket parsedPacket) {
|
||||
// 检查包是否有效
|
||||
if (!parsedPacket.isValid) {
|
||||
AppLogger.warn('⚠️ 数据包无效,不匹配任何命令');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否为应答包
|
||||
if (parsedPacket.packetType != BaseBleCommand.PACKET_TYPE_RESPONSE) {
|
||||
AppLogger.debug('📋 非应答包,类型: 0x${parsedPacket.packetType.toRadixString(16).padLeft(2, '0')}');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查数据长度是否足够包含命令ID
|
||||
if (parsedPacket.data.length < 2) {
|
||||
AppLogger.warn('⚠️ 应答数据长度不足,无法包含命令ID: ${parsedPacket.data.length}字节');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 提取命令ID (大端序)
|
||||
int responseCommandId = (parsedPacket.data[0] << 8) | parsedPacket.data[1];
|
||||
|
||||
// 检查命令ID是否匹配
|
||||
bool isMatch = (responseCommandId == commandId);
|
||||
AppLogger.debug('🔍 命令ID匹配检查: 期望=0x${commandId.toRadixString(16).padLeft(4, '0')}, ' +
|
||||
'实际=0x${responseCommandId.toRadixString(16).padLeft(4, '0')}, 匹配=${isMatch ? '✅' : '❌'}');
|
||||
|
||||
return isMatch;
|
||||
}
|
||||
|
||||
/// ✨✨✨ 完整的应答处理流程 ✨✨✨
|
||||
/// [rawPacketData] 接收到的完整数据包
|
||||
/// 返回解析后的业务数据,如果不匹配或解析失败则返回null
|
||||
dynamic handleResponse(List<int> rawPacketData) {
|
||||
AppLogger.highlight('✨✨✨ 🔄 开始处理${commandName}应答 ✨✨✨');
|
||||
|
||||
try {
|
||||
// 1. 解析数据包基本信息
|
||||
ParsedPacket parsedPacket = BaseBleCommand.parsePacket(rawPacketData);
|
||||
|
||||
// 2. 检查是否匹配当前命令
|
||||
if (!isMatchingResponse(parsedPacket)) {
|
||||
AppLogger.debug('📝 应答不匹配${commandName},跳过处理');
|
||||
return null;
|
||||
}
|
||||
|
||||
AppLogger.info('🎯 ${commandName}应答匹配成功,开始解析业务数据');
|
||||
|
||||
// 3. 调用子类的具体解析逻辑
|
||||
dynamic result = parseResponse(parsedPacket, parsedPacket.data);
|
||||
|
||||
AppLogger.highlight('✨✨✨ ✅ ${commandName}应答处理完成 ✨✨✨');
|
||||
return result;
|
||||
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error('❌ ${commandName}应答处理异常', error: e, stackTrace: stackTrace);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// ✨✨✨ 辅助方法 - 从数据中提取整数(大端序) ✨✨✨
|
||||
/// [data] 数据字节数组
|
||||
/// [offset] 起始偏移
|
||||
/// [length] 字节长度(1, 2, 4, 8)
|
||||
static int extractInt(List<int> data, int offset, int length) {
|
||||
if (offset + length > data.length) {
|
||||
throw ArgumentError('数据长度不足: offset=$offset, length=$length, dataLength=${data.length}');
|
||||
}
|
||||
|
||||
int result = 0;
|
||||
for (int i = 0; i < length; i++) {
|
||||
result = (result << 8) | data[offset + i];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// ✨✨✨ 辅助方法 - 从数据中提取字节数组 ✨✨✨
|
||||
/// [data] 源数据
|
||||
/// [offset] 起始偏移
|
||||
/// [length] 提取长度
|
||||
static List<int> extractBytes(List<int> data, int offset, int length) {
|
||||
if (offset + length > data.length) {
|
||||
throw ArgumentError('数据长度不足: offset=$offset, length=$length, dataLength=${data.length}');
|
||||
}
|
||||
return data.sublist(offset, offset + length);
|
||||
}
|
||||
|
||||
/// ✨✨✨ 辅助方法 - 从数据中提取字符串 ✨✨✨
|
||||
/// [data] 数据字节数组
|
||||
/// [offset] 起始偏移
|
||||
/// [length] 字符串字节长度
|
||||
/// [removeNullTerminator] 是否移除空终止符
|
||||
static String extractString(List<int> data, int offset, int length, {bool removeNullTerminator = true}) {
|
||||
List<int> stringBytes = extractBytes(data, offset, length);
|
||||
|
||||
if (removeNullTerminator) {
|
||||
// 找到第一个0字节并截断
|
||||
int nullIndex = stringBytes.indexOf(0);
|
||||
if (nullIndex >= 0) {
|
||||
stringBytes = stringBytes.sublist(0, nullIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return String.fromCharCodes(stringBytes);
|
||||
}
|
||||
|
||||
/// ✨✨✨ 辅助方法 - 将字节数组转换为十六进制字符串 ✨✨✨
|
||||
/// [data] 字节数组
|
||||
/// [separator] 分隔符,默认为空格
|
||||
static String bytesToHex(List<int> data, {String separator = ' '}) {
|
||||
return data.map((b) => '0x${b.toRadixString(16).padLeft(2, '0')}').join(separator);
|
||||
}
|
||||
|
||||
/// ✨✨✨ 辅助方法 - 将十六进制字符串解析为字节数组 ✨✨✨
|
||||
/// 支持格式:
|
||||
/// - "0x90 0x30 0x41 0x31" (带 0x 和空格)
|
||||
/// - "90 30 41 31" (无 0x,用空格分隔)
|
||||
/// - "90:30:41:31" (用冒号分隔)
|
||||
/// - "0x90304131" (连续无分隔)
|
||||
/// [hex] 十六进制字符串
|
||||
/// [separator] 分隔符(可选),如果为 null 则自动检测常见分隔符
|
||||
/// 返回 [List<int>] 字节数组,解析失败返回空列表
|
||||
static List<int> hexToBytes(String hex, {String? separator}) {
|
||||
if (hex.isEmpty) return [];
|
||||
|
||||
// 如果未指定分隔符,使用正则自动分割(支持空格、冒号、短横、逗号等)
|
||||
List<String> parts;
|
||||
if (separator != null) {
|
||||
parts = hex.split(separator);
|
||||
} else {
|
||||
// 移除所有空白和常见分隔符,然后每两个字符一组(适用于连续字符串)
|
||||
// 或按分隔符切分
|
||||
final hasSeparators = RegExp(r'[\s,:;-]').hasMatch(hex);
|
||||
if (hasSeparators) {
|
||||
parts = hex.split(RegExp(r'[\s,:;-]+'));
|
||||
} else {
|
||||
// 无分隔符:每两个字符一组
|
||||
hex = hex.replaceAll('0x', '').replaceAll(' ', '');
|
||||
if (hex.length % 2 != 0) hex = '0$hex'; // 补齐偶数
|
||||
parts = [];
|
||||
for (int i = 0; i < hex.length; i += 2) {
|
||||
parts.add(hex.substring(i, i + 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final result = <int>[];
|
||||
final RegExp hexPattern = RegExp(r'^0x([0-9a-fA-F]{2})$|^([0-9a-fA-F]{2})$');
|
||||
|
||||
for (String part in parts) {
|
||||
if (part.isEmpty) continue;
|
||||
|
||||
final match = hexPattern.firstMatch(part.trim());
|
||||
if (match == null) {
|
||||
print('Invalid hex part: $part');
|
||||
continue;
|
||||
}
|
||||
|
||||
// 提取十六进制数字(兼容 0xXX 和 XX)
|
||||
final hexValue = match.group(1) ?? match.group(2);
|
||||
try {
|
||||
final byte = int.parse(hexValue!, radix: 16);
|
||||
result.add(byte);
|
||||
} catch (e) {
|
||||
print('Failed to parse hex: $hexValue, error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
224
lib/ble/command/ble_command_manager.dart
Normal file
224
lib/ble/command/ble_command_manager.dart
Normal file
@ -0,0 +1,224 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:starwork_flutter/base/app_logger.dart';
|
||||
import 'package:starwork_flutter/ble/ble_service.dart';
|
||||
import 'package:starwork_flutter/ble/command/base/base_ble_command.dart';
|
||||
import 'package:starwork_flutter/ble/command/base/base_ble_response_parser.dart';
|
||||
import 'package:starwork_flutter/ble/command/response/ble_cmd_get_private_key_parser.dart';
|
||||
import 'package:starwork_flutter/ble/command/response/ble_cmd_get_public_key_parser.dart';
|
||||
import 'package:starwork_flutter/common/sm4_encipher/sm4.dart';
|
||||
|
||||
/// ✨✨✨ 蓝牙命令管理器 - 统一管理所有命令解析器 ✨✨✨
|
||||
class BleCommandManager {
|
||||
// 私有构造函数
|
||||
BleCommandManager._() {
|
||||
// ✅ 这里就是单例初始化的地方
|
||||
// 只会执行一次(第一次获取实例时)
|
||||
_initialize();
|
||||
}
|
||||
|
||||
// 单例实例
|
||||
static final BleCommandManager _instance = BleCommandManager._();
|
||||
|
||||
// 工厂构造函数,提供全局访问点
|
||||
factory BleCommandManager() => _instance;
|
||||
|
||||
/// 所有已注册的命令解析器
|
||||
final Map<int, BaseBleResponseParser> _parsers = {};
|
||||
|
||||
/// ✨✨✨ 初始化命令管理器 ✨✨✨
|
||||
void _initialize() {
|
||||
AppLogger.highlight('✨✨✨ 🚀 初始化蓝牙命令管理器 ✨✨✨');
|
||||
|
||||
// 注册所有命令解析器
|
||||
_registerParsers();
|
||||
|
||||
AppLogger.info('📋 已注册命令解析器数量: ${_parsers.length}');
|
||||
_parsers.forEach((commandId, parser) {
|
||||
AppLogger.debug(' • 0x${commandId.toRadixString(16).padLeft(4, '0')}: ${parser.commandName}');
|
||||
});
|
||||
|
||||
AppLogger.highlight('✨✨✨ ✅ 蓝牙命令管理器初始化完成 ✨✨✨');
|
||||
}
|
||||
|
||||
/// ✨✨✨ 注册所有命令解析器 ✨✨✨
|
||||
void _registerParsers() {
|
||||
// 注册获取公钥命令解析器
|
||||
registerParser(BleCmdGetPublicKeyParser());
|
||||
registerParser(BleCmdGetPrivateKeyParser());
|
||||
|
||||
// TODO: 在这里注册其他命令解析器
|
||||
// registerParser(BleCmdSetPasswordParser());
|
||||
// registerParser(BleCmdUnlockParser());
|
||||
// 等等...
|
||||
}
|
||||
|
||||
/// ✨✨✨ 注册命令解析器 ✨✨✨
|
||||
void registerParser(BaseBleResponseParser parser) {
|
||||
_parsers[parser.commandId] = parser;
|
||||
AppLogger.debug('📝 注册命令解析器: 0x${parser.commandId.toRadixString(16).padLeft(4, '0')} - ${parser.commandName}');
|
||||
}
|
||||
|
||||
/// ✨✨✨ 移除命令解析器 ✨✨✨
|
||||
void unregisterParser(int commandId) {
|
||||
BaseBleResponseParser? removed = _parsers.remove(commandId);
|
||||
if (removed != null) {
|
||||
AppLogger.debug('🗑️ 移除命令解析器: 0x${commandId.toRadixString(16).padLeft(4, '0')} - ${removed.commandName}');
|
||||
}
|
||||
}
|
||||
|
||||
/// ✨✨✨ 处理接收到的应答数据包 ✨✨✨
|
||||
/// [rawPacketData] 完整的原始数据包
|
||||
/// 返回解析后的业务数据,如果没有匹配的解析器则返回null
|
||||
dynamic handleResponse(List<int> rawPacketData) {
|
||||
AppLogger.debug('📊 收到蓝牙数据 (${rawPacketData.length}字节): ${rawPacketData.map((b) => '0x${b.toRadixString(16).padLeft(2, '0')}').join(' ')}');
|
||||
|
||||
try {
|
||||
// ✨✨✨ 首先解析数据包以获取命令ID ✨✨✨
|
||||
var parsedPacket = BaseBleCommand.parsePacket(rawPacketData);
|
||||
|
||||
// 检查数据包是否有效
|
||||
if (!parsedPacket.isValid) {
|
||||
AppLogger.warn('⚠️ 数据包无效: ${parsedPacket.errorMessage}');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查数据长度是否足够包含命令ID
|
||||
if (parsedPacket.data.length < 2) {
|
||||
AppLogger.warn('⚠️ 应答数据长度不足,无法包含命令ID: ${parsedPacket.data.length}字节');
|
||||
return null;
|
||||
}
|
||||
|
||||
// ✨✨✨ 根据加密类型进行解密处理 ✨✨✨
|
||||
List<int> processedData = _decryptDataIfNeeded(parsedPacket);
|
||||
|
||||
// ✨✨✨ 提取命令ID (大端序) ✨✨✨
|
||||
int commandId = (processedData[0] << 8) | processedData[1];
|
||||
AppLogger.debug('🔍 提取命令ID: 0x${commandId.toRadixString(16).padLeft(4, '0')}');
|
||||
|
||||
// ✨✨✨ 直接通过命令ID查找对应的解析器 ✨✨✨
|
||||
BaseBleResponseParser? parser = _parsers[commandId];
|
||||
if (parser != null) {
|
||||
AppLogger.debug('🎯 找到匹配的解析器: ${parser.commandName}');
|
||||
|
||||
// 创建一个新的ParsedPacket对象,使用解密后的数据
|
||||
ParsedPacket decryptedPacket = ParsedPacket(
|
||||
isValid: parsedPacket.isValid,
|
||||
packetType: parsedPacket.packetType,
|
||||
packetSequence: parsedPacket.packetSequence,
|
||||
version: parsedPacket.version,
|
||||
encryptType: parsedPacket.encryptType,
|
||||
encryptedDataLength: parsedPacket.encryptedDataLength,
|
||||
originalDataLength: parsedPacket.originalDataLength,
|
||||
data: processedData,
|
||||
crc16: parsedPacket.crc16,
|
||||
errorMessage: parsedPacket.errorMessage,
|
||||
);
|
||||
|
||||
// 传递解密后的数据给解析器
|
||||
dynamic result = parser.parseResponse(decryptedPacket, processedData);
|
||||
return result;
|
||||
} else {
|
||||
// 没有找到匹配的解析器
|
||||
AppLogger.warn('⚠️ 未找到命令ID 0x${commandId.toRadixString(16).padLeft(4, '0')} 对应的解析器');
|
||||
_logUnknownPacket(rawPacketData);
|
||||
return null;
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error('❌ 处理蓝牙应答异常', error: e, stackTrace: stackTrace);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// ✨✨✨ 根据加密类型解密数据 ✨✨✨
|
||||
/// [parsedPacket] 已解析的数据包
|
||||
/// 返回解密后的数据或原始数据(如果不需要解密)
|
||||
List<int> _decryptDataIfNeeded(ParsedPacket parsedPacket) {
|
||||
// 如果是明文,直接返回原始数据
|
||||
if (parsedPacket.encryptType == BaseBleCommand.ENCRYPT_TYPE_PLAIN) {
|
||||
AppLogger.debug('🔓 数据未加密,直接使用原始数据');
|
||||
return parsedPacket.data;
|
||||
}
|
||||
|
||||
switch (parsedPacket.encryptType) {
|
||||
case BaseBleCommand.ENCRYPT_TYPE_AES128:
|
||||
return _decryptAES128(parsedPacket.data);
|
||||
|
||||
case BaseBleCommand.ENCRYPT_TYPE_SM4_PRESET:
|
||||
var decrypt = SM4.decrypt(
|
||||
parsedPacket.data,
|
||||
key: utf8.encode('TMH_190068d76ae8'),
|
||||
mode: SM4CryptoMode.ECB,
|
||||
);
|
||||
return decrypt;
|
||||
case BaseBleCommand.ENCRYPT_TYPE_SM4_DEVICE:
|
||||
return _decryptSM4(parsedPacket.data);
|
||||
default:
|
||||
AppLogger.warn('⚠️ 未知的加密类型: ${parsedPacket.encryptType},使用原始数据');
|
||||
return parsedPacket.data;
|
||||
}
|
||||
}
|
||||
|
||||
/// ✨✨✨ AES128解密 ✨✨✨
|
||||
/// [encryptedData] 加密数据
|
||||
/// 返回解密后的数据
|
||||
List<int> _decryptAES128(List<int> encryptedData) {
|
||||
// TODO: 实现AES128解密逻辑
|
||||
|
||||
return encryptedData;
|
||||
}
|
||||
|
||||
/// ✨✨✨ SM4解密 ✨✨✨
|
||||
/// [encryptedData] 加密数据
|
||||
/// 返回解密后的数据
|
||||
List<int> _decryptSM4(List<int> encryptedData) {
|
||||
return encryptedData;
|
||||
}
|
||||
|
||||
/// ✨✨✨ 根据命令ID获取解析器 ✨✨✨
|
||||
BaseBleResponseParser? getParser(int commandId) {
|
||||
return _parsers[commandId];
|
||||
}
|
||||
|
||||
/// ✨✨✨ 获取所有已注册的命令ID ✨✨✨
|
||||
List<int> getRegisteredCommandIds() {
|
||||
return _parsers.keys.toList();
|
||||
}
|
||||
|
||||
/// ✨✨✨ 获取所有已注册的解析器信息 ✨✨✨
|
||||
Map<int, String> getRegisteredParsersInfo() {
|
||||
return _parsers.map((commandId, parser) => MapEntry(commandId, parser.commandName));
|
||||
}
|
||||
|
||||
/// ✨✨✨ 记录未知数据包信息 ✨✨✨
|
||||
void _logUnknownPacket(List<int> rawPacketData) {
|
||||
try {
|
||||
// 尝试基本解析以获取更多信息
|
||||
var parsedPacket = BaseBleCommand.parsePacket(rawPacketData);
|
||||
|
||||
if (parsedPacket.isValid && parsedPacket.data.length >= 2) {
|
||||
int commandId = (parsedPacket.data[0] << 8) | parsedPacket.data[1];
|
||||
AppLogger.warn('🔍 未知命令应答: 命令ID=0x${commandId.toRadixString(16).padLeft(4, '0')}, ' +
|
||||
'包类型=0x${parsedPacket.packetType.toRadixString(16).padLeft(2, '0')}, ' +
|
||||
'数据长度=${parsedPacket.data.length}字节');
|
||||
} else {
|
||||
AppLogger.warn('🔍 无效或无法识别的数据包');
|
||||
}
|
||||
} catch (e) {
|
||||
AppLogger.warn('🔍 无法解析的数据包: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// ✨✨✨ 清除所有解析器 ✨✨✨
|
||||
void clear() {
|
||||
AppLogger.info('🧹 清除所有命令解析器');
|
||||
_parsers.clear();
|
||||
}
|
||||
|
||||
/// ✨✨✨ 重新初始化 ✨✨✨
|
||||
void reinitialize() {
|
||||
AppLogger.info('🔄 重新初始化命令管理器');
|
||||
clear();
|
||||
_initialize();
|
||||
}
|
||||
}
|
||||
179
lib/ble/command/request/ble_cmd_get_private_key.dart
Normal file
179
lib/ble/command/request/ble_cmd_get_private_key.dart
Normal file
@ -0,0 +1,179 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:starwork_flutter/base/app_logger.dart';
|
||||
import 'package:starwork_flutter/ble/command/base/base_ble_command.dart';
|
||||
import 'package:starwork_flutter/ble/command/response/ble_cmd_get_private_key_parser.dart';
|
||||
import 'package:starwork_flutter/common/sm4_encipher/sm4.dart';
|
||||
|
||||
/// ✨✨✨ 获取私钥命令类 - 继承BaseBleCommand ✨✨✨
|
||||
class BleCmdGetPrivateKey extends BaseBleCommand<GetPrivateKeyResponse> {
|
||||
final String lockId;
|
||||
final String keyId;
|
||||
final String authUserID;
|
||||
final List<int> publicKey;
|
||||
final int nowTime;
|
||||
final int _encryptType; // 私有字段存储加密类型
|
||||
|
||||
/// 指令 ID: 0x3091
|
||||
static const int cmdId = 0x3091;
|
||||
|
||||
/// 构造函数
|
||||
BleCmdGetPrivateKey({
|
||||
int encryptType = BaseBleCommand.ENCRYPT_TYPE_SM4_PRESET,
|
||||
required this.lockId,
|
||||
required this.keyId,
|
||||
required this.authUserID,
|
||||
required this.nowTime,
|
||||
required this.publicKey,
|
||||
}) : _encryptType = encryptType {
|
||||
if (lockId.isEmpty) {
|
||||
throw ArgumentError('LockID cannot be empty');
|
||||
}
|
||||
if (lockId.length > 40) {
|
||||
throw ArgumentError('LockID must be at most 40 characters long, got ${lockId.length}');
|
||||
}
|
||||
AppLogger.debug('🔑 BleCmdGetPrivateKey 创建: LockID="$lockId", 加密类型=$_encryptType');
|
||||
}
|
||||
|
||||
/// ✨✨✨ 重写基类方法 - 返回当前命令的加密类型 ✨✨✨
|
||||
@override
|
||||
int getEncryptType() {
|
||||
return _encryptType;
|
||||
}
|
||||
|
||||
/// ✨✨✨ 实现BaseBleCommand的抽象方法 - 构建命令数据 ✨✨✨
|
||||
@override
|
||||
List<int> buildData() {
|
||||
// final List<int> buffer = [];
|
||||
//
|
||||
// // 1. 写入 CmdID (2 字节,大端序)
|
||||
// buffer.add((cmdId >> 8) & 0xFF); // 高字节
|
||||
// buffer.add(cmdId & 0xFF); // 低字节
|
||||
//
|
||||
// // 2. 处理 LockID:先转换实际字符,再检查是否需要填充到40字节
|
||||
// List<int> lockIdBytes = List.from(lockId.codeUnits); // 创建可修改的副本
|
||||
// List<int> keyIdBytes = List.from(keyId.codeUnits); // 创建可修改的副本
|
||||
// List<int> authUserIdBytes = List.from(authUserID.codeUnits); // 创建可修改的副本
|
||||
//
|
||||
// // 检查是否需要填充到40字节
|
||||
// if (lockIdBytes.length < 40) {
|
||||
// // 用0填充到40字节
|
||||
// int paddingNeeded = 40 - lockIdBytes.length;
|
||||
// lockIdBytes.addAll(List.filled(paddingNeeded, 0));
|
||||
// }
|
||||
//
|
||||
// // 检查是否需要填充到40字节
|
||||
// if (keyIdBytes.length < 40) {
|
||||
// // 用0填充到40字节
|
||||
// int paddingNeeded = 40 - keyIdBytes.length;
|
||||
// keyIdBytes.addAll(List.filled(paddingNeeded, 0));
|
||||
// }
|
||||
//
|
||||
// // 检查是否需要填充到40字节
|
||||
// if (authUserIdBytes.length < 20) {
|
||||
// // 用0填充到40字节
|
||||
// int paddingNeeded = 20 - authUserIdBytes.length;
|
||||
// authUserIdBytes.addAll(List.filled(paddingNeeded, 0));
|
||||
// }
|
||||
//
|
||||
// buffer.addAll(lockIdBytes);
|
||||
// buffer.addAll(keyIdBytes);
|
||||
// buffer.addAll(authUserIdBytes);
|
||||
// buffer.add((nowTime & 0xFF000000) >> 24);
|
||||
// buffer.add((nowTime & 0xFF0000) >> 16);
|
||||
// buffer.add((nowTime & 0xFF00) >> 8);
|
||||
// buffer.add((nowTime & 0xFF));
|
||||
//
|
||||
// List<int> countAuthCode = [];
|
||||
//
|
||||
// countAuthCode.addAll(keyIdBytes);
|
||||
// countAuthCode.addAll(authUserIdBytes);
|
||||
// countAuthCode.add((nowTime & 0xFF000000) >> 24);
|
||||
// countAuthCode.add((nowTime & 0xFF0000) >> 16);
|
||||
// countAuthCode.add((nowTime & 0xFF00) >> 8);
|
||||
// countAuthCode.add((nowTime & 0xFF));
|
||||
// countAuthCode.addAll(publicKey);
|
||||
//
|
||||
// var authCode = md5.convert(countAuthCode);
|
||||
//
|
||||
// buffer.add(authCode.bytes.length);
|
||||
// buffer.addAll(authCode.bytes);
|
||||
// var list = SM4.encrypt(buffer, key: utf8.encode(lockId), mode: SM4CryptoMode.ECB);
|
||||
// return list;
|
||||
//
|
||||
//
|
||||
//
|
||||
|
||||
List<int> data = <int>[];
|
||||
List<int> ebcData = <int>[];
|
||||
|
||||
// 指令类型
|
||||
final int type = cmdId;
|
||||
final double typeDouble = type / 256;
|
||||
final int type1 = typeDouble.toInt();
|
||||
final int type2 = type % 256;
|
||||
data.add(type1);
|
||||
data.add(type2);
|
||||
|
||||
// 锁id
|
||||
final int lockIDLength = utf8.encode(lockId!).length;
|
||||
data.addAll(utf8.encode(lockId));
|
||||
data = getFixedLengthList(data, 40 - lockIDLength);
|
||||
|
||||
//KeyID 40
|
||||
final int keyIDLength = utf8.encode(keyId!).length;
|
||||
data.addAll(utf8.encode(keyId));
|
||||
data = getFixedLengthList(data, 40 - keyIDLength);
|
||||
|
||||
//authUserID 40
|
||||
final int authUserIDLength = utf8.encode(authUserID!).length;
|
||||
data.addAll(utf8.encode(authUserID));
|
||||
data = getFixedLengthList(data, 20 - authUserIDLength);
|
||||
|
||||
//NowTime 4
|
||||
// DateTime now = DateTime.now();
|
||||
// int timestamp = now.millisecondsSinceEpoch;
|
||||
// var d1 = 0x11223344;
|
||||
data.add((nowTime & 0xff000000) >> 24);
|
||||
data.add((nowTime & 0xff0000) >> 16);
|
||||
data.add((nowTime & 0xff00) >> 8);
|
||||
data.add(nowTime & 0xff);
|
||||
|
||||
final List<int> authCodeData = <int>[];
|
||||
|
||||
//authUserID
|
||||
authCodeData.addAll(utf8.encode(authUserID!));
|
||||
|
||||
//KeyID
|
||||
authCodeData.addAll(utf8.encode(keyId!));
|
||||
|
||||
//NowTime 4
|
||||
// DateTime now = DateTime.now();
|
||||
// int timestamp = now.millisecondsSinceEpoch;
|
||||
// var d1 = 0x11223344;
|
||||
authCodeData.add((nowTime & 0xff000000) >> 24);
|
||||
authCodeData.add((nowTime & 0xff0000) >> 16);
|
||||
authCodeData.add((nowTime & 0xff00) >> 8);
|
||||
authCodeData.add(nowTime & 0xff);
|
||||
|
||||
authCodeData.addAll(publicKey);
|
||||
|
||||
// 把KeyID、authUserID、时间戳、公钥通过md5加密之后就是authCode
|
||||
var authCode = md5.convert(authCodeData);
|
||||
|
||||
data.add(authCode.bytes.length);
|
||||
data.addAll(authCode.bytes);
|
||||
|
||||
if ((data.length % 16) != 0) {
|
||||
final int add = 16 - data.length % 16;
|
||||
for (int i = 0; i < add; i++) {
|
||||
data.add(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 拿到数据之后通过LockId进行SM4 ECB加密 key:544d485f633335373034383064613864
|
||||
ebcData = SM4.encrypt(data, key: utf8.encode(lockId), mode: SM4CryptoMode.ECB);
|
||||
return ebcData;
|
||||
}
|
||||
}
|
||||
59
lib/ble/command/request/ble_cmd_get_public_key.dart
Normal file
59
lib/ble/command/request/ble_cmd_get_public_key.dart
Normal file
@ -0,0 +1,59 @@
|
||||
import 'package:starwork_flutter/base/app_logger.dart';
|
||||
import 'package:starwork_flutter/ble/command/base/base_ble_command.dart';
|
||||
import 'package:starwork_flutter/ble/command/response/ble_cmd_get_public_key_parser.dart';
|
||||
|
||||
/// ✨✨✨ 获取公钥命令类 - 继承BaseBleCommand ✨✨✨
|
||||
class BleCmdGetPublicKey extends BaseBleCommand<GetPublicKeyResponse> {
|
||||
final String lockId;
|
||||
final int _encryptType; // 私有字段存储加密类型
|
||||
|
||||
/// 指令 ID: 0x3090
|
||||
static const int cmdId = 0x3090;
|
||||
|
||||
/// 构造函数
|
||||
/// [lockId] 锁设备ID
|
||||
/// [encryptType] 加密类型,默认为明文
|
||||
BleCmdGetPublicKey({
|
||||
int encryptType = BaseBleCommand.ENCRYPT_TYPE_PLAIN,
|
||||
required this.lockId,
|
||||
}) : _encryptType = encryptType {
|
||||
if (lockId.isEmpty) {
|
||||
throw ArgumentError('LockID cannot be empty');
|
||||
}
|
||||
if (lockId.length > 40) {
|
||||
throw ArgumentError('LockID must be at most 40 characters long, got ${lockId.length}');
|
||||
}
|
||||
AppLogger.debug('🔑 BleCmdGetPublicKey 创建: LockID="$lockId", 加密类型=$_encryptType');
|
||||
}
|
||||
|
||||
/// ✨✨✨ 重写基类方法 - 返回当前命令的加密类型 ✨✨✨
|
||||
@override
|
||||
int getEncryptType() {
|
||||
return _encryptType;
|
||||
}
|
||||
|
||||
/// ✨✨✨ 实现BaseBleCommand的抽象方法 - 构建命令数据 ✨✨✨
|
||||
@override
|
||||
List<int> buildData() {
|
||||
final List<int> buffer = [];
|
||||
|
||||
// 1. 写入 CmdID (2 字节,大端序)
|
||||
buffer.add((cmdId >> 8) & 0xFF); // 高字节
|
||||
buffer.add(cmdId & 0xFF); // 低字节
|
||||
|
||||
// 2. 处理 LockID:先转换实际字符,再检查是否需要填充到40字节
|
||||
List<int> lockIdBytes = List.from(lockId.codeUnits); // 创建可修改的副本
|
||||
|
||||
// 检查是否需要填充到40字节
|
||||
if (lockIdBytes.length < 40) {
|
||||
// 用0填充到40字节
|
||||
int paddingNeeded = 40 - lockIdBytes.length;
|
||||
lockIdBytes.addAll(List.filled(paddingNeeded, 0));
|
||||
}
|
||||
|
||||
buffer.addAll(lockIdBytes);
|
||||
|
||||
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
150
lib/ble/command/response/ble_cmd_get_private_key_parser.dart
Normal file
150
lib/ble/command/response/ble_cmd_get_private_key_parser.dart
Normal file
@ -0,0 +1,150 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:starwork_flutter/base/app_logger.dart';
|
||||
import 'package:starwork_flutter/ble/command/base/base_ble_command.dart';
|
||||
import 'package:starwork_flutter/ble/command/base/base_ble_response_parser.dart';
|
||||
import 'package:starwork_flutter/ble/command/request/ble_cmd_get_private_key.dart';
|
||||
import 'package:starwork_flutter/ble/command/request/ble_cmd_get_public_key.dart';
|
||||
|
||||
/// ✨✨✨ 获取私钥命令应答数据结构 ✨✨✨
|
||||
class GetPrivateKeyResponse {
|
||||
final int commandId;
|
||||
final int statusCode;
|
||||
final List<int> commKey;
|
||||
final String commKeyHex;
|
||||
final List<int> signKey;
|
||||
final String signKeyHex;
|
||||
final int randPassTick;
|
||||
|
||||
GetPrivateKeyResponse({
|
||||
required this.commandId,
|
||||
required this.statusCode,
|
||||
required this.commKey,
|
||||
required this.commKeyHex,
|
||||
required this.signKey,
|
||||
required this.signKeyHex,
|
||||
required this.randPassTick,
|
||||
});
|
||||
|
||||
/// 是否成功
|
||||
bool get isSuccess => statusCode == 0x00;
|
||||
|
||||
/// 状态描述
|
||||
String get statusDescription {
|
||||
switch (statusCode) {
|
||||
case 0x00:
|
||||
return '成功';
|
||||
case 0x01:
|
||||
return '失败';
|
||||
case 0x02:
|
||||
return '参数错误';
|
||||
case 0x03:
|
||||
return '设备忙';
|
||||
case 0x06:
|
||||
return '需要token';
|
||||
default:
|
||||
return '未知状态(0x${statusCode.toRadixString(16).padLeft(2, '0')})';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'GetPrivateKeyResponse{commandId: $commandId, statusCode: $statusCode, commKey: $commKey, commKeyHex: $commKeyHex, signKey: $signKey, signKeyHex: $signKeyHex, randPassTick: $randPassTick}';
|
||||
}
|
||||
}
|
||||
|
||||
/// ✨✨✨ 获取私钥命令应答解析器 ✨✨✨
|
||||
class BleCmdGetPrivateKeyParser extends BaseBleResponseParser {
|
||||
@override
|
||||
int get commandId => BleCmdGetPrivateKey.cmdId;
|
||||
|
||||
@override
|
||||
String get commandName => '获取私钥命令';
|
||||
|
||||
/// ✨✨✨ 解析获取私钥命令的应答数据 ✨✨✨
|
||||
@override
|
||||
GetPrivateKeyResponse parseResponse(ParsedPacket parsedPacket, List<int> rawResponseData) {
|
||||
try {
|
||||
// 数据包格式分析:
|
||||
// 字节 0-1: 命令ID (0x3090, 大端序)
|
||||
// 字节 2: 状态码 (0x00=成功, 其他=失败)
|
||||
// 字节 3-N: 私钥数据 (长度可变)
|
||||
|
||||
if (rawResponseData.length < 3) {
|
||||
throw ArgumentError('应答数据长度不足: ${rawResponseData.length}字节 < 3字节');
|
||||
}
|
||||
|
||||
int offset = 0;
|
||||
|
||||
// 1. 提取命令ID (已在isMatchingResponse中验证过,这里记录日志)
|
||||
int responseCommandId = BaseBleResponseParser.extractInt(rawResponseData, offset, 2);
|
||||
offset += 2;
|
||||
|
||||
// 2. 提取状态码
|
||||
int statusCode = rawResponseData[offset++];
|
||||
|
||||
if (statusCode == 0) {
|
||||
List<int> commKey = [];
|
||||
if (offset < rawResponseData.length) {
|
||||
commKey = BaseBleResponseParser.extractBytes(rawResponseData, offset, 16);
|
||||
}
|
||||
offset += 16;
|
||||
String commKeyHex = BaseBleResponseParser.bytesToHex(commKey, separator: '');
|
||||
|
||||
List<int> signKey = [];
|
||||
if (offset < rawResponseData.length) {
|
||||
signKey = BaseBleResponseParser.extractBytes(rawResponseData, offset, 16);
|
||||
}
|
||||
offset += 16;
|
||||
String signKeyHex = BaseBleResponseParser.bytesToHex(signKey, separator: '');
|
||||
|
||||
List<int> randPassTick = [];
|
||||
if (offset < rawResponseData.length) {
|
||||
randPassTick = BaseBleResponseParser.extractBytes(rawResponseData, offset, rawResponseData.length - offset);
|
||||
}
|
||||
|
||||
final buffer = Uint8List.fromList(randPassTick).buffer;
|
||||
final data = ByteData.view(buffer);
|
||||
|
||||
// 4. 创建应答对象
|
||||
GetPrivateKeyResponse response = GetPrivateKeyResponse(
|
||||
commandId: responseCommandId,
|
||||
statusCode: statusCode,
|
||||
commKey: commKey,
|
||||
commKeyHex: commKeyHex,
|
||||
signKey: signKey,
|
||||
signKeyHex: signKeyHex,
|
||||
randPassTick: data.getInt64(0, Endian.little),
|
||||
);
|
||||
|
||||
if (response.isSuccess) {
|
||||
AppLogger.highlight('✨✨✨ ✅ 获取私钥成功,私钥长度: ${commKey.length}字节 ✨✨✨');
|
||||
} else {
|
||||
AppLogger.warn('⚠️ 获取私钥失败: ${response.statusDescription}');
|
||||
}
|
||||
|
||||
return response;
|
||||
} else {
|
||||
AppLogger.warn('⚠️ 获取私钥失败: ${statusCode}');
|
||||
return GetPrivateKeyResponse(
|
||||
commandId: responseCommandId,
|
||||
statusCode: statusCode,
|
||||
commKey: [],
|
||||
commKeyHex: '',
|
||||
signKey: [],
|
||||
signKeyHex: '',
|
||||
randPassTick: -1,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
AppLogger.error('❌ 解析获取私钥应答数据异常', error: e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// ✨✨✨ 静态便捷方法 - 直接解析原始数据包 ✨✨✨
|
||||
static GetPrivateKeyResponse? parseRawPacket(List<int> rawPacketData) {
|
||||
BleCmdGetPrivateKeyParser parser = BleCmdGetPrivateKeyParser();
|
||||
return parser.handleResponse(rawPacketData) as GetPrivateKeyResponse?;
|
||||
}
|
||||
}
|
||||
131
lib/ble/command/response/ble_cmd_get_public_key_parser.dart
Normal file
131
lib/ble/command/response/ble_cmd_get_public_key_parser.dart
Normal file
@ -0,0 +1,131 @@
|
||||
import 'package:starwork_flutter/base/app_logger.dart';
|
||||
import 'package:starwork_flutter/ble/command/base/base_ble_command.dart';
|
||||
import 'package:starwork_flutter/ble/command/base/base_ble_response_parser.dart';
|
||||
import 'package:starwork_flutter/ble/command/request/ble_cmd_get_public_key.dart';
|
||||
|
||||
/// ✨✨✨ 获取公钥命令应答数据结构 ✨✨✨
|
||||
class GetPublicKeyResponse {
|
||||
final int commandId;
|
||||
final int statusCode;
|
||||
final List<int> publicKey;
|
||||
final String publicKeyHex;
|
||||
|
||||
GetPublicKeyResponse({
|
||||
required this.commandId,
|
||||
required this.statusCode,
|
||||
required this.publicKey,
|
||||
required this.publicKeyHex,
|
||||
});
|
||||
|
||||
/// 是否成功
|
||||
bool get isSuccess => statusCode == 0x00;
|
||||
|
||||
/// 状态描述
|
||||
String get statusDescription {
|
||||
switch (statusCode) {
|
||||
case 0x00:
|
||||
return '成功';
|
||||
case 0x01:
|
||||
return '失败';
|
||||
case 0x02:
|
||||
return '参数错误';
|
||||
case 0x03:
|
||||
return '设备忙';
|
||||
case 0x06:
|
||||
return '需要token';
|
||||
default:
|
||||
return '未知状态(0x${statusCode.toRadixString(16).padLeft(2, '0')})';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'GetPublicKeyResponse(commandId: 0x${commandId.toRadixString(16).padLeft(4, '0')}, ' +
|
||||
'status: $statusDescription, publicKeyLength: ${publicKey.length}, ' +
|
||||
'publicKey: ${publicKeyHex.substring(0, publicKeyHex.length > 32 ? 32 : publicKeyHex.length)}...)';
|
||||
}
|
||||
}
|
||||
|
||||
/// ✨✨✨ 获取公钥命令应答解析器 ✨✨✨
|
||||
class BleCmdGetPublicKeyParser extends BaseBleResponseParser {
|
||||
@override
|
||||
int get commandId => BleCmdGetPublicKey.cmdId; // 0x3090
|
||||
|
||||
@override
|
||||
String get commandName => '获取公钥命令';
|
||||
|
||||
/// ✨✨✨ 解析获取公钥命令的应答数据 ✨✨✨
|
||||
@override
|
||||
GetPublicKeyResponse parseResponse(ParsedPacket parsedPacket, List<int> rawResponseData) {
|
||||
try {
|
||||
// 数据包格式分析:
|
||||
// 字节 0-1: 命令ID (0x3090, 大端序)
|
||||
// 字节 2: 状态码 (0x00=成功, 其他=失败)
|
||||
// 字节 3-N: 公钥数据 (长度可变)
|
||||
|
||||
if (rawResponseData.length < 3) {
|
||||
throw ArgumentError('应答数据长度不足: ${rawResponseData.length}字节 < 3字节');
|
||||
}
|
||||
|
||||
int offset = 0;
|
||||
|
||||
// 1. 提取命令ID (已在isMatchingResponse中验证过,这里记录日志)
|
||||
int responseCommandId = BaseBleResponseParser.extractInt(rawResponseData, offset, 2);
|
||||
offset += 2;
|
||||
|
||||
// 2. 提取状态码
|
||||
int statusCode = rawResponseData[offset++];
|
||||
|
||||
if (statusCode == 0) {
|
||||
List<int> publicKey = [];
|
||||
if (offset < rawResponseData.length) {
|
||||
publicKey = BaseBleResponseParser.extractBytes(rawResponseData, offset, rawResponseData.length - offset);
|
||||
}
|
||||
|
||||
String publicKeyHex = BaseBleResponseParser.bytesToHex(publicKey, separator: '');
|
||||
|
||||
// 4. 创建应答对象
|
||||
GetPublicKeyResponse response = GetPublicKeyResponse(
|
||||
commandId: responseCommandId,
|
||||
statusCode: statusCode,
|
||||
publicKey: publicKey,
|
||||
publicKeyHex: publicKeyHex,
|
||||
);
|
||||
|
||||
return response;
|
||||
} else {
|
||||
return GetPublicKeyResponse(
|
||||
commandId: responseCommandId,
|
||||
statusCode: statusCode,
|
||||
publicKey: [],
|
||||
publicKeyHex: '',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
AppLogger.error('❌ 解析获取公钥应答数据异常', error: e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取状态码描述
|
||||
String _getStatusDescription(int statusCode) {
|
||||
switch (statusCode) {
|
||||
case 0x00:
|
||||
return '成功';
|
||||
case 0x01:
|
||||
return '失败';
|
||||
case 0x02:
|
||||
return '参数错误';
|
||||
case 0x03:
|
||||
return '设备忙';
|
||||
default:
|
||||
return '未知状态';
|
||||
}
|
||||
}
|
||||
|
||||
/// ✨✨✨ 静态便捷方法 - 直接解析原始数据包 ✨✨✨
|
||||
static GetPublicKeyResponse? parseRawPacket(List<int> rawPacketData) {
|
||||
BleCmdGetPublicKeyParser parser = BleCmdGetPublicKeyParser();
|
||||
return parser.handleResponse(rawPacketData) as GetPublicKeyResponse?;
|
||||
}
|
||||
}
|
||||
3
lib/ble/exceptional/ble_connected_exception.dart
Normal file
3
lib/ble/exceptional/ble_connected_exception.dart
Normal file
@ -0,0 +1,3 @@
|
||||
class BleConnectedException implements Exception{
|
||||
|
||||
}
|
||||
@ -1,4 +1,8 @@
|
||||
class CacheKeys {
|
||||
static const String isSendValidationCode = 'isSendValidationCode';
|
||||
static const String token = 'token';
|
||||
static const String publicKeyHex = 'publicKeyHex';
|
||||
static const String starCloudUserInfo = 'starCloudUserInfo';
|
||||
static const String starCloudUserLoginInfo = 'starCloudUserLoginInfo';
|
||||
|
||||
}
|
||||
|
||||
545
lib/common/sm4_encipher/sm4.dart
Executable file
545
lib/common/sm4_encipher/sm4.dart
Executable file
@ -0,0 +1,545 @@
|
||||
import 'dart:convert';
|
||||
import 'utils/utils.dart';
|
||||
|
||||
enum SM4CryptoMode { ECB, CBC }
|
||||
|
||||
class SM4 {
|
||||
static const List<int> S_BOX = [
|
||||
0xd6,
|
||||
0x90,
|
||||
0xe9,
|
||||
0xfe,
|
||||
0xcc,
|
||||
0xe1,
|
||||
0x3d,
|
||||
0xb7,
|
||||
0x16,
|
||||
0xb6,
|
||||
0x14,
|
||||
0xc2,
|
||||
0x28,
|
||||
0xfb,
|
||||
0x2c,
|
||||
0x05,
|
||||
0x2b,
|
||||
0x67,
|
||||
0x9a,
|
||||
0x76,
|
||||
0x2a,
|
||||
0xbe,
|
||||
0x04,
|
||||
0xc3,
|
||||
0xaa,
|
||||
0x44,
|
||||
0x13,
|
||||
0x26,
|
||||
0x49,
|
||||
0x86,
|
||||
0x06,
|
||||
0x99,
|
||||
0x9c,
|
||||
0x42,
|
||||
0x50,
|
||||
0xf4,
|
||||
0x91,
|
||||
0xef,
|
||||
0x98,
|
||||
0x7a,
|
||||
0x33,
|
||||
0x54,
|
||||
0x0b,
|
||||
0x43,
|
||||
0xed,
|
||||
0xcf,
|
||||
0xac,
|
||||
0x62,
|
||||
0xe4,
|
||||
0xb3,
|
||||
0x1c,
|
||||
0xa9,
|
||||
0xc9,
|
||||
0x08,
|
||||
0xe8,
|
||||
0x95,
|
||||
0x80,
|
||||
0xdf,
|
||||
0x94,
|
||||
0xfa,
|
||||
0x75,
|
||||
0x8f,
|
||||
0x3f,
|
||||
0xa6,
|
||||
0x47,
|
||||
0x07,
|
||||
0xa7,
|
||||
0xfc,
|
||||
0xf3,
|
||||
0x73,
|
||||
0x17,
|
||||
0xba,
|
||||
0x83,
|
||||
0x59,
|
||||
0x3c,
|
||||
0x19,
|
||||
0xe6,
|
||||
0x85,
|
||||
0x4f,
|
||||
0xa8,
|
||||
0x68,
|
||||
0x6b,
|
||||
0x81,
|
||||
0xb2,
|
||||
0x71,
|
||||
0x64,
|
||||
0xda,
|
||||
0x8b,
|
||||
0xf8,
|
||||
0xeb,
|
||||
0x0f,
|
||||
0x4b,
|
||||
0x70,
|
||||
0x56,
|
||||
0x9d,
|
||||
0x35,
|
||||
0x1e,
|
||||
0x24,
|
||||
0x0e,
|
||||
0x5e,
|
||||
0x63,
|
||||
0x58,
|
||||
0xd1,
|
||||
0xa2,
|
||||
0x25,
|
||||
0x22,
|
||||
0x7c,
|
||||
0x3b,
|
||||
0x01,
|
||||
0x21,
|
||||
0x78,
|
||||
0x87,
|
||||
0xd4,
|
||||
0x00,
|
||||
0x46,
|
||||
0x57,
|
||||
0x9f,
|
||||
0xd3,
|
||||
0x27,
|
||||
0x52,
|
||||
0x4c,
|
||||
0x36,
|
||||
0x02,
|
||||
0xe7,
|
||||
0xa0,
|
||||
0xc4,
|
||||
0xc8,
|
||||
0x9e,
|
||||
0xea,
|
||||
0xbf,
|
||||
0x8a,
|
||||
0xd2,
|
||||
0x40,
|
||||
0xc7,
|
||||
0x38,
|
||||
0xb5,
|
||||
0xa3,
|
||||
0xf7,
|
||||
0xf2,
|
||||
0xce,
|
||||
0xf9,
|
||||
0x61,
|
||||
0x15,
|
||||
0xa1,
|
||||
0xe0,
|
||||
0xae,
|
||||
0x5d,
|
||||
0xa4,
|
||||
0x9b,
|
||||
0x34,
|
||||
0x1a,
|
||||
0x55,
|
||||
0xad,
|
||||
0x93,
|
||||
0x32,
|
||||
0x30,
|
||||
0xf5,
|
||||
0x8c,
|
||||
0xb1,
|
||||
0xe3,
|
||||
0x1d,
|
||||
0xf6,
|
||||
0xe2,
|
||||
0x2e,
|
||||
0x82,
|
||||
0x66,
|
||||
0xca,
|
||||
0x60,
|
||||
0xc0,
|
||||
0x29,
|
||||
0x23,
|
||||
0xab,
|
||||
0x0d,
|
||||
0x53,
|
||||
0x4e,
|
||||
0x6f,
|
||||
0xd5,
|
||||
0xdb,
|
||||
0x37,
|
||||
0x45,
|
||||
0xde,
|
||||
0xfd,
|
||||
0x8e,
|
||||
0x2f,
|
||||
0x03,
|
||||
0xff,
|
||||
0x6a,
|
||||
0x72,
|
||||
0x6d,
|
||||
0x6c,
|
||||
0x5b,
|
||||
0x51,
|
||||
0x8d,
|
||||
0x1b,
|
||||
0xaf,
|
||||
0x92,
|
||||
0xbb,
|
||||
0xdd,
|
||||
0xbc,
|
||||
0x7f,
|
||||
0x11,
|
||||
0xd9,
|
||||
0x5c,
|
||||
0x41,
|
||||
0x1f,
|
||||
0x10,
|
||||
0x5a,
|
||||
0xd8,
|
||||
0x0a,
|
||||
0xc1,
|
||||
0x31,
|
||||
0x88,
|
||||
0xa5,
|
||||
0xcd,
|
||||
0x7b,
|
||||
0xbd,
|
||||
0x2d,
|
||||
0x74,
|
||||
0xd0,
|
||||
0x12,
|
||||
0xb8,
|
||||
0xe5,
|
||||
0xb4,
|
||||
0xb0,
|
||||
0x89,
|
||||
0x69,
|
||||
0x97,
|
||||
0x4a,
|
||||
0x0c,
|
||||
0x96,
|
||||
0x77,
|
||||
0x7e,
|
||||
0x65,
|
||||
0xb9,
|
||||
0xf1,
|
||||
0x09,
|
||||
0xc5,
|
||||
0x6e,
|
||||
0xc6,
|
||||
0x84,
|
||||
0x18,
|
||||
0xf0,
|
||||
0x7d,
|
||||
0xec,
|
||||
0x3a,
|
||||
0xdc,
|
||||
0x4d,
|
||||
0x20,
|
||||
0x79,
|
||||
0xee,
|
||||
0x5f,
|
||||
0x3e,
|
||||
0xd7,
|
||||
0xcb,
|
||||
0x39,
|
||||
0x48
|
||||
];
|
||||
|
||||
static const List<int> FK = [0xA3B1BAC6, 0x56AA3350, 0x677D9197, 0xB27022DC];
|
||||
|
||||
static const List<int> CK = [
|
||||
0x00070e15,
|
||||
0x1c232a31,
|
||||
0x383f464d,
|
||||
0x545b6269,
|
||||
0x70777e85,
|
||||
0x8c939aa1,
|
||||
0xa8afb6bd,
|
||||
0xc4cbd2d9,
|
||||
0xe0e7eef5,
|
||||
0xfc030a11,
|
||||
0x181f262d,
|
||||
0x343b4249,
|
||||
0x50575e65,
|
||||
0x6c737a81,
|
||||
0x888f969d,
|
||||
0xa4abb2b9,
|
||||
0xc0c7ced5,
|
||||
0xdce3eaf1,
|
||||
0xf8ff060d,
|
||||
0x141b2229,
|
||||
0x30373e45,
|
||||
0x4c535a61,
|
||||
0x686f767d,
|
||||
0x848b9299,
|
||||
0xa0a7aeb5,
|
||||
0xbcc3cad1,
|
||||
0xd8dfe6ed,
|
||||
0xf4fb0209,
|
||||
0x10171e25,
|
||||
0x2c333a41,
|
||||
0x484f565d,
|
||||
0x646b7279
|
||||
];
|
||||
|
||||
static const int SM4_ENCRYPT = 1;
|
||||
|
||||
static const int SM4_DECRYPT = 0;
|
||||
|
||||
static const int blockSize = 16;
|
||||
|
||||
static final _encryptKey = List<int>.filled(32, 0);
|
||||
static final _decryptKey = List<int>.filled(32, 0);
|
||||
|
||||
static int _readUint32BE(List<int> b, int i) {
|
||||
return ((b[i] & 0xff) << 24) |
|
||||
((b[i + 1] & 0xff) << 16) |
|
||||
((b[i + 2] & 0xff) << 8) |
|
||||
(b[i + 3] & 0xff);
|
||||
}
|
||||
|
||||
static void _writeUint32BE(int n, List<int> b, int i) {
|
||||
b[i] = ((n >> 24) & 0xff);
|
||||
b[i + 1] = ((n >> 16) & 0xff);
|
||||
b[i + 2] = ((n >> 8) & 0xff);
|
||||
b[i + 3] = n & 0xff;
|
||||
}
|
||||
|
||||
static int _Sbox(int inch) => S_BOX[inch & 0xFF];
|
||||
|
||||
static int _sm4F(int x0, int x1, int x2, int x3, int rk) {
|
||||
final x = x1 ^ x2 ^ x3 ^ rk;
|
||||
int bb = 0;
|
||||
int c = 0;
|
||||
List<int> a = List<int>.filled(4, 0);
|
||||
List<int> b = List<int>.filled(4, 0);
|
||||
_writeUint32BE(x, a, 0);
|
||||
b[0] = _Sbox(a[0]);
|
||||
b[1] = _Sbox(a[1]);
|
||||
b[2] = _Sbox(a[2]);
|
||||
b[3] = _Sbox(a[3]);
|
||||
bb = _readUint32BE(b, 0);
|
||||
|
||||
c = bb ^
|
||||
SMUtils.leftShift(bb, 2) ^
|
||||
SMUtils.leftShift(bb, 10) ^
|
||||
SMUtils.leftShift(bb, 18) ^
|
||||
SMUtils.leftShift(bb, 24);
|
||||
return x0 ^ c;
|
||||
}
|
||||
|
||||
static int _calculateRoundKey(int key) {
|
||||
int roundKey = 0;
|
||||
List<int> keyBytes = List<int>.filled(4, 0);
|
||||
List<int> sboxBytes = List<int>.filled(4, 0);
|
||||
_writeUint32BE(key, keyBytes, 0);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
sboxBytes[i] = _Sbox(keyBytes[i]);
|
||||
}
|
||||
int temp = _readUint32BE(sboxBytes, 0);
|
||||
roundKey = temp ^ SMUtils.leftShift(temp, 13) ^ SMUtils.leftShift(temp, 23);
|
||||
return roundKey;
|
||||
}
|
||||
|
||||
static void setKey(List<int> key) {
|
||||
List<int> keyBytes = key;
|
||||
List<int> intermediateKeys = List<int>.filled(36, 0);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
intermediateKeys[i] = _readUint32BE(keyBytes, i * 4) ^ FK[i];
|
||||
}
|
||||
for (int i = 0; i < 32; i++) {
|
||||
intermediateKeys[i + 4] = intermediateKeys[i] ^
|
||||
_calculateRoundKey(intermediateKeys[i + 1] ^
|
||||
intermediateKeys[i + 2] ^
|
||||
intermediateKeys[i + 3] ^
|
||||
CK[i]);
|
||||
_encryptKey[i] = intermediateKeys[i + 4];
|
||||
}
|
||||
|
||||
for (int i = 0; i < 16; i++) {
|
||||
int temp = _encryptKey[i];
|
||||
_decryptKey[i] = _encryptKey[31 - i];
|
||||
_decryptKey[31 - i] = temp;
|
||||
}
|
||||
}
|
||||
|
||||
static void _round(List<int> sk, List<int> input, List<int> output) {
|
||||
int i = 0;
|
||||
List<int> ulbuf = List<int>.filled(36, 0);
|
||||
ulbuf[0] = _readUint32BE(input, 0);
|
||||
ulbuf[1] = _readUint32BE(input, 4);
|
||||
ulbuf[2] = _readUint32BE(input, 8);
|
||||
ulbuf[3] = _readUint32BE(input, 12);
|
||||
while (i < 32) {
|
||||
ulbuf[i + 4] =
|
||||
_sm4F(ulbuf[i], ulbuf[i + 1], ulbuf[i + 2], ulbuf[i + 3], sk[i]);
|
||||
i++;
|
||||
}
|
||||
|
||||
_writeUint32BE(ulbuf[35], output, 0);
|
||||
_writeUint32BE(ulbuf[34], output, 4);
|
||||
_writeUint32BE(ulbuf[33], output, 8);
|
||||
_writeUint32BE(ulbuf[32], output, 12);
|
||||
}
|
||||
|
||||
static List<int> _padding(List<int> input, int mode) {
|
||||
final int padLen = blockSize - (input.length % blockSize);
|
||||
|
||||
if (mode == SM4_ENCRYPT) {
|
||||
final paddedList = List<int>.filled(input.length + padLen, 0);
|
||||
paddedList.setRange(0, input.length, input);
|
||||
for (int i = input.length; i < paddedList.length; i++) {
|
||||
paddedList[i] = padLen;
|
||||
}
|
||||
return paddedList;
|
||||
} else {
|
||||
// final lastByte = input.last;
|
||||
// final cutLen = input.length - lastByte;
|
||||
// Log.p("object input.length:${input.length} lastByte:$lastByte input:$input cutLen:$cutLen");
|
||||
// return input.sublist(0, cutLen);
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
static List<int> _crypto(
|
||||
List<int> data, int flag, SM4CryptoMode mode, String? iv) {
|
||||
late List<int> lastVector;
|
||||
if (mode == SM4CryptoMode.CBC) {
|
||||
if (iv == null || iv.length != 32)
|
||||
throw Exception("IV must be a string of length 16");
|
||||
else
|
||||
lastVector = SMUtils.hexStringToBytes(iv);
|
||||
}
|
||||
final key = (flag == SM4_ENCRYPT) ? _encryptKey : _decryptKey;
|
||||
if (flag == SM4_ENCRYPT) {
|
||||
data = _padding(data, SM4_ENCRYPT);
|
||||
}
|
||||
final length = data.length;
|
||||
final List<int> output = [];
|
||||
|
||||
for (int offset = 0; offset < length; offset += blockSize) {
|
||||
final outData = List<int>.filled(blockSize, 0);
|
||||
final copyLen =
|
||||
(offset + blockSize <= length) ? blockSize : length - offset;
|
||||
final input = data.sublist(offset, offset + copyLen);
|
||||
if (mode == SM4CryptoMode.CBC && flag == SM4_ENCRYPT) {
|
||||
for (int i = 0; i < blockSize; i++) {
|
||||
input[i] = input[i] ^ lastVector[i];
|
||||
}
|
||||
}
|
||||
_round(key, input, outData);
|
||||
|
||||
if (mode == SM4CryptoMode.CBC && flag == SM4_DECRYPT) {
|
||||
for (int i = 0; i < blockSize; i++) {
|
||||
outData[i] ^= lastVector[i];
|
||||
}
|
||||
}
|
||||
output.addAll(outData);
|
||||
|
||||
if (mode == SM4CryptoMode.CBC) {
|
||||
if (flag == SM4_ENCRYPT) {
|
||||
lastVector = outData;
|
||||
} else {
|
||||
lastVector = input;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (flag == SM4_DECRYPT) {
|
||||
return _padding(output, SM4_DECRYPT);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/// Utf8 to byte list
|
||||
static List<int> _utf8ToArray(String str) {
|
||||
return utf8.encode(str);
|
||||
}
|
||||
|
||||
// /// auto add 0x00
|
||||
static List<int> _autoAddZero(List<int> list) {
|
||||
/// supplementary list
|
||||
List<int> supplementList = List.filled(16, 0x00);
|
||||
|
||||
/// complete list
|
||||
List<int> completeList = [...list, ...supplementList].sublist(0, 16);
|
||||
return completeList;
|
||||
}
|
||||
|
||||
/// hex byte list to hex string
|
||||
static String _listToHex(List<int> arr) {
|
||||
String hexString = arr
|
||||
.map((item) {
|
||||
String itemHexString = item.toRadixString(16);
|
||||
// The hexadecimal notation is 0123456789ABCDEF
|
||||
//if there is a single one, add 0
|
||||
if (itemHexString.length == 1) {
|
||||
return '0$itemHexString';
|
||||
} else {
|
||||
return itemHexString;
|
||||
}
|
||||
})
|
||||
.toList()
|
||||
.join('');
|
||||
|
||||
return hexString;
|
||||
}
|
||||
|
||||
static String createHexKey({
|
||||
required String key,
|
||||
autoPushZero = true,
|
||||
}) {
|
||||
List<int> keyList = _utf8ToArray(key);
|
||||
|
||||
if (autoPushZero) {
|
||||
if (keyList.length < 128 / 8) {
|
||||
keyList = _autoAddZero(keyList);
|
||||
}
|
||||
if (keyList.length > 128 / 8) {
|
||||
keyList = keyList.sublist(0, 16);
|
||||
}
|
||||
}
|
||||
return _listToHex(keyList);
|
||||
}
|
||||
|
||||
static List<int> encrypt(List<int> data,
|
||||
{List<int>? key, SM4CryptoMode mode = SM4CryptoMode.ECB, String? iv}) {
|
||||
if (key != null) setKey(key);
|
||||
List<int> input = data;
|
||||
List<int> output = _crypto(input, SM4_ENCRYPT, mode, iv);
|
||||
return output;
|
||||
}
|
||||
|
||||
// static decrypt(String cipherText,
|
||||
// {String? key, SM4CryptoMode mode = SM4CryptoMode.ECB, String? iv}) {
|
||||
// if (key != null) setKey(key);
|
||||
// List<int> input = SMUtils.hexStringToBytes(cipherText);
|
||||
// List<int> output = _crypto(input, SM4_DECRYPT, mode, iv);
|
||||
// return utf8.decode(output);
|
||||
// }
|
||||
|
||||
static decrypt(List<int> data,
|
||||
{List<int>? key, SM4CryptoMode mode = SM4CryptoMode.ECB, String? iv}) {
|
||||
if (key != null) setKey(key);
|
||||
List<int> input = data;
|
||||
List<int> output = _crypto(input, SM4_DECRYPT, mode, iv);
|
||||
return output;
|
||||
}
|
||||
}
|
||||
150
lib/common/sm4_encipher/utils/asn1.dart
Executable file
150
lib/common/sm4_encipher/utils/asn1.dart
Executable file
@ -0,0 +1,150 @@
|
||||
|
||||
|
||||
class _ASN1Object {
|
||||
String? tlv;
|
||||
String t = '00';
|
||||
String l = '00';
|
||||
String v = '';
|
||||
|
||||
_ASN1Object() {
|
||||
tlv = null;
|
||||
}
|
||||
|
||||
/// 获取 der 编码比特流16进制串
|
||||
String getEncodedHex() {
|
||||
if (tlv == null) {
|
||||
v = getValue();
|
||||
l = getLength();
|
||||
tlv = t + l + v;
|
||||
}
|
||||
return tlv!;
|
||||
}
|
||||
|
||||
String getLength() {
|
||||
int n = v.length ~/ 2; // 字节数
|
||||
String nHex = n.toRadixString(16);
|
||||
if (nHex.length % 2 == 1) nHex = '0$nHex';
|
||||
|
||||
if (n < 128) {
|
||||
return nHex;
|
||||
} else {
|
||||
int head = 128 + nHex.length ~/ 2;
|
||||
return head.toRadixString(16) + nHex;
|
||||
}
|
||||
}
|
||||
|
||||
String getValue() {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
class _DERInteger extends _ASN1Object {
|
||||
_DERInteger(BigInt? bigint) : super() {
|
||||
t = '02'; // 整型标签说明
|
||||
if (bigint != null) v = bigintToValue(bigint);
|
||||
}
|
||||
|
||||
@override
|
||||
String getValue() {
|
||||
return v;
|
||||
}
|
||||
|
||||
String bigintToValue(BigInt inputBigInt) {
|
||||
String hexString = inputBigInt.toRadixString(16);
|
||||
if (!hexString.startsWith('-')) {
|
||||
if (hexString.length % 2 == 1) {
|
||||
hexString = '0$hexString';
|
||||
} else if (!RegExp(r'^[0-7]').hasMatch(hexString)) {
|
||||
hexString = '00$hexString';
|
||||
}
|
||||
} else {
|
||||
// Negative number
|
||||
hexString = hexString.substring(1);
|
||||
|
||||
int paddedLength = hexString.length;
|
||||
if (paddedLength % 2 == 1) {
|
||||
paddedLength += 1; // Pad to a whole byte
|
||||
} else if (!RegExp(r'^[0-7]').hasMatch(hexString)) {
|
||||
paddedLength += 2;
|
||||
}
|
||||
|
||||
String bitmask = '';
|
||||
for (int i = 0; i < paddedLength; i++) {
|
||||
bitmask += 'f';
|
||||
}
|
||||
BigInt bitmaskBigInt = BigInt.parse(bitmask, radix: 16);
|
||||
|
||||
BigInt twosComplementBigInt = bitmaskBigInt ^ inputBigInt;
|
||||
twosComplementBigInt = twosComplementBigInt + BigInt.one;
|
||||
hexString = twosComplementBigInt.toRadixString(16).replaceAll(RegExp(r'^-'), '');
|
||||
}
|
||||
return hexString;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class _DERSequence extends _ASN1Object {
|
||||
List<_ASN1Object> asn1Array;
|
||||
|
||||
_DERSequence(this.asn1Array) : super() {
|
||||
t = '30'; // 序列标签说明
|
||||
}
|
||||
|
||||
@override
|
||||
String getValue() {
|
||||
v = asn1Array.map((asn1Object) => asn1Object.getEncodedHex()).join('');
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
int getLenOfL(String str, int start) {
|
||||
if (int.parse(str[start + 2]) < 8) return 1;
|
||||
return int.parse(str.substring(start + 2, start + 4)) & 0x7f + 1;
|
||||
}
|
||||
|
||||
int getL(String str, int start) {
|
||||
// 获取 l
|
||||
int len = getLenOfL(str, start);
|
||||
String l = str.substring(start + 2, start + 2 + len * 2);
|
||||
|
||||
if (l.isEmpty) return -1;
|
||||
BigInt bigint = int.parse(l[0]) < 8
|
||||
? BigInt.parse(l, radix: 16)
|
||||
: BigInt.parse(l.substring(2), radix: 16);
|
||||
|
||||
return bigint.toInt();
|
||||
}
|
||||
|
||||
int getStartOfV(String str, int start) {
|
||||
int len = getLenOfL(str, start);
|
||||
return start + (len + 1) * 2;
|
||||
}
|
||||
|
||||
class ASN1Utils {
|
||||
/// ASN.1 der 编码,针对 sm2 签名
|
||||
static String encodeDer(BigInt r, BigInt s) {
|
||||
final derR = _DERInteger(r);
|
||||
final derS = _DERInteger(s);
|
||||
final derSeq = _DERSequence([derR, derS]);
|
||||
return derSeq.getEncodedHex();
|
||||
}
|
||||
|
||||
/// 解析 ASN.1 der,针对 sm2 验签
|
||||
static Map<String, BigInt> decodeDer(String input) {
|
||||
int start = getStartOfV(input, 0);
|
||||
|
||||
int vIndexR = getStartOfV(input, start);
|
||||
int lR = getL(input, start);
|
||||
String vR = input.substring(vIndexR, vIndexR + lR * 2);
|
||||
|
||||
int nextStart = vIndexR + vR.length;
|
||||
int vIndexS = getStartOfV(input, nextStart);
|
||||
int lS = getL(input, nextStart);
|
||||
String vS = input.substring(vIndexS, vIndexS + lS * 2);
|
||||
|
||||
BigInt r = BigInt.parse(vR, radix: 16);
|
||||
BigInt s = BigInt.parse(vS, radix: 16);
|
||||
|
||||
return {'r': r, 's': s};
|
||||
}
|
||||
}
|
||||
248
lib/common/sm4_encipher/utils/ec.dart
Executable file
248
lib/common/sm4_encipher/utils/ec.dart
Executable file
@ -0,0 +1,248 @@
|
||||
class ECFieldElementFp {
|
||||
final BigInt x;
|
||||
final BigInt q;
|
||||
|
||||
ECFieldElementFp(this.q, this.x) {
|
||||
// TODO if (x.compareTo(q) >= 0) error
|
||||
}
|
||||
|
||||
/// 判断相等
|
||||
bool equals(ECFieldElementFp other) {
|
||||
if (other == this) return true;
|
||||
return (q == other.q && x == other.x);
|
||||
}
|
||||
|
||||
/// 返回具体数值
|
||||
BigInt toBigInteger() {
|
||||
return x;
|
||||
}
|
||||
|
||||
/// 取反
|
||||
ECFieldElementFp negate() {
|
||||
return ECFieldElementFp(q, (-x) % q);
|
||||
}
|
||||
|
||||
/// 相加
|
||||
ECFieldElementFp add(ECFieldElementFp b) {
|
||||
return ECFieldElementFp(q, (x + b.toBigInteger()) % q);
|
||||
}
|
||||
|
||||
/// 相减
|
||||
ECFieldElementFp subtract(ECFieldElementFp b) {
|
||||
return ECFieldElementFp(q, (x - b.toBigInteger()) % q);
|
||||
}
|
||||
|
||||
/// 相乘
|
||||
ECFieldElementFp multiply(ECFieldElementFp b) {
|
||||
return ECFieldElementFp(q, (x * b.toBigInteger()) % q);
|
||||
}
|
||||
|
||||
/// 相除
|
||||
ECFieldElementFp divide(ECFieldElementFp b) {
|
||||
return ECFieldElementFp(q, (x * b.toBigInteger().modInverse(q)) % q);
|
||||
}
|
||||
|
||||
/// 平方
|
||||
ECFieldElementFp square() {
|
||||
return ECFieldElementFp(q, (x * x) % q);
|
||||
}
|
||||
}
|
||||
|
||||
class ECPointFp {
|
||||
final ECCurveFp curve;
|
||||
late final ECFieldElementFp? x;
|
||||
late final ECFieldElementFp? y;
|
||||
late final BigInt z;
|
||||
BigInt? zinv;
|
||||
|
||||
ECPointFp(this.curve, this.x, this.y, [BigInt? z]) {
|
||||
this.z = z ?? BigInt.one;
|
||||
zinv = null;
|
||||
}
|
||||
|
||||
ECFieldElementFp getX() {
|
||||
zinv ??= z.modInverse(curve.q);
|
||||
return curve.fromBigInteger(x!.toBigInteger() * zinv! % curve.q);
|
||||
}
|
||||
|
||||
ECFieldElementFp getY() {
|
||||
zinv ??= z.modInverse(curve.q);
|
||||
return curve.fromBigInteger(y!.toBigInteger() * zinv! % curve.q);
|
||||
}
|
||||
|
||||
bool equals(ECPointFp other) {
|
||||
if (other == this) return true;
|
||||
if (isInfinity()) return other.isInfinity();
|
||||
if (other.isInfinity()) return isInfinity();
|
||||
|
||||
final u = (other.y!.toBigInteger() * z - y!.toBigInteger() * other.z) % curve.q;
|
||||
if (u != BigInt.zero) return false;
|
||||
|
||||
final v = (other.x!.toBigInteger() * z - x!.toBigInteger() * other.z) % curve.q;
|
||||
return v == BigInt.zero;
|
||||
}
|
||||
|
||||
bool isInfinity() {
|
||||
if (x == null && y == null) return true;
|
||||
return z == BigInt.zero && y!.toBigInteger() != BigInt.zero;
|
||||
}
|
||||
|
||||
ECPointFp negate() {
|
||||
return ECPointFp(curve, x, y!.negate(), z);
|
||||
}
|
||||
|
||||
ECPointFp add(ECPointFp b) {
|
||||
if (isInfinity()) return b;
|
||||
if (b.isInfinity()) return this;
|
||||
final x1 = x!.toBigInteger();
|
||||
final y1 = y!.toBigInteger();
|
||||
final z1 = z;
|
||||
final x2 = b.x!.toBigInteger();
|
||||
final y2 = b.y!.toBigInteger();
|
||||
final z2 = b.z;
|
||||
final q = curve.q;
|
||||
final w1 = (x1 * z2) % q;
|
||||
final w2 = (x2 * z1) % q;
|
||||
final w3 = (w1 - w2) % q;
|
||||
final w4 = (y1 * z2) % q;
|
||||
final w5 = (y2 * z1) % q;
|
||||
final w6 = (w4 - w5) % q;
|
||||
|
||||
if (w3 == BigInt.zero) {
|
||||
if (w6 == BigInt.zero) {
|
||||
return twice();
|
||||
}
|
||||
return curve.infinity;
|
||||
}
|
||||
|
||||
final w7 = (w1 + w2) % q;
|
||||
final w8 = (z1 * z2) % q;
|
||||
final w9 = (w3 * w3) % q;
|
||||
final w10 = (w3 * w9) % q;
|
||||
final w11 = (w8 * (w6 * w6) % q - w7 * w9) % q;
|
||||
|
||||
final x3 = (w3 * w11) % q;
|
||||
final y3 = (w6 * (w9 * w1 % q - w11) - w4 * w10) % q;
|
||||
final z3 = (w10 * w8) % q;
|
||||
|
||||
return ECPointFp(curve, curve.fromBigInteger(x3), curve.fromBigInteger(y3), z3);
|
||||
}
|
||||
|
||||
ECPointFp twice() {
|
||||
if (isInfinity()) return this;
|
||||
if (y!.toBigInteger().sign == 0) return curve.infinity;
|
||||
|
||||
final x1 = x!.toBigInteger();
|
||||
final y1 = y!.toBigInteger();
|
||||
final z1 = z;
|
||||
final q = curve.q;
|
||||
final a = curve.a.toBigInteger();
|
||||
|
||||
final w1 = (x1 * x1 * BigInt.from(3) + a * (z1 * z1)) % q;
|
||||
final w2 = (y1 * BigInt.from(2) * z1) % q;
|
||||
final w3 = (y1 * y1) % q;
|
||||
final w4 = (w3 * x1 * z1) % q;
|
||||
final w5 = (w2 * w2) % q;
|
||||
final w6 = (w1 * w1 - w4 * BigInt.from(8)) % q;
|
||||
|
||||
final x3 = (w2 * w6) % q;
|
||||
final y3 = (w1 * (w4 * BigInt.from(4) - w6) - w5 * BigInt.from(2) * w3) % q;
|
||||
final z3 = (w2 * w5) % q;
|
||||
|
||||
return ECPointFp(curve, curve.fromBigInteger(x3), curve.fromBigInteger(y3), z3);
|
||||
}
|
||||
|
||||
ECPointFp multiply(BigInt k) {
|
||||
if (isInfinity()) return this;
|
||||
if (k.sign == 0) return curve.infinity;
|
||||
|
||||
final k3 = k * BigInt.from(3);
|
||||
final neg = negate();
|
||||
ECPointFp Q = this;
|
||||
|
||||
for (int i = k3.bitLength - 2; i > 0; i--) {
|
||||
Q = Q.twice();
|
||||
|
||||
/*final k3Bit = (k3 >> i) & BigInt.one == BigInt.one;
|
||||
final kBit = (k >> i) & BigInt.one == BigInt.zero;*/
|
||||
|
||||
final k3Bit = (k3 >> i).isOdd;
|
||||
;
|
||||
final kBit = (k >> i).isOdd;
|
||||
|
||||
if (k3Bit != kBit) {
|
||||
Q = Q.add(k3Bit ? this : neg);
|
||||
}
|
||||
}
|
||||
|
||||
return Q;
|
||||
}
|
||||
}
|
||||
|
||||
class ECCurveFp {
|
||||
ECCurveFp(this.q, BigInt a, BigInt b) {
|
||||
this.a = fromBigInteger(a);
|
||||
this.b = fromBigInteger(b);
|
||||
infinity = ECPointFp(this, null, null); // 无穷远点
|
||||
}
|
||||
|
||||
final BigInt q;
|
||||
late ECFieldElementFp a;
|
||||
late ECFieldElementFp b;
|
||||
late ECPointFp infinity;
|
||||
|
||||
bool equals(Object? other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! ECCurveFp) return false;
|
||||
return q == other.q && a == other.a && b == other.b;
|
||||
}
|
||||
|
||||
ECFieldElementFp fromBigInteger(BigInt x) {
|
||||
return ECFieldElementFp(q, x);
|
||||
}
|
||||
|
||||
ECPointFp? decodePointHex(String s) {
|
||||
switch (int.parse(s.substring(0, 2), radix: 16)) {
|
||||
case 0:
|
||||
return infinity;
|
||||
case 2:
|
||||
case 3:
|
||||
final x = fromBigInteger(BigInt.parse(s.substring(2), radix: 16));
|
||||
var y = fromBigInteger(x
|
||||
.multiply(x.square())
|
||||
.add(x.multiply(a))
|
||||
.add(b)
|
||||
.toBigInteger()
|
||||
.modPow(q ~/ BigInt.from(4) + BigInt.one, q));
|
||||
|
||||
/* var y = x
|
||||
.multiply(x.square())
|
||||
.add(x.multiply(a.add(b)))
|
||||
.toBigInteger()
|
||||
.modPow(q ~/ BigInt.from(4) + BigInt.one, q);*/
|
||||
if (y.toBigInteger() % BigInt.two !=
|
||||
BigInt.parse(s.substring(0, 2), radix: 16) - BigInt.two) {
|
||||
y = y.negate();
|
||||
}
|
||||
return ECPointFp(this, x, y);
|
||||
case 4:
|
||||
case 6:
|
||||
case 7:
|
||||
final len = (s.length - 2) ~/ 2;
|
||||
final xHex = s.substring(2, 2 + len);
|
||||
final yHex = s.substring(2 + len, 2 + 2 * len);
|
||||
/*print("xHex: ${BigInt.parse(xHex, radix: 16).toRadixString(16)}");
|
||||
print("yHex: ${BigInt.parse(yHex, radix: 16).toRadixString(16)}");*/
|
||||
return ECPointFp(this, fromBigInteger(BigInt.parse(xHex, radix: 16)),
|
||||
fromBigInteger(BigInt.parse(yHex, radix: 16)));
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String leftPad(String input, int num) {
|
||||
if (input.length >= num) return input;
|
||||
return List.filled(num - input.length, '0').join() + input;
|
||||
}
|
||||
|
||||
46
lib/common/sm4_encipher/utils/utils.dart
Executable file
46
lib/common/sm4_encipher/utils/utils.dart
Executable file
@ -0,0 +1,46 @@
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
class SMUtils{
|
||||
static int leftShift(int x, int n){
|
||||
int s = n & 31;
|
||||
x = (x & 0xFFFFFFFF).toSigned(32);
|
||||
return (((x << s) | ((x & 0xFFFFFFFF) >> (32 - s))) & 0xFFFFFFFF).toSigned(32);
|
||||
}
|
||||
|
||||
static int rightShift(int x, int n) {
|
||||
int s = n & 31;
|
||||
x = (x & 0xFFFFFFFF).toSigned(32);
|
||||
return ((x >> s) | ((x << (32 - s)) & 0xFFFFFFFF)).toSigned(32);
|
||||
}
|
||||
|
||||
static String bytesToHexString(List<int> bytes) {
|
||||
final buffer = StringBuffer();
|
||||
for (final byte in bytes) {
|
||||
buffer.write(byte.toRadixString(16).padLeft(2, '0'));
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
static List<int> hexStringToBytes(String hexString) {
|
||||
final length = hexString.length ~/ 2;
|
||||
final bytes = List<int>.filled(length, 0);
|
||||
for (int i = 0; i < length; i++) {
|
||||
final byteString = hexString.substring(i * 2, i * 2 + 2);
|
||||
bytes[i] = int.parse(byteString, radix: 16);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
static String utf8ToHexString(String input) {
|
||||
List<int> utf8Encoded = utf8.encode(input);
|
||||
// 转换到16进制
|
||||
StringBuffer hexChars = StringBuffer();
|
||||
for (int i = 0; i < utf8Encoded.length; i++) {
|
||||
int bite = utf8Encoded[i];
|
||||
hexChars.write((bite >> 4).toRadixString(16));
|
||||
hexChars.write((bite & 0x0f).toRadixString(16));
|
||||
}
|
||||
return hexChars.toString();
|
||||
}
|
||||
}
|
||||
@ -25,8 +25,7 @@ class SharedPreferencesUtils {
|
||||
final expireAt = DateTime.now().add(expiry).millisecondsSinceEpoch;
|
||||
|
||||
// 使用 Transaction 确保原子性(可选)
|
||||
return await prefs.setString(key, value) &&
|
||||
await prefs.setInt(expiryKey, expireAt);
|
||||
return await prefs.setString(key, value) && await prefs.setInt(expiryKey, expireAt);
|
||||
}
|
||||
|
||||
/// 获取带过期时间的字符串
|
||||
@ -72,6 +71,10 @@ class SharedPreferencesUtils {
|
||||
return _prefs?.getString(key);
|
||||
}
|
||||
|
||||
static Future<bool> delString(String key) async {
|
||||
return _prefs?.remove(key) ?? Future.value(false);
|
||||
}
|
||||
|
||||
// bool
|
||||
static Future<void> setBool(String key, dynamic value) async {
|
||||
_prefs?.setBool(key, value);
|
||||
|
||||
@ -67,7 +67,7 @@ class F {
|
||||
// Debug/Profile环境的StarCloud地址(开发环境)
|
||||
switch (appFlavor) {
|
||||
case Flavor.sky:
|
||||
return 'http://192.168.1.121:8111';
|
||||
return 'http://192.168.1.121:8111/sdk';
|
||||
case Flavor.xhj:
|
||||
return 'http://local.cloud.star-lock.cn';
|
||||
}
|
||||
|
||||
@ -3,39 +3,32 @@ import 'package:starwork_flutter/base/app_logger.dart';
|
||||
import 'package:starwork_flutter/base/app_permission.dart';
|
||||
import 'package:starwork_flutter/base/base_controller.dart';
|
||||
import 'package:starwork_flutter/ble/ble_service.dart';
|
||||
import 'package:starwork_flutter/ble/command/base/base_ble_response_parser.dart';
|
||||
import 'package:starwork_flutter/ble/command/request/ble_cmd_get_private_key.dart';
|
||||
import 'package:starwork_flutter/ble/command/request/ble_cmd_get_public_key.dart';
|
||||
import 'package:starwork_flutter/ble/command/response/ble_cmd_get_private_key_parser.dart';
|
||||
import 'package:starwork_flutter/ble/command/response/ble_cmd_get_public_key_parser.dart';
|
||||
import 'package:starwork_flutter/ble/model/scan_device_info.dart';
|
||||
import 'package:starwork_flutter/common/constant/app_toast_messages.dart';
|
||||
import 'package:starwork_flutter/routes/app_routes.dart';
|
||||
import 'package:starwork_flutter/common/constant/cache_keys.dart';
|
||||
import 'package:starwork_flutter/common/utils/shared_preferences_utils.dart';
|
||||
|
||||
class SearchDeviceController extends BaseController {
|
||||
// 搜索状态管理
|
||||
final RxBool isSearching = BleService().isScanningNow.obs;
|
||||
final RxBool isSearching = false.obs;
|
||||
|
||||
// 设备列表管理
|
||||
final RxList<ScanDeviceInfo> deviceList = <ScanDeviceInfo>[].obs;
|
||||
|
||||
late void Function(ScanDeviceInfo device) onDeviceFound;
|
||||
// 权限状态标识
|
||||
final RxBool permissionsGranted = false.obs;
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
super.onInit();
|
||||
var locationPermission = await AppPermission.requestLocationPermission();
|
||||
if (!locationPermission) {
|
||||
showToast(AppToastMessages.notLocationPermission);
|
||||
return;
|
||||
}
|
||||
var bluetoothPermissions = await AppPermission.requestBluetoothPermissions();
|
||||
if (!bluetoothPermissions) {
|
||||
showToast(AppToastMessages.notBluetoothPermissions);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
// 启动搜索
|
||||
BleService().enableBluetoothSearch(onDeviceFound: _onDeviceFound);
|
||||
// 异步初始化权限
|
||||
_initializePermissions();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -45,23 +38,191 @@ class SearchDeviceController extends BaseController {
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
/// 初始化权限
|
||||
Future<void> _initializePermissions() async {
|
||||
try {
|
||||
AppLogger.highlight('🔐 开始检查权限...');
|
||||
|
||||
// 使用 BleService 中的统一权限申请方法
|
||||
bool hasPermissions = await _checkAndRequestBlePermission();
|
||||
|
||||
if (!hasPermissions) {
|
||||
AppLogger.error('❌ 蓝牙相关权限被拒绝');
|
||||
showToast('蓝牙功能需要相关权限,请在设置中开启');
|
||||
return;
|
||||
}
|
||||
|
||||
AppLogger.highlight('✅ 所有权限已获得');
|
||||
|
||||
// 所有权限都获得了,标记为可以开始搜索
|
||||
permissionsGranted.value = true;
|
||||
AppLogger.highlight('🎉 所有权限已就绪,准备开始搜索');
|
||||
|
||||
// 开始搜索
|
||||
await _startBluetoothSearch();
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error('权限初始化失败', error: e, stackTrace: stackTrace);
|
||||
showToast('权限初始化失败,请重试');
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动蓝牙搜索
|
||||
Future<void> _startBluetoothSearch() async {
|
||||
if (!permissionsGranted.value) {
|
||||
AppLogger.warn('⚠️ 权限未就绪,无法开始搜索');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
AppLogger.highlight('🔍 开始启动蓝牙搜索...');
|
||||
|
||||
// 清空设备列表
|
||||
deviceList.clear();
|
||||
|
||||
// 更新搜索状态
|
||||
isSearching.value = true;
|
||||
|
||||
// 启动搜索
|
||||
BleService().enableBluetoothSearch(onDeviceFound: _onDeviceFound);
|
||||
|
||||
// 定期更新搜索状态
|
||||
_updateSearchingStatus();
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error('启动蓝牙搜索失败', error: e, stackTrace: stackTrace);
|
||||
isSearching.value = false;
|
||||
showToast('启动搜索失败,请刷新并重试');
|
||||
}
|
||||
}
|
||||
|
||||
/// 定期更新搜索状态
|
||||
void _updateSearchingStatus() {
|
||||
// 每秒检查一次搜索状态
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
if (isSearching.value) {
|
||||
bool actuallyScanning = BleService().isScanningNow;
|
||||
if (isSearching.value != actuallyScanning) {
|
||||
isSearching.value = actuallyScanning;
|
||||
if (!actuallyScanning) {
|
||||
AppLogger.highlight('🔍 搜索已停止');
|
||||
isSearching.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果还在搜索,继续检查
|
||||
if (actuallyScanning) {
|
||||
_updateSearchingStatus();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 搜索结果回调
|
||||
void _onDeviceFound(ScanDeviceInfo device) {
|
||||
deviceList.add(device);
|
||||
deviceList.refresh();
|
||||
AppLogger.highlight('📲 发现新设备: ${device.advName}');
|
||||
|
||||
// 检查是否已存在相同设备(避免重复添加)
|
||||
bool exists = deviceList.any((existingDevice) => existingDevice.rawDeviceInfo.device.remoteId == device.rawDeviceInfo.device.remoteId);
|
||||
|
||||
if (!exists) {
|
||||
deviceList.add(device);
|
||||
deviceList.refresh();
|
||||
AppLogger.debug('✅ 设备已添加到列表,当前设备数量: ${deviceList.length},设备信息:${deviceList.toString()}');
|
||||
} else {
|
||||
AppLogger.debug('⚠️ 设备已存在,跳过添加');
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新设备数据
|
||||
Future<void> refreshDevices() async {
|
||||
BleService().stopBluetoothSearch();
|
||||
deviceList.clear();
|
||||
// 模拟网络请求延迟
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
BleService().enableBluetoothSearch(onDeviceFound: _onDeviceFound);
|
||||
AppLogger.highlight('🔄 开始刷新设备列表');
|
||||
|
||||
if (!permissionsGranted.value) {
|
||||
AppLogger.warn('⚠️ 权限未就绪,无法刷新');
|
||||
showToast('请先授权必要的权限');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 停止当前搜索
|
||||
BleService().stopBluetoothSearch();
|
||||
isSearching.value = false;
|
||||
|
||||
// 清空设备列表
|
||||
deviceList.clear();
|
||||
|
||||
// 模拟网络请求延迟
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
// 重新开始搜索
|
||||
await _startBluetoothSearch();
|
||||
|
||||
AppLogger.highlight('✅ 设备列表刷新完成');
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error('刷新设备列表失败', error: e, stackTrace: stackTrace);
|
||||
showToast('刷新失败,请重试');
|
||||
}
|
||||
}
|
||||
|
||||
// 连接设备
|
||||
void connectingDevices() async {
|
||||
Get.toNamed(AppRoutes.confirmPairDevice);
|
||||
void connectingDevices(ScanDeviceInfo device) async {
|
||||
try {
|
||||
// 停止搜索
|
||||
BleService().stopBluetoothSearch();
|
||||
isSearching.value = false;
|
||||
|
||||
// 创建命令
|
||||
BleCmdGetPublicKey getPublicKeyCmd = BleCmdGetPublicKey(lockId: device.advName);
|
||||
|
||||
// 发送命令,允许自动连接和搜索设备
|
||||
GetPublicKeyResponse? publicKeyResponse = await BleService().sendCommand<GetPublicKeyResponse>(
|
||||
command: getPublicKeyCmd,
|
||||
targetDeviceName: device.advName,
|
||||
// 通过名称搜索设备
|
||||
autoConnectIfNeeded: true,
|
||||
);
|
||||
if (publicKeyResponse != null && publicKeyResponse.isSuccess) {
|
||||
AppLogger.info('🎯 获取公钥成功: ${publicKeyResponse.publicKeyHex}');
|
||||
SharedPreferencesUtils.setString(CacheKeys.publicKeyHex, publicKeyResponse.publicKeyHex);
|
||||
BleCmdGetPrivateKey getPrivateKeyCmd = BleCmdGetPrivateKey(
|
||||
lockId: device.advName,
|
||||
keyId: '1',
|
||||
authUserID: '1',
|
||||
nowTime: DateTime
|
||||
.now()
|
||||
.millisecondsSinceEpoch ~/ 1000,
|
||||
publicKey: publicKeyResponse.publicKey,
|
||||
);
|
||||
|
||||
// 发送命令,允许自动连接和搜索设备
|
||||
GetPrivateKeyResponse? privateKeyResponse = await BleService().sendCommand<GetPrivateKeyResponse>(
|
||||
command: getPrivateKeyCmd,
|
||||
targetDeviceName: device.advName,
|
||||
// 通过名称搜索设备
|
||||
autoConnectIfNeeded: true,
|
||||
);
|
||||
if (privateKeyResponse != null && privateKeyResponse.isSuccess) {
|
||||
AppLogger.info('🎯 获取私钥成功: ${privateKeyResponse.commKeyHex}');
|
||||
}
|
||||
} else {
|
||||
AppLogger.warn('⚠️ 命令发送完成,但未收到有效响应');
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.error('连接设备失败', error: e, stackTrace: stackTrace);
|
||||
showToast('连接失败,请重试');
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _checkAndRequestBlePermission() async {
|
||||
var locationPermission = await AppPermission.requestLocationPermission();
|
||||
if (!locationPermission) {
|
||||
showToast(AppToastMessages.notLocationPermission);
|
||||
return false;
|
||||
}
|
||||
var bluetoothPermissions = await AppPermission.requestBluetoothPermissions();
|
||||
if (!bluetoothPermissions) {
|
||||
showToast(AppToastMessages.notBluetoothPermissions);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -174,7 +174,7 @@ class SearchDeviceView extends GetView<SearchDeviceController> {
|
||||
_buildItem({required ScanDeviceInfo device, required int index}) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
controller.connectingDevices();
|
||||
controller.connectingDevices(device);
|
||||
},
|
||||
child: Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 10.w),
|
||||
|
||||
@ -3,20 +3,26 @@ import 'dart:async';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:starwork_flutter/api/api_response.dart';
|
||||
import 'package:starwork_flutter/api/model/common/request/send_validation_code_request.dart';
|
||||
import 'package:starwork_flutter/api/model/starcloud/request/starcloud_create_user_request.dart';
|
||||
import 'package:starwork_flutter/api/model/starcloud/request/starcloud_login_request.dart';
|
||||
import 'package:starwork_flutter/api/model/user/request/validation_code_login.dart';
|
||||
import 'package:starwork_flutter/api/service/common_api_service.dart';
|
||||
import 'package:starwork_flutter/api/service/user_api_service.dart';
|
||||
import 'package:starwork_flutter/api/starcloud/starcloud_api_service.dart';
|
||||
import 'package:starwork_flutter/base/app_logger.dart';
|
||||
import 'package:starwork_flutter/base/base_controller.dart';
|
||||
import 'package:starwork_flutter/common/constant/cache_keys.dart';
|
||||
import 'package:starwork_flutter/common/constant/platform_type.dart';
|
||||
import 'package:starwork_flutter/common/constant/verification_code_channel.dart';
|
||||
import 'package:starwork_flutter/common/constant/verification_code_type.dart';
|
||||
import 'package:starwork_flutter/common/utils/shared_preferences_utils.dart';
|
||||
import 'package:starwork_flutter/flavors.dart';
|
||||
import 'package:starwork_flutter/routes/app_routes.dart';
|
||||
|
||||
class InputVerificationCodeController extends BaseController {
|
||||
final userApi = Get.find<UserApiService>();
|
||||
final commonApi = Get.find<CommonApiService>();
|
||||
final starCloudApi = Get.find<StarCloudApiService>();
|
||||
|
||||
var rawPhone = ''.obs;
|
||||
var phone = ''.obs;
|
||||
@ -71,6 +77,7 @@ class InputVerificationCodeController extends BaseController {
|
||||
|
||||
// 校验验证码
|
||||
void checkVerificationCode(String pin) async {
|
||||
/// 如果是登录页面进入的校验验证码
|
||||
if (previousRoute.value.contains(AppRoutes.login)) {
|
||||
showLoading();
|
||||
var validationCodeLoginResult = await userApi.validationCodeLogin(
|
||||
@ -95,10 +102,6 @@ class InputVerificationCodeController extends BaseController {
|
||||
hideLoading();
|
||||
}
|
||||
|
||||
void _handleSeedVerificationCode({
|
||||
required VerificationCodeType codeType,
|
||||
}) async {}
|
||||
|
||||
// 发送验证码
|
||||
void seedVerificationCode({
|
||||
required VerificationCodeType codeType,
|
||||
@ -134,13 +137,4 @@ class InputVerificationCodeController extends BaseController {
|
||||
}
|
||||
hideLoading();
|
||||
}
|
||||
|
||||
// 跳转页面
|
||||
void _toPage() async {
|
||||
if (previousRoute.value.contains(AppRoutes.login)) {
|
||||
Get.offAllNamed(AppRoutes.main);
|
||||
} else if (previousRoute.value.contains(AppRoutes.forgotPassword)) {
|
||||
Get.toNamed(AppRoutes.setNewPassword);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,7 +82,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||
|
||||
@ -38,6 +38,9 @@ dependencies:
|
||||
super_tooltip: ^2.0.8
|
||||
# 蓝牙库
|
||||
flutter_blue_plus: ^1.35.7
|
||||
# 加解密库
|
||||
crypto: ^3.0.3
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user