fix: 增加获取设备公钥、私钥接口

This commit is contained in:
liyi 2025-09-08 16:49:11 +08:00
parent 799479447c
commit a3ad89d0d4
32 changed files with 3554 additions and 151 deletions

View File

@ -13,24 +13,23 @@ class ApiResponse<T> {
// JSON ApiResponse
factory ApiResponse.fromJson(
Map<String, dynamic> json,
T Function(dynamic)? dataFromJson, // data null
) {
Map<String, dynamic> json,
T Function(dynamic)? dataFromJson, // data null
) {
final dataJson = json['data'];
final T? parsedData = dataJson != null && dataFromJson != null
? dataFromJson(dataJson)
: null;
final T? parsedData = dataJson != null && dataFromJson != null ? dataFromJson(dataJson) : null;
return ApiResponse<T>(
errorCode: json['errorCode'],
errorMsg: json['errorMsg'],
errorCode: json['errorCode'] == null ? json['errcode'] : json['errorCode'],
errorMsg: json['errorMsg'] == null ? json['errmsg'] : json['errorMsg'],
description: json['description'],
data: parsedData,
);
}
//
factory ApiResponse.success(T data, {
factory ApiResponse.success(
T data, {
String? errorMsg,
int? errorCode,
String? description,
@ -45,11 +44,11 @@ class ApiResponse<T> {
//
factory ApiResponse.error(
String errorMsg, {
int? errorCode,
T? data,
String? description,
}) {
String errorMsg, {
int? errorCode,
T? data,
String? description,
}) {
return ApiResponse<T>(
description: description,
errorMsg: errorMsg,
@ -62,9 +61,9 @@ class ApiResponse<T> {
return errorCode == 0;
}
@override
String toString() {
return 'ApiResponse(description: $description, errorMsg: $errorMsg, data: $data, errorCode: $errorCode)';
}
}
}

View File

@ -12,8 +12,6 @@ class BaseApiService {
dio.options.baseUrl = F.apiHost;
dio.options.connectTimeout = const Duration(seconds: 30);
dio.options.receiveTimeout = const Duration(seconds: 30);
}
///
@ -28,8 +26,7 @@ class BaseApiService {
// 🔍
if (kDebugMode) {
final uri = Uri.parse('${dio.options.baseUrl}$path');
final queryString =
queryParameters != null ? '?${uri.queryParameters.toString()}' : '';
final queryString = queryParameters != null ? '?${uri.queryParameters.toString()}' : '';
debugPrint('🟦 API Request: $method ${uri.toString()}$queryString');
if (data != null) {
debugPrint('🟦 Request Body: $data');
@ -43,20 +40,16 @@ class BaseApiService {
response = await dio.get(path, queryParameters: queryParameters);
break;
case HttpConstant.post:
response = await dio.post(path,
data: data, queryParameters: queryParameters);
response = await dio.post(path, data: data, queryParameters: queryParameters);
break;
case HttpConstant.put:
response =
await dio.put(path, data: data, queryParameters: queryParameters);
response = await dio.put(path, data: data, queryParameters: queryParameters);
break;
case HttpConstant.delete:
response = await dio.delete(path,
data: data, queryParameters: queryParameters);
response = await dio.delete(path, data: data, queryParameters: queryParameters);
break;
case HttpConstant.patch:
response = await dio.patch(path,
data: data, queryParameters: queryParameters);
response = await dio.patch(path, data: data, queryParameters: queryParameters);
break;
default:
return ApiResponse.error('Unsupported method: $method');
@ -64,8 +57,7 @@ class BaseApiService {
//
if (kDebugMode) {
debugPrint(
'🟩 API Response [${response.statusCode}] ${response.requestOptions.path}');
debugPrint('🟩 API Response [${response.statusCode}] ${response.requestOptions.path}');
debugPrint('🟩 Response Data: ${response.data}');
}

View File

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

View 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,
};
}
}

View File

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

View File

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

View 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';
}

View 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),
);
}
}

View 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');
}
}
}

View File

@ -8,6 +8,8 @@ import 'package:get/get.dart';
import 'package:starwork_flutter/api/base_api_service.dart';
import 'package:starwork_flutter/api/service/common_api_service.dart';
import 'package:starwork_flutter/api/service/user_api_service.dart';
import 'package:starwork_flutter/api/starcloud/starcloud_api_service.dart';
import 'package:starwork_flutter/api/starcloud/starcloud_base_api_service.dart';
import 'package:starwork_flutter/common/utils/shared_preferences_utils.dart';
import 'package:starwork_flutter/flavors.dart';
import 'package:starwork_flutter/views/login/login_controller.dart';
@ -20,10 +22,12 @@ class AppInitialization {
setSystemStatusBar();
await SharedPreferencesUtils.init();
initEasyLoading();
Get.put(BaseApiService());
Get.put(StarCloudBaseApiService());
Get.put(CommonApiService(Get.find<BaseApiService>()));
Get.put(UserApiService(Get.find<BaseApiService>()));
Get.put(StarCloudApiService(Get.find<StarCloudBaseApiService>()));
Get.put(LoginController());
Get.put(MainController());

View File

@ -1,13 +1,12 @@
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
class BleConfig {
//
BleConfig._() {
//
//
}
//
static final BleConfig _instance = BleConfig._();
static Guid serviceId = Guid('fff0');
// 访
factory BleConfig() => _instance;
// id
static Guid characteristicIdSubscription = Guid('fff1');
// id
static Guid characteristicIdWrite = Guid('fff2');
}

View File

@ -2,9 +2,34 @@ import 'dart:async';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:starwork_flutter/base/app_logger.dart';
import 'package:starwork_flutter/ble/ble_config.dart';
import 'package:starwork_flutter/ble/command/base/base_ble_command.dart';
import 'package:starwork_flutter/ble/command/ble_command_manager.dart';
import 'package:starwork_flutter/ble/command/request/ble_cmd_get_public_key.dart';
import 'package:starwork_flutter/ble/command/response/ble_cmd_get_public_key_parser.dart';
import 'package:starwork_flutter/ble/model/scan_device_info.dart';
import 'package:starwork_flutter/common/constant/device_type.dart';
///
class _PendingCommand<T> {
final BaseBleCommand<T> command;
final String targetDeviceId;
final String? targetDeviceName;
final Duration timeout;
final Completer<T?> completer;
final DateTime createdAt;
final int commandId; // ID字段
_PendingCommand({
required this.command,
required this.targetDeviceId,
this.targetDeviceName,
required this.timeout,
required this.completer,
required this.commandId, // ID参数
}) : createdAt = DateTime.now();
}
class BleService {
//
BleService._() {
@ -22,15 +47,57 @@ class BleService {
///
final Map<String, ScanResult> _discoveredDevices = {};
// 使
final BleCommandManager bleCommandManager = BleCommandManager();
/// mtu大小
int _mtu = 23;
///
StreamSubscription<BluetoothAdapterState>? _adapterStateSubscription;
///
StreamSubscription<List<ScanResult>>? _scanResultSubscription;
///
StreamSubscription<BluetoothConnectionState>? _connectionStateSubscription;
/// mtu的变化
StreamSubscription<int>? _mtuChangeSubscription;
///
StreamSubscription<List<int>>? _characteristicDataSubscription;
///
BluetoothDevice? _connectedDevice;
///
BluetoothCharacteristic? _writeCharacteristic;
///
BluetoothCharacteristic? _subscriptionCharacteristic;
///
final Map<String, Completer<dynamic>> _commandResponseWaiters = {};
///
final Map<String, Timer> _commandTimeouts = {};
/// ID到命令键的映射
final Map<int, String> _commandIdToKeyMap = {};
/// ()
final List<_PendingCommand<dynamic>> _pendingCommands = [];
///
Timer? _scanningMonitorTimer;
//
BluetoothAdapterState _bluetoothAdapterState = BluetoothAdapterState.unknown;
//
BluetoothConnectionState _bluetoothConnectionState = BluetoothConnectionState.disconnected;
///
BluetoothAdapterState get bluetoothAdapterState => _bluetoothAdapterState;
@ -53,90 +120,308 @@ class BleService {
///
void enableBluetoothSearch({
DeviceType deviceType = DeviceType.all,
Duration searchTime = const Duration(seconds: 30),
Duration searchTime = const Duration(seconds: 15),
required void Function(ScanDeviceInfo device) onDeviceFound,
}) async {
//
var isScanningNow = FlutterBluePlus.isScanningNow;
if (isScanningNow) {
AppLogger.warn('正处于搜索状态,请勿重复搜索');
return;
//
AppLogger.highlight('🔍 当前蓝牙适配器状态: $_bluetoothAdapterState');
// unknown
if (_bluetoothAdapterState == BluetoothAdapterState.unknown) {
AppLogger.highlight('⏳ 等待蓝牙适配器状态更新...');
await Future.delayed(const Duration(milliseconds: 500));
AppLogger.highlight('🔍 等待后蓝牙适配器状态: $_bluetoothAdapterState');
}
if (_bluetoothConnectionState == BluetoothConnectionState.connected) {
//
List<BluetoothDevice> devs = FlutterBluePlus.connectedDevices;
for (var d in devs) {
d.disconnect();
}
}
if (_bluetoothAdapterState == BluetoothAdapterState.on) {
FlutterBluePlus.startScan(timeout: searchTime);
AppLogger.highlight('🚀 开始启动蓝牙扫描,搜索时长: ${searchTime.inSeconds}');
///
_scanResultSubscription?.cancel();
_discoveredDevices.clear();
try {
FlutterBluePlus.startScan(timeout: searchTime);
///
_scanResultSubscription = FlutterBluePlus.onScanResults.listen(
(List<ScanResult> results) {
for (var result in results) {
var device = result.device;
final deviceId = device.remoteId.toString();
final platformName = device.platformName;
var serviceUuids = result.advertisementData.serviceUuids;
///
_scanResultSubscription?.cancel();
_discoveredDevices.clear();
//
if (!_discoveredDevices.containsKey(deviceId) && platformName.isNotEmpty) {
_discoveredDevices[deviceId] = result;
bool pairStatus = false;
bool hasNewEvent = false;
for (var uuid in serviceUuids) {
String uuidStr = uuid.toString().replaceAll('-', '');
if (uuidStr.length == 8) {
var pairStatusStr = uuidStr.substring(4, 6);
var hasNewEventStr = uuidStr.substring(6, 8);
pairStatus = pairStatusStr == '01';
hasNewEvent = hasNewEventStr == '01';
var scanDeviceInfo = ScanDeviceInfo(
isBinding: pairStatus,
advName: device.advName,
rawDeviceInfo: result,
hasNewEvent: hasNewEvent,
);
onDeviceFound.call(scanDeviceInfo);
} else if (uuidStr.length == 32) {
var pairStatusStr = uuidStr.substring(26, 28);
pairStatus = pairStatusStr == '00'; // 4534
int statusValue = int.parse(pairStatusStr, radix: 16);
// byte01
int byte0 = (statusValue >> 0) & 0x01; //
// byte12
int byte1 = (statusValue >> 1) & 0x01; //
//
pairStatus = (byte0 == 1);
//
hasNewEvent = (byte1 == 1);
var scanDeviceInfo = ScanDeviceInfo(
isBinding: pairStatus,
advName: device.advName,
rawDeviceInfo: result,
hasNewEvent: hasNewEvent,
);
onDeviceFound.call(scanDeviceInfo);
///
_scanResultSubscription = FlutterBluePlus.onScanResults.listen(
(List<ScanResult> results) {
for (var result in results) {
var device = result.device;
final deviceId = device.remoteId.toString();
final platformName = device.platformName;
var serviceUuids = result.advertisementData.serviceUuids;
//
if (!_discoveredDevices.containsKey(deviceId) && platformName.isNotEmpty) {
_discoveredDevices[deviceId] = result;
bool pairStatus = false;
bool hasNewEvent = false;
for (var uuid in serviceUuids) {
String uuidStr = uuid.toString().replaceAll('-', '');
if (uuidStr.length == 8) {
var pairStatusStr = uuidStr.substring(4, 6);
var hasNewEventStr = uuidStr.substring(6, 8);
pairStatus = pairStatusStr == '01';
hasNewEvent = hasNewEventStr == '01';
var scanDeviceInfo = ScanDeviceInfo(
isBinding: pairStatus,
advName: device.platformName,
rawDeviceInfo: result,
hasNewEvent: hasNewEvent,
);
onDeviceFound.call(scanDeviceInfo);
} else if (uuidStr.length == 32) {
var pairStatusStr = uuidStr.substring(26, 28);
pairStatus = pairStatusStr == '00'; // 4534
int statusValue = int.parse(pairStatusStr, radix: 16);
// byte01
int byte0 = (statusValue >> 0) & 0x01; //
// byte12
int byte1 = (statusValue >> 1) & 0x01; //
//
pairStatus = (byte0 == 1);
//
hasNewEvent = (byte1 == 1);
var scanDeviceInfo = ScanDeviceInfo(
isBinding: pairStatus,
advName: device.platformName,
rawDeviceInfo: result,
hasNewEvent: hasNewEvent,
);
onDeviceFound.call(scanDeviceInfo);
}
}
} else {
// RSSI
_discoveredDevices[deviceId] = result;
}
} else {
// RSSI
_discoveredDevices[deviceId] = result;
}
}
},
onError: (e) => AppLogger.error('搜索设备时遇到错误:' + e),
);
},
onError: (e) => AppLogger.error('搜索设备时遇到错误:' + e),
);
AppLogger.highlight('✅ 蓝牙搜索已成功启动');
//
_monitorScanningState();
} catch (e, stackTrace) {
AppLogger.error('启动蓝牙搜索失败', error: e, stackTrace: stackTrace);
}
} else {
AppLogger.error('❌ 蓝牙适配器未开启,当前状态: $_bluetoothAdapterState');
}
}
///
void _monitorScanningState() {
//
_scanningMonitorTimer?.cancel();
_scanningMonitorTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
bool currentlyScanning = FlutterBluePlus.isScanningNow;
if (!currentlyScanning) {
AppLogger.highlight('🔍 搜索已停止 (监控器检测)');
timer.cancel();
_scanningMonitorTimer = null;
}
});
}
///
void stopBluetoothSearch() async {
var isScanningNow = FlutterBluePlus.isScanningNow;
if (isScanningNow) {
FlutterBluePlus.stopScan();
}
///
_discoveredDevices.clear();
//
_scanningMonitorTimer?.cancel();
_scanningMonitorTimer = null;
///
_discoveredDevices.clear();
AppLogger.highlight('🛑 蓝牙搜索已停止');
}
void seedData({
required BaseBleCommand command,
}) async {
//
List<BluetoothDevice> devs = FlutterBluePlus.connectedDevices;
for (var d in devs) {
d.disconnect();
}
}
///
void _handleListenBluetoothConnectionState(BluetoothConnectionState state) {
_bluetoothConnectionState = state;
}
/// mtu变化
void _handleListenMtuChange(int mtu) {
_mtu = mtu;
BaseBleCommand.MAX_PACKET_DATA_SIZE = mtu;
}
///
void _handleListenCharacteristicData(List<int> data) {
AppLogger.highlight('✨✨✨ 📨 收到订阅数据:$data (长度: ${data.length}) ✨✨✨');
//
if (data.isNotEmpty) {
dynamic result = bleCommandManager.handleResponse(data);
if (result != null) {
//
_triggerCommandResponseWaiters(result);
//
// if (result is GetPublicKeyResponse) {
// GetPublicKeyResponse response = result;
// }
} else {
AppLogger.warn('⚠️ 数据包解析失败或不匹配任何命令');
}
} else {
AppLogger.warn('✨✨✨ ⚠️ 收到空数据 ✨✨✨');
}
}
///
void _triggerCommandResponseWaiters(dynamic response) {
//
List<String> completedKeys = [];
// GetPublicKeyResponse类型ID进行匹配
int commandId = response.commandId;
String? commandKey = _commandIdToKeyMap[commandId];
if (commandKey != null) {
//
Completer? completer = _commandResponseWaiters[commandKey];
if (completer != null && !completer.isCompleted) {
AppLogger.debug('🔔 精确匹配命令响应: 命令ID=0x${commandId.toRadixString(16).padLeft(4, '0')}, 键=$commandKey');
completer.complete(response);
completedKeys.add(commandKey);
//
_cleanupCommandWaiter(commandKey);
_commandIdToKeyMap.remove(commandId);
return;
}
}
// 退
_commandResponseWaiters.forEach((key, completer) {
if (!completer.isCompleted) {
AppLogger.debug('🔔 触发命令响应(回退模式): $key');
completer.complete(response);
completedKeys.add(key);
}
});
//
for (String key in completedKeys) {
_cleanupCommandWaiter(key);
}
}
///
Future<T?> sendCommand<T>({
required BaseBleCommand<T> command,
String? targetDeviceId,
String? targetDeviceName,
Duration timeout = const Duration(seconds: 10),
bool autoConnectIfNeeded = true,
Duration searchTimeoutIfNeeded = const Duration(seconds: 15),
}) async {
AppLogger.highlight('✨✨✨ 🚀 开始发送蓝牙命令: ${command.runtimeType} ✨✨✨');
try {
// 1.
bool isConnected = await _ensureDeviceConnected(
targetDeviceId: targetDeviceId,
targetDeviceName: targetDeviceName,
autoConnect: autoConnectIfNeeded,
searchTimeout: searchTimeoutIfNeeded,
);
if (!isConnected) {
throw Exception('设备未连接,无法发送命令');
}
// 2.
if (_writeCharacteristic == null) {
throw Exception('写入特征值未初始化');
}
// 3.
List<List<int>> packets = command.build();
AppLogger.info('📦 命令数据包数量: ${packets.length}');
// 4.
String commandKey = _generateCommandKey(command);
Completer<T?> responseCompleter = Completer<T?>();
_commandResponseWaiters[commandKey] = responseCompleter;
// 5. cmdId静态字段
try {
// cmdId字段
if (command is BleCmdGetPublicKey) {
int commandId = BleCmdGetPublicKey.cmdId;
_commandIdToKeyMap[commandId] = commandKey;
AppLogger.debug('📝 命令ID映射: 0x${commandId.toRadixString(16).padLeft(4, '0')} -> $commandKey');
}
//
} catch (e) {
AppLogger.warn('⚠️ 无法获取命令ID: $e');
}
// 6.
Timer timeoutTimer = Timer(timeout, () {
if (!responseCompleter.isCompleted) {
_cleanupCommandWaiter(commandKey);
responseCompleter.completeError(TimeoutException('命令响应超时', timeout));
}
});
_commandTimeouts[commandKey] = timeoutTimer;
// 7.
for (int i = 0; i < packets.length; i++) {
List<int> packet = packets[i];
AppLogger.debug('📤 发送第${i + 1}个数据包,数据包: (${packet.toString()},长度:${packet.length}字节)');
await _writeCharacteristic!.write(packet, withoutResponse: false);
//
// if (i < packets.length - 1) {
// await Future.delayed(const Duration(milliseconds: 50));
// }
}
AppLogger.info('✅ 所有数据包发送完成,等待应答...');
// 8.
T? response = await responseCompleter.future;
AppLogger.highlight('✨✨✨ ✅ 命令发送成功,收到应答: $response ✨✨✨');
return response;
} catch (e, stackTrace) {
AppLogger.error('❌ 发送蓝牙命令失败', error: e, stackTrace: stackTrace);
rethrow;
}
}
@ -144,6 +429,273 @@ class BleService {
///
_adapterStateSubscription?.cancel();
_scanResultSubscription?.cancel();
_connectionStateSubscription?.cancel();
_mtuChangeSubscription?.cancel();
_characteristicDataSubscription?.cancel();
///
_scanningMonitorTimer?.cancel();
_scanningMonitorTimer = null;
///
for (String key in _commandResponseWaiters.keys) {
_cleanupCommandWaiter(key);
}
_commandResponseWaiters.clear();
_commandTimeouts.clear();
_commandIdToKeyMap.clear(); // ID映射
///
for (_PendingCommand<dynamic> pendingCmd in _pendingCommands) {
if (!pendingCmd.completer.isCompleted) {
pendingCmd.completer.completeError(Exception('服务已关闭'));
}
}
_pendingCommands.clear();
///
_bluetoothAdapterState = BluetoothAdapterState.unknown;
_connectedDevice = null;
_writeCharacteristic = null;
_subscriptionCharacteristic = null;
AppLogger.highlight('🗑️ BleService 资源已清理');
}
///
Future<bool> _ensureDeviceConnected({
String? targetDeviceId,
String? targetDeviceName,
bool autoConnect = true,
Duration searchTimeout = const Duration(seconds: 15),
}) async {
// 1.
if (_connectedDevice != null && _bluetoothConnectionState == BluetoothConnectionState.connected) {
// ID
if (targetDeviceId != null && _connectedDevice!.remoteId.toString() != targetDeviceId) {
AppLogger.info('🔄 当前连接的设备与目标不匹配,需要切换连接');
await _connectedDevice!.disconnect();
_connectedDevice = null;
_writeCharacteristic = null;
_subscriptionCharacteristic = null;
} else {
AppLogger.info('✅ 设备已连接,无需重新连接');
return true;
}
}
// 2.
if (!autoConnect) {
AppLogger.warn('⚠️ 设备未连接且不允许自动连接');
return false;
}
// 3.
List<BluetoothDevice> connectedDevices = FlutterBluePlus.connectedDevices;
if (targetDeviceId != null) {
for (BluetoothDevice device in connectedDevices) {
if (device.remoteId.toString() == targetDeviceId) {
AppLogger.info('🔗 找到已连接的目标设备');
return await _setupDeviceConnection(device);
}
}
}
// 4.
AppLogger.info('🔍 开始搜索目标设备...');
BluetoothDevice? foundDevice = await _searchForTargetDevice(
targetDeviceId: targetDeviceId,
targetDeviceName: targetDeviceName,
searchTimeout: searchTimeout,
);
if (foundDevice == null) {
AppLogger.error('❌ 未找到目标设备');
return false;
}
// 5.
AppLogger.info('🔗 开始连接设备: ${foundDevice.platformName}');
return await _connectToDevice(foundDevice);
}
///
Future<BluetoothDevice?> _searchForTargetDevice({
String? targetDeviceId,
String? targetDeviceName,
Duration searchTimeout = const Duration(seconds: 15),
}) async {
Completer<BluetoothDevice?> searchCompleter = Completer<BluetoothDevice?>();
//
enableBluetoothSearch(
searchTime: searchTimeout,
onDeviceFound: (ScanDeviceInfo deviceInfo) {
BluetoothDevice device = deviceInfo.rawDeviceInfo.device;
String deviceId = device.remoteId.toString();
String deviceName = device.platformName;
AppLogger.debug('🔍 发现设备: $deviceName ($deviceId)');
//
bool isTargetDevice = false;
if (targetDeviceId != null && deviceId == targetDeviceId) {
isTargetDevice = true;
AppLogger.info('🎯 通过ID匹配到目标设备: $deviceName');
} else if (targetDeviceName != null && deviceName.contains(targetDeviceName)) {
isTargetDevice = true;
AppLogger.info('🎯 通过名称匹配到目标设备: $deviceName');
}
if (isTargetDevice && !searchCompleter.isCompleted) {
searchCompleter.complete(device);
}
},
);
//
Timer(searchTimeout, () {
if (!searchCompleter.isCompleted) {
searchCompleter.complete(null);
}
});
return await searchCompleter.future;
}
///
Future<bool> _connectToDevice(BluetoothDevice device) async {
try {
AppLogger.info('🔗 正在连接设备: ${device.platformName}');
await device.connect(timeout: const Duration(seconds: 10));
//
return await _setupDeviceConnection(device);
} catch (e) {
AppLogger.error('❌ 连接设备失败: ${device.platformName}', error: e);
return false;
}
}
///
Future<bool> _setupDeviceConnection(BluetoothDevice device) async {
try {
AppLogger.info('🔧 设置设备连接: ${device.platformName}');
//
_connectedDevice = device;
//
_connectionStateSubscription?.cancel();
_connectionStateSubscription = device.connectionState.listen(_handleListenBluetoothConnectionState);
// MTU变化
_mtuChangeSubscription?.cancel();
_mtuChangeSubscription = device.mtu.listen(_handleListenMtuChange);
//
List<BluetoothService> services = await device.discoverServices();
AppLogger.info('🔍 发现服务数量: ${services.length}');
//
BluetoothService? targetService;
for (BluetoothService service in services) {
if (service.uuid == BleConfig.serviceId) {
targetService = service;
break;
}
}
if (targetService == null) {
throw Exception('未找到目标服务: ${BleConfig.serviceId}');
}
AppLogger.info('✅ 找到目标服务: ${targetService.uuid}');
//
BluetoothCharacteristic? writeChar;
BluetoothCharacteristic? subscribeChar;
for (BluetoothCharacteristic characteristic in targetService.characteristics) {
if (characteristic.uuid == BleConfig.characteristicIdWrite) {
writeChar = characteristic;
AppLogger.info('✅ 找到写入特征值: ${characteristic.uuid}');
} else if (characteristic.uuid == BleConfig.characteristicIdSubscription) {
subscribeChar = characteristic;
AppLogger.info('✅ 找到订阅特征值: ${characteristic.uuid}');
}
}
if (writeChar == null || subscribeChar == null) {
throw Exception('未找到所需的特征值');
}
//
_writeCharacteristic = writeChar;
_subscriptionCharacteristic = subscribeChar;
//
await subscribeChar.setNotifyValue(true);
AppLogger.info('✅ 已订阅通知');
//
_characteristicDataSubscription?.cancel();
_characteristicDataSubscription = subscribeChar.onValueReceived.listen(_handleListenCharacteristicData);
AppLogger.highlight('✨✨✨ ✅ 设备连接设置完成 ✨✨✨');
//
await _processPendingCommands();
return true;
} catch (e, stackTrace) {
AppLogger.error('❌ 设置设备连接失败', error: e, stackTrace: stackTrace);
_connectedDevice = null;
_writeCharacteristic = null;
_subscriptionCharacteristic = null;
return false;
}
}
///
Future<void> _processPendingCommands() async {
if (_pendingCommands.isEmpty) return;
AppLogger.info('📦 处理待发送命令: ${_pendingCommands.length}');
List<_PendingCommand<dynamic>> commandsToProcess = List.from(_pendingCommands);
_pendingCommands.clear();
for (_PendingCommand<dynamic> pendingCmd in commandsToProcess) {
try {
AppLogger.info('🚀 发送待处理命令: ${pendingCmd.commandId}');
dynamic result = await sendCommand(
command: pendingCmd.command,
targetDeviceId: pendingCmd.targetDeviceId,
targetDeviceName: pendingCmd.targetDeviceName,
timeout: pendingCmd.timeout,
);
pendingCmd.completer.complete(result);
} catch (e) {
pendingCmd.completer.completeError(e);
}
}
}
///
String _generateCommandKey<T>(BaseBleCommand<T> command) {
return '${command.runtimeType}_${DateTime.now().millisecondsSinceEpoch}';
}
///
void _cleanupCommandWaiter(String commandKey) {
// ID映射中移除
_commandIdToKeyMap.removeWhere((commandId, key) => key == commandKey);
_commandResponseWaiters.remove(commandKey);
Timer? timer = _commandTimeouts.remove(commandKey);
timer?.cancel();
}
}

View 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;
}
}

View 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;
}
}

View 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();
}
}

View 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. LockID40
// 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) {
// // 040
// int paddingNeeded = 40 - lockIdBytes.length;
// lockIdBytes.addAll(List.filled(paddingNeeded, 0));
// }
//
// // 40
// if (keyIdBytes.length < 40) {
// // 040
// int paddingNeeded = 40 - keyIdBytes.length;
// keyIdBytes.addAll(List.filled(paddingNeeded, 0));
// }
//
// // 40
// if (authUserIdBytes.length < 20) {
// // 040
// 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);
// KeyIDauthUserIDmd5加密之后就是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;
}
}

View 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. LockID40
List<int> lockIdBytes = List.from(lockId.codeUnits); //
// 40
if (lockIdBytes.length < 40) {
// 040
int paddingNeeded = 40 - lockIdBytes.length;
lockIdBytes.addAll(List.filled(paddingNeeded, 0));
}
buffer.addAll(lockIdBytes);
return buffer;
}
}

View 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?;
}
}

View 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?;
}
}

View File

@ -0,0 +1,3 @@
class BleConnectedException implements Exception{
}

View File

@ -1,4 +1,8 @@
class CacheKeys {
static const String isSendValidationCode = 'isSendValidationCode';
static const String token = 'token';
static const String publicKeyHex = 'publicKeyHex';
static const String starCloudUserInfo = 'starCloudUserInfo';
static const String starCloudUserLoginInfo = 'starCloudUserLoginInfo';
}

