From a3ad89d0d4e51dc8b53d24a5b549741ea03fb9a5 Mon Sep 17 00:00:00 2001 From: liyi Date: Mon, 8 Sep 2025 16:49:11 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E5=85=AC=E9=92=A5=E3=80=81=E7=A7=81=E9=92=A5?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/api/api_response.dart | 31 +- lib/api/base_api_service.dart | 20 +- .../starcloud_create_user_request.dart | 16 + .../request/starcloud_login_request.dart | 21 + .../starcloud_create_user_response.dart | 32 + .../response/starcloud_login_response.dart | 30 + lib/api/starcloud/starcloud_api_path.dart | 107 +++ lib/api/starcloud/starcloud_api_service.dart | 42 ++ .../starcloud/starcloud_base_api_service.dart | 120 +++ lib/base/app_initialization.dart | 6 +- lib/ble/ble_config.dart | 17 +- lib/ble/ble_service.dart | 682 ++++++++++++++++-- lib/ble/command/base/base_ble_command.dart | 398 ++++++++++ .../base/base_ble_response_parser.dart | 191 +++++ lib/ble/command/ble_command_manager.dart | 224 ++++++ .../request/ble_cmd_get_private_key.dart | 179 +++++ .../request/ble_cmd_get_public_key.dart | 59 ++ .../ble_cmd_get_private_key_parser.dart | 150 ++++ .../ble_cmd_get_public_key_parser.dart | 131 ++++ .../exceptional/ble_connected_exception.dart | 3 + lib/common/constant/cache_keys.dart | 4 + lib/common/sm4_encipher/sm4.dart | 545 ++++++++++++++ lib/common/sm4_encipher/utils/asn1.dart | 150 ++++ lib/common/sm4_encipher/utils/ec.dart | 248 +++++++ lib/common/sm4_encipher/utils/utils.dart | 46 ++ .../utils/shared_preferences_utils.dart | 7 +- lib/flavors.dart | 2 +- .../search_device_controller.dart | 217 +++++- .../searchDevice/search_device_view.dart | 2 +- .../input_verification_code_controller.dart | 20 +- pubspec.lock | 2 +- pubspec.yaml | 3 + 32 files changed, 3554 insertions(+), 151 deletions(-) create mode 100644 lib/api/model/starcloud/request/starcloud_create_user_request.dart create mode 100644 lib/api/model/starcloud/request/starcloud_login_request.dart create mode 100644 lib/api/model/starcloud/response/starcloud_create_user_response.dart create mode 100644 lib/api/model/starcloud/response/starcloud_login_response.dart create mode 100644 lib/api/starcloud/starcloud_api_path.dart create mode 100644 lib/api/starcloud/starcloud_api_service.dart create mode 100644 lib/api/starcloud/starcloud_base_api_service.dart create mode 100644 lib/ble/command/base/base_ble_command.dart create mode 100644 lib/ble/command/base/base_ble_response_parser.dart create mode 100644 lib/ble/command/ble_command_manager.dart create mode 100644 lib/ble/command/request/ble_cmd_get_private_key.dart create mode 100644 lib/ble/command/request/ble_cmd_get_public_key.dart create mode 100644 lib/ble/command/response/ble_cmd_get_private_key_parser.dart create mode 100644 lib/ble/command/response/ble_cmd_get_public_key_parser.dart create mode 100644 lib/ble/exceptional/ble_connected_exception.dart create mode 100755 lib/common/sm4_encipher/sm4.dart create mode 100755 lib/common/sm4_encipher/utils/asn1.dart create mode 100755 lib/common/sm4_encipher/utils/ec.dart create mode 100755 lib/common/sm4_encipher/utils/utils.dart diff --git a/lib/api/api_response.dart b/lib/api/api_response.dart index 007fa8f..87c063f 100644 --- a/lib/api/api_response.dart +++ b/lib/api/api_response.dart @@ -13,24 +13,23 @@ class ApiResponse { // ✅ 新增:从 JSON 创建 ApiResponse factory ApiResponse.fromJson( - Map json, - T Function(dynamic)? dataFromJson, // 可为空,有些接口 data 是 null - ) { + Map 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( - 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 { // 失败工厂构造 factory ApiResponse.error( - String errorMsg, { - int? errorCode, - T? data, - String? description, - }) { + String errorMsg, { + int? errorCode, + T? data, + String? description, + }) { return ApiResponse( description: description, errorMsg: errorMsg, @@ -62,9 +61,9 @@ class ApiResponse { return errorCode == 0; } - @override String toString() { return 'ApiResponse(description: $description, errorMsg: $errorMsg, data: $data, errorCode: $errorCode)'; } -} \ No newline at end of file + +} diff --git a/lib/api/base_api_service.dart b/lib/api/base_api_service.dart index 77866a4..cd47f53 100644 --- a/lib/api/base_api_service.dart +++ b/lib/api/base_api_service.dart @@ -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}'); } diff --git a/lib/api/model/starcloud/request/starcloud_create_user_request.dart b/lib/api/model/starcloud/request/starcloud_create_user_request.dart new file mode 100644 index 0000000..a427f86 --- /dev/null +++ b/lib/api/model/starcloud/request/starcloud_create_user_request.dart @@ -0,0 +1,16 @@ +class StarCloudCreateUserRequest { + final String clientId; + final String clientSecret; + + StarCloudCreateUserRequest({ + required this.clientId, + required this.clientSecret, + }); + + Map toJson() { + return { + 'clientId': clientId, + 'clientSecret': clientSecret, + }; + } +} diff --git a/lib/api/model/starcloud/request/starcloud_login_request.dart b/lib/api/model/starcloud/request/starcloud_login_request.dart new file mode 100644 index 0000000..b0a7979 --- /dev/null +++ b/lib/api/model/starcloud/request/starcloud_login_request.dart @@ -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 toJson() { + return { + 'clientId': clientId, + 'clientSecret': clientSecret, + 'username': username, + 'password': password, + }; + } +} diff --git a/lib/api/model/starcloud/response/starcloud_create_user_response.dart b/lib/api/model/starcloud/response/starcloud_create_user_response.dart new file mode 100644 index 0000000..b9dbea1 --- /dev/null +++ b/lib/api/model/starcloud/response/starcloud_create_user_response.dart @@ -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 toJson() { + return { + 'username': username, + 'password': password, + 'uid': uid, + }; + } + + factory StarCloudCreateUserResponse.fromJson(Map json) { + return StarCloudCreateUserResponse( + username: json['username'] ?? '', + password: json['password'] ?? '', + uid: json['uid'] ?? -1, + ); + } + + @override + String toString() { + return 'StarCloudCreateUserResponse{username: $username, password: $password, uid: $uid}'; + } +} diff --git a/lib/api/model/starcloud/response/starcloud_login_response.dart b/lib/api/model/starcloud/response/starcloud_login_response.dart new file mode 100644 index 0000000..c204242 --- /dev/null +++ b/lib/api/model/starcloud/response/starcloud_login_response.dart @@ -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 json) { + return StarCloudLoginResponse( + accessToken: json['access_token'] ?? '', + uid: json['uid'] ?? '', + refreshToken: json['refresh_token'] ?? '', + expiresIn: json['expires_in'] ?? '', + ); + } + Map toJson() { + return { + 'accessToken': accessToken, + 'uid': uid, + 'refreshToken': refreshToken, + 'expiresIn': expiresIn, + }; + } +} diff --git a/lib/api/starcloud/starcloud_api_path.dart b/lib/api/starcloud/starcloud_api_path.dart new file mode 100644 index 0000000..dd453a0 --- /dev/null +++ b/lib/api/starcloud/starcloud_api_path.dart @@ -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'; +} diff --git a/lib/api/starcloud/starcloud_api_service.dart b/lib/api/starcloud/starcloud_api_service.dart new file mode 100644 index 0000000..eab1ef0 --- /dev/null +++ b/lib/api/starcloud/starcloud_api_service.dart @@ -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> createUser({ + required StarCloudCreateUserRequest request, + }) { + return _api.makeRequest( + // 通过实例调用 + path: StarCloudApiPath.createUser, + method: HttpConstant.post, + data: request.toJson(), + fromJson: (data) => StarCloudCreateUserResponse.fromJson(data), + ); + } + + // 登录星云 + Future> login({ + required StarCloudLoginRequest request, + }) { + return _api.makeRequest( + // 通过实例调用 + path: StarCloudApiPath.createUser, + method: HttpConstant.post, + data: request.toJson(), + fromJson: (data) => StarCloudLoginResponse.fromJson(data), + ); + } +} diff --git a/lib/api/starcloud/starcloud_base_api_service.dart b/lib/api/starcloud/starcloud_base_api_service.dart new file mode 100644 index 0000000..f33fc36 --- /dev/null +++ b/lib/api/starcloud/starcloud_base_api_service.dart @@ -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> makeRequest({ + required String path, + String method = HttpConstant.post, + dynamic data, + Map? 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; + + // ✅ 直接用 ApiResponse.fromJson 解析整个响应 + final apiResponse = ApiResponse.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'); + } + } +} diff --git a/lib/base/app_initialization.dart b/lib/base/app_initialization.dart index 86c71c3..d517b8d 100644 --- a/lib/base/app_initialization.dart +++ b/lib/base/app_initialization.dart @@ -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())); Get.put(UserApiService(Get.find())); + Get.put(StarCloudApiService(Get.find())); Get.put(LoginController()); Get.put(MainController()); diff --git a/lib/ble/ble_config.dart b/lib/ble/ble_config.dart index b2ffd14..30e1452 100644 --- a/lib/ble/ble_config.dart +++ b/lib/ble/ble_config.dart @@ -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'); } diff --git a/lib/ble/ble_service.dart b/lib/ble/ble_service.dart index 0e31721..72d747b 100644 --- a/lib/ble/ble_service.dart +++ b/lib/ble/ble_service.dart @@ -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 { + final BaseBleCommand command; + final String targetDeviceId; + final String? targetDeviceName; + final Duration timeout; + final Completer 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 _discoveredDevices = {}; + // 使用命令管理器处理数据包 + final BleCommandManager bleCommandManager = BleCommandManager(); + + /// mtu大小 + int _mtu = 23; + /// 用来监听蓝牙适配器状态的订阅流 StreamSubscription? _adapterStateSubscription; /// 用来监听搜索到的设备的订阅流 StreamSubscription>? _scanResultSubscription; + /// 用来监听连接设备时的连接状态 + StreamSubscription? _connectionStateSubscription; + + /// 用来监听mtu的变化 + StreamSubscription? _mtuChangeSubscription; + + /// 用来监听订阅服务的数据 + StreamSubscription>? _characteristicDataSubscription; + + /// 当前连接的设备 + BluetoothDevice? _connectedDevice; + + /// 当前的写入特征值 + BluetoothCharacteristic? _writeCharacteristic; + + /// 当前的订阅特征值 + BluetoothCharacteristic? _subscriptionCharacteristic; + + /// ✨✨✨ 命令响应等待器映射 ✨✨✨ + final Map> _commandResponseWaiters = {}; + + /// ✨✨✨ 命令超时定时器映射 ✨✨✨ + final Map _commandTimeouts = {}; + + /// ✨✨✨ 命令ID到命令键的映射 ✨✨✨ + final Map _commandIdToKeyMap = {}; + + /// 待发送命令队列 (用于自动连接后发送) + final List<_PendingCommand> _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 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 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 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 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 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 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 sendCommand({ + required BaseBleCommand 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> packets = command.build(); + AppLogger.info('📦 命令数据包数量: ${packets.length}'); + + // 4. 设置命令响应等待器 + String commandKey = _generateCommandKey(command); + Completer responseCompleter = Completer(); + _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 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 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 _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 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 _searchForTargetDevice({ + String? targetDeviceId, + String? targetDeviceName, + Duration searchTimeout = const Duration(seconds: 15), + }) async { + Completer searchCompleter = Completer(); + + // 启动搜索 + 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 _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 _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 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 _processPendingCommands() async { + if (_pendingCommands.isEmpty) return; + + AppLogger.info('📦 处理待发送命令: ${_pendingCommands.length}个'); + + List<_PendingCommand> commandsToProcess = List.from(_pendingCommands); + _pendingCommands.clear(); + + for (_PendingCommand 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(BaseBleCommand 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(); } } diff --git a/lib/ble/command/base/base_ble_command.dart b/lib/ble/command/base/base_ble_command.dart new file mode 100644 index 0000000..2001274 --- /dev/null +++ b/lib/ble/command/base/base_ble_command.dart @@ -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 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 { + /// 包头固定值: 0XEF01EE02 + static const List 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 buildPacketHeader({ + required int packetType, + required int packetSequence, + int encryptType = ENCRYPT_TYPE_PLAIN, + required int encryptedDataLength, + required int originalDataLength, + }) { + List 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 buildPacketTail(List packetData) { + int crc16 = calculateCRC16Kermit(packetData); + + // CRC16校验位 (2字节,大端序) + List tail = [ + (crc16 >> 8) & 0xFF, // 高字节 + crc16 & 0xFF, // 低字节 + ]; + + return tail; + } + + /// ✨✨✨ 计算CRC16-KERMIT校验 ✨✨✨ + static int calculateCRC16Kermit(List 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> splitIntoPackets({ + required List originalData, + int encryptType = ENCRYPT_TYPE_PLAIN, + }) { + List> 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 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 header = buildPacketHeader( + packetType: PACKET_TYPE_REQUEST, + packetSequence: packetSequence, + encryptType: encryptType, + encryptedDataLength: encryptedDataLength, + originalDataLength: originalDataLength, + ); + + // 组装完整包 (包头 + 数据) + List packetWithHeader = []; + packetWithHeader.addAll(header); + packetWithHeader.addAll(currentPacketData); + + // 计算并添加包尾 + List tail = buildPacketTail(packetWithHeader); + + // 完整的数据包 + List completePacket = []; + completePacket.addAll(packetWithHeader); + completePacket.addAll(tail); + + packets.add(completePacket); + } + + return packets; + } + + /// ✨✨✨ 抽象方法 - 子类需要实现具体的数据构建逻辑 ✨✨✨ + List buildData(); + + /// ✨✨✨ 获取当前命令的加密类型 - 子类可以重写此方法 ✨✨✨ + /// 默认返回明文类型,子类可以根据需要重写 + int getEncryptType() { + return ENCRYPT_TYPE_PLAIN; + } + + /// ✨✨✨ 构建完整的命令包 ✨✨✨ + /// 使用子类定义的加密类型自动组装数据包 + List> build() { + // 获取子类实现的数据 + List commandData = buildData(); + + // 使用子类定义的加密类型 + int encryptType = getEncryptType(); + + // 分包处理 + List> packets = splitIntoPackets( + originalData: commandData, + encryptType: encryptType, + ); + + return packets; + } + + /// ✨✨✨ 解析接收到的数据包 ✨✨✨ + /// [rawData] 接收到的原始数据包字节 + /// 返回解析结果,包含包的各个字段信息 + static ParsedPacket parsePacket(List 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 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 data = rawData.sublist(offset, offset + actualDataLength); + offset += actualDataLength; + + // // 9. 提取CRC16校验位 (大端序) + int crc16 = (rawData[offset] << 8) | rawData[offset + 1]; + + // + // // 10. 验证CRC16校验 + // List 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 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 getFixedLengthList(List data, int length) { + for (int i = 0; i < length; i++) { + data.add(0); + } + return data; + } +} diff --git a/lib/ble/command/base/base_ble_response_parser.dart b/lib/ble/command/base/base_ble_response_parser.dart new file mode 100644 index 0000000..db3c7be --- /dev/null +++ b/lib/ble/command/base/base_ble_response_parser.dart @@ -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 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 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 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 extractBytes(List 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 data, int offset, int length, {bool removeNullTerminator = true}) { + List 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 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] 字节数组,解析失败返回空列表 + static List hexToBytes(String hex, {String? separator}) { + if (hex.isEmpty) return []; + + // 如果未指定分隔符,使用正则自动分割(支持空格、冒号、短横、逗号等) + List 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 = []; + 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; + } +} \ No newline at end of file diff --git a/lib/ble/command/ble_command_manager.dart b/lib/ble/command/ble_command_manager.dart new file mode 100644 index 0000000..df6e734 --- /dev/null +++ b/lib/ble/command/ble_command_manager.dart @@ -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 _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 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 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 _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 _decryptAES128(List encryptedData) { + // TODO: 实现AES128解密逻辑 + + return encryptedData; + } + + /// ✨✨✨ SM4解密 ✨✨✨ + /// [encryptedData] 加密数据 + /// 返回解密后的数据 + List _decryptSM4(List encryptedData) { + return encryptedData; + } + + /// ✨✨✨ 根据命令ID获取解析器 ✨✨✨ + BaseBleResponseParser? getParser(int commandId) { + return _parsers[commandId]; + } + + /// ✨✨✨ 获取所有已注册的命令ID ✨✨✨ + List getRegisteredCommandIds() { + return _parsers.keys.toList(); + } + + /// ✨✨✨ 获取所有已注册的解析器信息 ✨✨✨ + Map getRegisteredParsersInfo() { + return _parsers.map((commandId, parser) => MapEntry(commandId, parser.commandName)); + } + + /// ✨✨✨ 记录未知数据包信息 ✨✨✨ + void _logUnknownPacket(List 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(); + } +} diff --git a/lib/ble/command/request/ble_cmd_get_private_key.dart b/lib/ble/command/request/ble_cmd_get_private_key.dart new file mode 100644 index 0000000..682a166 --- /dev/null +++ b/lib/ble/command/request/ble_cmd_get_private_key.dart @@ -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 { + final String lockId; + final String keyId; + final String authUserID; + final List 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 buildData() { + // final List buffer = []; + // + // // 1. 写入 CmdID (2 字节,大端序) + // buffer.add((cmdId >> 8) & 0xFF); // 高字节 + // buffer.add(cmdId & 0xFF); // 低字节 + // + // // 2. 处理 LockID:先转换实际字符,再检查是否需要填充到40字节 + // List lockIdBytes = List.from(lockId.codeUnits); // 创建可修改的副本 + // List keyIdBytes = List.from(keyId.codeUnits); // 创建可修改的副本 + // List 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 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 data = []; + List ebcData = []; + + // 指令类型 + 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 authCodeData = []; + + //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; + } +} diff --git a/lib/ble/command/request/ble_cmd_get_public_key.dart b/lib/ble/command/request/ble_cmd_get_public_key.dart new file mode 100644 index 0000000..7924b33 --- /dev/null +++ b/lib/ble/command/request/ble_cmd_get_public_key.dart @@ -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 { + 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 buildData() { + final List buffer = []; + + // 1. 写入 CmdID (2 字节,大端序) + buffer.add((cmdId >> 8) & 0xFF); // 高字节 + buffer.add(cmdId & 0xFF); // 低字节 + + // 2. 处理 LockID:先转换实际字符,再检查是否需要填充到40字节 + List 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; + } +} diff --git a/lib/ble/command/response/ble_cmd_get_private_key_parser.dart b/lib/ble/command/response/ble_cmd_get_private_key_parser.dart new file mode 100644 index 0000000..284c62a --- /dev/null +++ b/lib/ble/command/response/ble_cmd_get_private_key_parser.dart @@ -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 commKey; + final String commKeyHex; + final List 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 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 commKey = []; + if (offset < rawResponseData.length) { + commKey = BaseBleResponseParser.extractBytes(rawResponseData, offset, 16); + } + offset += 16; + String commKeyHex = BaseBleResponseParser.bytesToHex(commKey, separator: ''); + + List signKey = []; + if (offset < rawResponseData.length) { + signKey = BaseBleResponseParser.extractBytes(rawResponseData, offset, 16); + } + offset += 16; + String signKeyHex = BaseBleResponseParser.bytesToHex(signKey, separator: ''); + + List 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 rawPacketData) { + BleCmdGetPrivateKeyParser parser = BleCmdGetPrivateKeyParser(); + return parser.handleResponse(rawPacketData) as GetPrivateKeyResponse?; + } +} diff --git a/lib/ble/command/response/ble_cmd_get_public_key_parser.dart b/lib/ble/command/response/ble_cmd_get_public_key_parser.dart new file mode 100644 index 0000000..3aadb10 --- /dev/null +++ b/lib/ble/command/response/ble_cmd_get_public_key_parser.dart @@ -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 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 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 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 rawPacketData) { + BleCmdGetPublicKeyParser parser = BleCmdGetPublicKeyParser(); + return parser.handleResponse(rawPacketData) as GetPublicKeyResponse?; + } +} diff --git a/lib/ble/exceptional/ble_connected_exception.dart b/lib/ble/exceptional/ble_connected_exception.dart new file mode 100644 index 0000000..9030b99 --- /dev/null +++ b/lib/ble/exceptional/ble_connected_exception.dart @@ -0,0 +1,3 @@ +class BleConnectedException implements Exception{ + +} \ No newline at end of file diff --git a/lib/common/constant/cache_keys.dart b/lib/common/constant/cache_keys.dart index 1f3c81a..0858bd9 100644 --- a/lib/common/constant/cache_keys.dart +++ b/lib/common/constant/cache_keys.dart @@ -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'; + } diff --git a/lib/common/sm4_encipher/sm4.dart b/lib/common/sm4_encipher/sm4.dart new file mode 100755 index 0000000..c875bbb --- /dev/null +++ b/lib/common/sm4_encipher/sm4.dart @@ -0,0 +1,545 @@ +import 'dart:convert'; +import 'utils/utils.dart'; + +enum SM4CryptoMode { ECB, CBC } + +class SM4 { + static const List 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 FK = [0xA3B1BAC6, 0x56AA3350, 0x677D9197, 0xB27022DC]; + + static const List 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.filled(32, 0); + static final _decryptKey = List.filled(32, 0); + + static int _readUint32BE(List 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 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 a = List.filled(4, 0); + List b = List.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 keyBytes = List.filled(4, 0); + List sboxBytes = List.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 key) { + List keyBytes = key; + List intermediateKeys = List.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 sk, List input, List output) { + int i = 0; + List ulbuf = List.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 _padding(List input, int mode) { + final int padLen = blockSize - (input.length % blockSize); + + if (mode == SM4_ENCRYPT) { + final paddedList = List.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 _crypto( + List data, int flag, SM4CryptoMode mode, String? iv) { + late List 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 output = []; + + for (int offset = 0; offset < length; offset += blockSize) { + final outData = List.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 _utf8ToArray(String str) { + return utf8.encode(str); + } + + // /// auto add 0x00 + static List _autoAddZero(List list) { + /// supplementary list + List supplementList = List.filled(16, 0x00); + + /// complete list + List completeList = [...list, ...supplementList].sublist(0, 16); + return completeList; + } + + /// hex byte list to hex string + static String _listToHex(List 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 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 encrypt(List data, + {List? key, SM4CryptoMode mode = SM4CryptoMode.ECB, String? iv}) { + if (key != null) setKey(key); + List input = data; + List 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 input = SMUtils.hexStringToBytes(cipherText); + // List output = _crypto(input, SM4_DECRYPT, mode, iv); + // return utf8.decode(output); + // } + + static decrypt(List data, + {List? key, SM4CryptoMode mode = SM4CryptoMode.ECB, String? iv}) { + if (key != null) setKey(key); + List input = data; + List output = _crypto(input, SM4_DECRYPT, mode, iv); + return output; + } +} diff --git a/lib/common/sm4_encipher/utils/asn1.dart b/lib/common/sm4_encipher/utils/asn1.dart new file mode 100755 index 0000000..144578a --- /dev/null +++ b/lib/common/sm4_encipher/utils/asn1.dart @@ -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 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}; + } +} \ No newline at end of file diff --git a/lib/common/sm4_encipher/utils/ec.dart b/lib/common/sm4_encipher/utils/ec.dart new file mode 100755 index 0000000..66d137c --- /dev/null +++ b/lib/common/sm4_encipher/utils/ec.dart @@ -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; +} + diff --git a/lib/common/sm4_encipher/utils/utils.dart b/lib/common/sm4_encipher/utils/utils.dart new file mode 100755 index 0000000..5ed948d --- /dev/null +++ b/lib/common/sm4_encipher/utils/utils.dart @@ -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 bytes) { + final buffer = StringBuffer(); + for (final byte in bytes) { + buffer.write(byte.toRadixString(16).padLeft(2, '0')); + } + return buffer.toString(); + } + + static List hexStringToBytes(String hexString) { + final length = hexString.length ~/ 2; + final bytes = List.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 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(); + } +} diff --git a/lib/common/utils/shared_preferences_utils.dart b/lib/common/utils/shared_preferences_utils.dart index 5c8f2c5..90b2323 100644 --- a/lib/common/utils/shared_preferences_utils.dart +++ b/lib/common/utils/shared_preferences_utils.dart @@ -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 delString(String key) async { + return _prefs?.remove(key) ?? Future.value(false); + } + // bool static Future setBool(String key, dynamic value) async { _prefs?.setBool(key, value); diff --git a/lib/flavors.dart b/lib/flavors.dart index 7b7bc3e..8aabe20 100644 --- a/lib/flavors.dart +++ b/lib/flavors.dart @@ -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'; } diff --git a/lib/views/device/searchDevice/search_device_controller.dart b/lib/views/device/searchDevice/search_device_controller.dart index 731edc1..19f52c8 100644 --- a/lib/views/device/searchDevice/search_device_controller.dart +++ b/lib/views/device/searchDevice/search_device_controller.dart @@ -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 deviceList = [].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 _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 _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 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( + 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( + 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 _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; } } diff --git a/lib/views/device/searchDevice/search_device_view.dart b/lib/views/device/searchDevice/search_device_view.dart index 79876ea..8e7a2f5 100644 --- a/lib/views/device/searchDevice/search_device_view.dart +++ b/lib/views/device/searchDevice/search_device_view.dart @@ -174,7 +174,7 @@ class SearchDeviceView extends GetView { _buildItem({required ScanDeviceInfo device, required int index}) { return GestureDetector( onTap: () { - controller.connectingDevices(); + controller.connectingDevices(device); }, child: Container( margin: EdgeInsets.symmetric(horizontal: 10.w), diff --git a/lib/views/login/inputVerificationCode/input_verification_code_controller.dart b/lib/views/login/inputVerificationCode/input_verification_code_controller.dart index a5f8cf1..887b568 100644 --- a/lib/views/login/inputVerificationCode/input_verification_code_controller.dart +++ b/lib/views/login/inputVerificationCode/input_verification_code_controller.dart @@ -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(); final commonApi = Get.find(); + final starCloudApi = Get.find(); 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); - } - } } diff --git a/pubspec.lock b/pubspec.lock index fde5355..6b9ce98 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -82,7 +82,7 @@ packages: source: hosted version: "1.18.0" crypto: - dependency: transitive + dependency: "direct main" description: name: crypto sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab diff --git a/pubspec.yaml b/pubspec.yaml index 4833b06..429d4b2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,9 @@ dependencies: super_tooltip: ^2.0.8 # 蓝牙库 flutter_blue_plus: ^1.35.7 + # 加解密库 + crypto: ^3.0.3 + dev_dependencies: flutter_test: