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