545
lib/common/sm4_encipher/sm4.dart Executable file
View 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;
}
}

View 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};
}
}

View 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;
}

View 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();
}
}

View File

@ -25,8 +25,7 @@ class SharedPreferencesUtils {
final expireAt = DateTime.now().add(expiry).millisecondsSinceEpoch;
// 使 Transaction
return await prefs.setString(key, value) &&
await prefs.setInt(expiryKey, expireAt);
return await prefs.setString(key, value) && await prefs.setInt(expiryKey, expireAt);
}
///
@ -72,6 +71,10 @@ class SharedPreferencesUtils {
return _prefs?.getString(key);
}
static Future<bool> delString(String key) async {
return _prefs?.remove(key) ?? Future.value(false);
}
// bool
static Future<void> setBool(String key, dynamic value) async {
_prefs?.setBool(key, value);

View File

@ -67,7 +67,7 @@ class F {
// Debug/Profile环境的StarCloud地址
switch (appFlavor) {
case Flavor.sky:
return 'http://192.168.1.121:8111';
return 'http://192.168.1.121:8111/sdk';
case Flavor.xhj:
return 'http://local.cloud.star-lock.cn';
}

View File

@ -3,39 +3,32 @@ import 'package:starwork_flutter/base/app_logger.dart';
import 'package:starwork_flutter/base/app_permission.dart';
import 'package:starwork_flutter/base/base_controller.dart';
import 'package:starwork_flutter/ble/ble_service.dart';
import 'package:starwork_flutter/ble/command/base/base_ble_response_parser.dart';
import 'package:starwork_flutter/ble/command/request/ble_cmd_get_private_key.dart';
import 'package:starwork_flutter/ble/command/request/ble_cmd_get_public_key.dart';
import 'package:starwork_flutter/ble/command/response/ble_cmd_get_private_key_parser.dart';
import 'package:starwork_flutter/ble/command/response/ble_cmd_get_public_key_parser.dart';
import 'package:starwork_flutter/ble/model/scan_device_info.dart';
import 'package:starwork_flutter/common/constant/app_toast_messages.dart';
import 'package:starwork_flutter/routes/app_routes.dart';
import 'package:starwork_flutter/common/constant/cache_keys.dart';
import 'package:starwork_flutter/common/utils/shared_preferences_utils.dart';
class SearchDeviceController extends BaseController {
//
final RxBool isSearching = BleService().isScanningNow.obs;
final RxBool isSearching = false.obs;
//
final RxList<ScanDeviceInfo> deviceList = <ScanDeviceInfo>[].obs;
late void Function(ScanDeviceInfo device) onDeviceFound;
//
final RxBool permissionsGranted = false.obs;
@override
void onInit() async {
super.onInit();
var locationPermission = await AppPermission.requestLocationPermission();
if (!locationPermission) {
showToast(AppToastMessages.notLocationPermission);
return;
}
var bluetoothPermissions = await AppPermission.requestBluetoothPermissions();
if (!bluetoothPermissions) {
showToast(AppToastMessages.notBluetoothPermissions);
return;
}
}
@override
void onReady() {
super.onReady();
//
BleService().enableBluetoothSearch(onDeviceFound: _onDeviceFound);
//
_initializePermissions();
}
@override
@ -45,23 +38,191 @@ class SearchDeviceController extends BaseController {
super.onClose();
}
///
Future<void> _initializePermissions() async {
try {
AppLogger.highlight('🔐 开始检查权限...');
// 使 BleService
bool hasPermissions = await _checkAndRequestBlePermission();
if (!hasPermissions) {
AppLogger.error('❌ 蓝牙相关权限被拒绝');
showToast('蓝牙功能需要相关权限,请在设置中开启');
return;
}
AppLogger.highlight('✅ 所有权限已获得');
//
permissionsGranted.value = true;
AppLogger.highlight('🎉 所有权限已就绪,准备开始搜索');
//
await _startBluetoothSearch();
} catch (e, stackTrace) {
AppLogger.error('权限初始化失败', error: e, stackTrace: stackTrace);
showToast('权限初始化失败,请重试');
}
}
///
Future<void> _startBluetoothSearch() async {
if (!permissionsGranted.value) {
AppLogger.warn('⚠️ 权限未就绪,无法开始搜索');
return;
}
try {
AppLogger.highlight('🔍 开始启动蓝牙搜索...');
//
deviceList.clear();
//
isSearching.value = true;
//
BleService().enableBluetoothSearch(onDeviceFound: _onDeviceFound);
//
_updateSearchingStatus();
} catch (e, stackTrace) {
AppLogger.error('启动蓝牙搜索失败', error: e, stackTrace: stackTrace);
isSearching.value = false;
showToast('启动搜索失败,请刷新并重试');
}
}
///
void _updateSearchingStatus() {
//
Future.delayed(const Duration(seconds: 1), () {
if (isSearching.value) {
bool actuallyScanning = BleService().isScanningNow;
if (isSearching.value != actuallyScanning) {
isSearching.value = actuallyScanning;
if (!actuallyScanning) {
AppLogger.highlight('🔍 搜索已停止');
isSearching.value = false;
}
}
//
if (actuallyScanning) {
_updateSearchingStatus();
}
}
});
}
///
void _onDeviceFound(ScanDeviceInfo device) {
deviceList.add(device);
deviceList.refresh();
AppLogger.highlight('📲 发现新设备: ${device.advName}');
//
bool exists = deviceList.any((existingDevice) => existingDevice.rawDeviceInfo.device.remoteId == device.rawDeviceInfo.device.remoteId);
if (!exists) {
deviceList.add(device);
deviceList.refresh();
AppLogger.debug('✅ 设备已添加到列表,当前设备数量: ${deviceList.length},设备信息:${deviceList.toString()}');
} else {
AppLogger.debug('⚠️ 设备已存在,跳过添加');
}
}
//
Future<void> refreshDevices() async {
BleService().stopBluetoothSearch();
deviceList.clear();
//
await Future.delayed(const Duration(seconds: 1));
BleService().enableBluetoothSearch(onDeviceFound: _onDeviceFound);
AppLogger.highlight('🔄 开始刷新设备列表');
if (!permissionsGranted.value) {
AppLogger.warn('⚠️ 权限未就绪,无法刷新');
showToast('请先授权必要的权限');
return;
}
try {
//
BleService().stopBluetoothSearch();
isSearching.value = false;
//
deviceList.clear();
//
await Future.delayed(const Duration(seconds: 1));
//
await _startBluetoothSearch();
AppLogger.highlight('✅ 设备列表刷新完成');
} catch (e, stackTrace) {
AppLogger.error('刷新设备列表失败', error: e, stackTrace: stackTrace);
showToast('刷新失败,请重试');
}
}
//
void connectingDevices() async {
Get.toNamed(AppRoutes.confirmPairDevice);
void connectingDevices(ScanDeviceInfo device) async {
try {
//
BleService().stopBluetoothSearch();
isSearching.value = false;
//
BleCmdGetPublicKey getPublicKeyCmd = BleCmdGetPublicKey(lockId: device.advName);
//
GetPublicKeyResponse? publicKeyResponse = await BleService().sendCommand<GetPublicKeyResponse>(
command: getPublicKeyCmd,
targetDeviceName: device.advName,
//
autoConnectIfNeeded: true,
);
if (publicKeyResponse != null && publicKeyResponse.isSuccess) {
AppLogger.info('🎯 获取公钥成功: ${publicKeyResponse.publicKeyHex}');
SharedPreferencesUtils.setString(CacheKeys.publicKeyHex, publicKeyResponse.publicKeyHex);
BleCmdGetPrivateKey getPrivateKeyCmd = BleCmdGetPrivateKey(
lockId: device.advName,
keyId: '1',
authUserID: '1',
nowTime: DateTime
.now()
.millisecondsSinceEpoch ~/ 1000,
publicKey: publicKeyResponse.publicKey,
);
//
GetPrivateKeyResponse? privateKeyResponse = await BleService().sendCommand<GetPrivateKeyResponse>(
command: getPrivateKeyCmd,
targetDeviceName: device.advName,
//
autoConnectIfNeeded: true,
);
if (privateKeyResponse != null && privateKeyResponse.isSuccess) {
AppLogger.info('🎯 获取私钥成功: ${privateKeyResponse.commKeyHex}');
}
} else {
AppLogger.warn('⚠️ 命令发送完成,但未收到有效响应');
}
} catch (e, stackTrace) {
AppLogger.error('连接设备失败', error: e, stackTrace: stackTrace);
showToast('连接失败,请重试');
}
}
Future<bool> _checkAndRequestBlePermission() async {
var locationPermission = await AppPermission.requestLocationPermission();
if (!locationPermission) {
showToast(AppToastMessages.notLocationPermission);
return false;
}
var bluetoothPermissions = await AppPermission.requestBluetoothPermissions();
if (!bluetoothPermissions) {
showToast(AppToastMessages.notBluetoothPermissions);
return false;
}
return true;
}
}

View File

@ -174,7 +174,7 @@ class SearchDeviceView extends GetView<SearchDeviceController> {
_buildItem({required ScanDeviceInfo device, required int index}) {
return GestureDetector(
onTap: () {
controller.connectingDevices();
controller.connectingDevices(device);
},
child: Container(
margin: EdgeInsets.symmetric(horizontal: 10.w),

View File

@ -3,20 +3,26 @@ import 'dart:async';
import 'package:get/get.dart';
import 'package:starwork_flutter/api/api_response.dart';
import 'package:starwork_flutter/api/model/common/request/send_validation_code_request.dart';
import 'package:starwork_flutter/api/model/starcloud/request/starcloud_create_user_request.dart';
import 'package:starwork_flutter/api/model/starcloud/request/starcloud_login_request.dart';
import 'package:starwork_flutter/api/model/user/request/validation_code_login.dart';
import 'package:starwork_flutter/api/service/common_api_service.dart';
import 'package:starwork_flutter/api/service/user_api_service.dart';
import 'package:starwork_flutter/api/starcloud/starcloud_api_service.dart';
import 'package:starwork_flutter/base/app_logger.dart';
import 'package:starwork_flutter/base/base_controller.dart';
import 'package:starwork_flutter/common/constant/cache_keys.dart';
import 'package:starwork_flutter/common/constant/platform_type.dart';
import 'package:starwork_flutter/common/constant/verification_code_channel.dart';
import 'package:starwork_flutter/common/constant/verification_code_type.dart';
import 'package:starwork_flutter/common/utils/shared_preferences_utils.dart';
import 'package:starwork_flutter/flavors.dart';
import 'package:starwork_flutter/routes/app_routes.dart';
class InputVerificationCodeController extends BaseController {
final userApi = Get.find<UserApiService>();
final commonApi = Get.find<CommonApiService>();
final starCloudApi = Get.find<StarCloudApiService>();
var rawPhone = ''.obs;
var phone = ''.obs;
@ -71,6 +77,7 @@ class InputVerificationCodeController extends BaseController {
//
void checkVerificationCode(String pin) async {
///
if (previousRoute.value.contains(AppRoutes.login)) {
showLoading();
var validationCodeLoginResult = await userApi.validationCodeLogin(
@ -95,10 +102,6 @@ class InputVerificationCodeController extends BaseController {
hideLoading();
}
void _handleSeedVerificationCode({
required VerificationCodeType codeType,
}) async {}
//
void seedVerificationCode({
required VerificationCodeType codeType,
@ -134,13 +137,4 @@ class InputVerificationCodeController extends BaseController {
}
hideLoading();
}
//
void _toPage() async {
if (previousRoute.value.contains(AppRoutes.login)) {
Get.offAllNamed(AppRoutes.main);
} else if (previousRoute.value.contains(AppRoutes.forgotPassword)) {
Get.toNamed(AppRoutes.setNewPassword);
}
}
}

View File

@ -82,7 +82,7 @@ packages:
source: hosted
version: "1.18.0"
crypto:
dependency: transitive
dependency: "direct main"
description:
name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab

View File

@ -38,6 +38,9 @@ dependencies:
super_tooltip: ^2.0.8
# 蓝牙库
flutter_blue_plus: ^1.35.7
# 加解密库
crypto: ^3.0.3
dev_dependencies:
flutter_test: