feat: 增加团队管理页

This commit is contained in:
liyi 2025-09-12 10:35:07 +08:00
parent c1a6830614
commit 7b57957e5b
28 changed files with 141 additions and 3206 deletions

View File

@ -5,4 +5,6 @@ class ApiPath {
static const String allTeamList = "/v1/team/teamListAll";
static const String sceneList = "/v1/team/sceneList";
static const String createTeam = "/v1/team/createTeam";
static const String changeTeam = "/v1/team/changeTeam";
static const String bindTeamStarCloudAccount = "/v1/team/bindStarCloudAccount";
}

View File

@ -18,11 +18,12 @@ class BaseApiService {
dio.options.baseUrl = F.apiHost;
dio.options.connectTimeout = const Duration(seconds: 30);
dio.options.receiveTimeout = const Duration(seconds: 30);
dio.options.headers['content-type'] = 'application/json';
//
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
var token = SharedPreferencesUtils.getString(CacheKeys.token);
AppLogger.info('token:${token}');
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}

View File

@ -0,0 +1,36 @@
class BindTeamStarCloudAccountRequest {
String teamId;
String teamNo;
String username;
String password;
BindTeamStarCloudAccountRequest({
required this.teamId,
required this.teamNo,
required this.username,
required this.password,
});
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['teamId'] = teamId;
data['teamNo'] = teamNo;
data['username'] = username;
data['password'] = password;
return data;
}
factory BindTeamStarCloudAccountRequest.fromJson(Map<String, dynamic> json) {
return BindTeamStarCloudAccountRequest(
teamId: json['teamId'] as String,
teamNo: json['teamNo'] as String,
username: json['username'] as String,
password: json['password'] as String,
);
}
@override
String toString() {
return 'BindTeamStarCloudAccountRequest{teamId: $teamId, teamNo: $teamNo, username: $username, password: $password}';
}
}

View File

@ -0,0 +1,24 @@
class ChangeCurrentTeamRequest {
String teamNo;
ChangeCurrentTeamRequest({
required this.teamNo,
});
Map<String, dynamic> toJson() {
return {
"teamNo": teamNo,
};
}
factory ChangeCurrentTeamRequest.fromJson(Map<String, dynamic> json) {
return ChangeCurrentTeamRequest(
teamNo: json["teamNo"],
);
}
@override
String toString() {
return 'ChangeCurrentTeamRequest{teamNo: $teamNo}';
}
}

View File

@ -2,6 +2,8 @@ import 'package:get/get.dart';
import 'package:starwork_flutter/api/api_path.dart';
import 'package:starwork_flutter/api/api_response.dart';
import 'package:starwork_flutter/api/base_api_service.dart';
import 'package:starwork_flutter/api/model/team/request/bind_team_star_cloud_account_request.dart';
import 'package:starwork_flutter/api/model/team/request/change_current_team_request.dart';
import 'package:starwork_flutter/api/model/team/request/create_team_request.dart';
import 'package:starwork_flutter/api/model/team/response/all_team_list_response.dart';
import 'package:starwork_flutter/api/model/team/response/create_team_response.dart';
@ -47,4 +49,30 @@ class TeamApiService {
fromJson: (data) => SceneInfoResponseList.fromJson(data),
);
}
//
Future<ApiResponse<void>> requestChangeCurrentTeam({
required ChangeCurrentTeamRequest request,
}) {
return _api.makeRequest(
//
path: ApiPath.changeTeam,
method: HttpConstant.post,
data: request,
fromJson: (data) {},
);
}
//
Future<ApiResponse<SceneInfoResponseList>> requestBindTeamStarCloudAccount({
required BindTeamStarCloudAccountRequest request,
}) {
return _api.makeRequest(
//
path: ApiPath.bindTeamStarCloudAccount,
method: HttpConstant.post,
data: request,
fromJson: (data) => SceneInfoResponseList.fromJson(data),
);
}
}

View File

@ -1,12 +0,0 @@
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
class BleConfig {
static Guid serviceId = Guid('fff0');
// id
static Guid characteristicIdSubscription = Guid('fff1');
// id
static Guid characteristicIdWrite = Guid('fff2');
}

File diff suppressed because it is too large Load Diff

View File

@ -1,401 +0,0 @@
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 = 0x20; // 2.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;
}
}
if (i == 0) {
//
int packetSequence = getNextPacketSequence();
//
List<int> header = buildPacketHeader(
packetType: PACKET_TYPE_REQUEST,
packetSequence: packetSequence,
encryptType: encryptType,
encryptedDataLength: originalData.length,
originalDataLength: originalData.length,
);
// ( + )
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);
} else {
packets.add(currentPacketData);
}
}
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);
AppLogger.highlight('加密类型:${encryptType}');
// 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

@ -1,153 +0,0 @@
import 'dart:convert';
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);
}
String utf8String(List<int> codeUnits) {
codeUnits.reversed;
final List<int> uniqueList = <int>[];
for (int i = 0; i < codeUnits.length; i++) {
if (codeUnits[i] != 0) {
uniqueList.add(codeUnits[i]);
}
}
uniqueList.reversed;
return utf8.decode(uniqueList).toString();
}
String asciiString(List<int> codeUnits) {
String result = '';
for (final int value in codeUnits) {
result += String.fromCharCode(value);
}
return result;
}
}

View File

@ -1,270 +0,0 @@
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_add_admin_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/ble/command/response/ble_cmd_read_lock_status_parser.dart';
import 'package:starwork_flutter/common/constant/cache_keys.dart';
import 'package:starwork_flutter/common/sm4_encipher/sm4.dart';
import 'package:starwork_flutter/common/utils/shared_preferences_utils.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());
registerParser(BleCmdReadLockStatusParser());
registerParser(BleCmdAddAdminParser());
// 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 = _decryptDataIfNeededSync(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> _decryptDataIfNeededSync(ParsedPacket parsedPacket) {
//
if (parsedPacket.encryptType == BaseBleCommand.ENCRYPT_TYPE_PLAIN) {
AppLogger.debug('🔓 数据未加密,直接使用原始数据');
return parsedPacket.data;
}
switch (parsedPacket.encryptType) {
case BaseBleCommand.ENCRYPT_TYPE_AES128:
return parsedPacket.data;
case BaseBleCommand.ENCRYPT_TYPE_SM4_PRESET:
var connectedDevice = BleService().connectedDevice;
var platformName = connectedDevice?.platformName ?? '';
if (platformName.isEmpty) {
AppLogger.warn('⚠️ 解密数据时未找到设备名称');
return parsedPacket.data;
}
var decrypt = SM4.decrypt(
parsedPacket.data,
key: utf8.encode(platformName),
mode: SM4CryptoMode.ECB,
);
return decrypt;
case BaseBleCommand.ENCRYPT_TYPE_SM4_DEVICE:
// 使
var cachedPrivateKey = BleService.cachedPrivateKey;
if (cachedPrivateKey == null || cachedPrivateKey.isEmpty) {
AppLogger.warn('⚠️ 解密数据时未找到缓存的私钥');
return parsedPacket.data;
}
var decrypt = SM4.decrypt(
parsedPacket.data,
key: cachedPrivateKey,
mode: SM4CryptoMode.ECB,
);
return decrypt;
default:
AppLogger.warn('⚠️ 未知的加密类型: ${parsedPacket.encryptType},使用原始数据');
return parsedPacket.data;
}
}
///
/// [parsedPacket]
///
Future<List<int>> _decryptDataIfNeeded(ParsedPacket parsedPacket) async {
//
if (parsedPacket.encryptType == BaseBleCommand.ENCRYPT_TYPE_PLAIN) {
AppLogger.debug('🔓 数据未加密,直接使用原始数据');
return parsedPacket.data;
}
switch (parsedPacket.encryptType) {
case BaseBleCommand.ENCRYPT_TYPE_AES128:
return parsedPacket.data;
case BaseBleCommand.ENCRYPT_TYPE_SM4_PRESET:
var connectedDevice = BleService().connectedDevice;
var platformName = connectedDevice?.platformName;
var decrypt = SM4.decrypt(
parsedPacket.data,
key: utf8.encode(platformName!),
mode: SM4CryptoMode.ECB,
);
return decrypt;
case BaseBleCommand.ENCRYPT_TYPE_SM4_DEVICE:
var lockCommKey = await SharedPreferencesUtils.getString(CacheKeys.lockCommKey);
if (lockCommKey == null) {
AppLogger.warn('⚠️ 解密订阅数据时未找到私钥');
return parsedPacket.data;
}
var decrypt = SM4.decrypt(
parsedPacket.data,
key: utf8.encode(lockCommKey),
mode: SM4CryptoMode.ECB,
);
return decrypt;
default:
AppLogger.warn('⚠️ 未知的加密类型: ${parsedPacket.encryptType},使用原始数据');
return parsedPacket.data;
}
}
/// 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

@ -1,230 +0,0 @@
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/ble/command/response/model/ble_add_admin_response.dart';
import 'package:starwork_flutter/ble/constant/lock_ble_constant.dart';
import 'package:starwork_flutter/common/sm4_encipher/sm4.dart';
/// - BaseBleCommand
class BleCmdAddAdmin extends BaseBleCommand<BleAddAdminResponse> {
final String lockId;
final String authUserId;
final String keyId;
final String userId;
final int openMode;
final int keyType;
final int startDate;
final int expireDate;
final int useCountLimit;
final int isRound;
final int weekRound;
final int startHour;
final int startMin;
final int endHour;
final int endMin;
final int role;
final String password;
final List<int> token;
final List<int> publicKey;
final List<int> privateKey;
final int _encryptType;
/// ID: 0x3001
static const int cmdId = 0x3001;
///
BleCmdAddAdmin({
int encryptType = BaseBleCommand.ENCRYPT_TYPE_SM4_DEVICE,
required this.lockId,
required this.authUserId,
required this.keyId,
required this.userId,
this.openMode = 1, // 1
this.keyType = LockBleConstant.keyTypeTemporary,
required this.startDate,
required this.expireDate,
this.useCountLimit = 0xFFFF,
this.isRound = 0,
this.weekRound = 0,
this.startHour = 0,
this.startMin = 0,
this.endHour = 0,
this.endMin = 0,
this.role = 255,
required this.password,
required this.token,
required this.publicKey,
required this.privateKey,
}) : _encryptType = encryptType {
AppLogger.debug('🔑 BleCmdAddAdmin 创建: LockID="$lockId", 加密类型=$_encryptType');
}
/// -
@override
int getEncryptType() {
return _encryptType;
}
/// BaseBleCommand的抽象方法 -
@override
List<int> buildData() {
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);
// lockId 40
final int lockIDLength = utf8.encode(lockId).length;
data.addAll(utf8.encode(lockId));
data = getFixedLengthList(data, 40 - lockIDLength);
// authUserId 20
final int authUserIdLength = utf8.encode(authUserId).length;
data.addAll(utf8.encode(authUserId));
data = getFixedLengthList(data, 20 - authUserIdLength);
//KeyID 40
final int keyIDLength = utf8.encode(keyId).length;
data.addAll(utf8.encode(keyId));
data = getFixedLengthList(data, 40 - keyIDLength);
//userId 20
final int userIdLength = utf8.encode(userId).length;
data.addAll(utf8.encode(userId));
data = getFixedLengthList(data, 20 - userIdLength);
// openModel 1
data.add(openMode);
// keyType 1
data.add(keyType);
int? d1, d2;
if (role == LockBleConstant.roleSuperAdmin) {
d1 = 0;
d2 = 0xffffffff;
} else {
d1 = startDate;
d2 = expireDate;
}
// StartDate 4
data.add((d1 & 0xff000000) >> 24);
data.add((d1 & 0xff0000) >> 16);
data.add((d1 & 0xff00) >> 8);
data.add(d1 & 0xff);
// expireDate 4
data.add((d2 & 0xff000000) >> 24);
data.add((d2 & 0xff0000) >> 16);
data.add((d2 & 0xff00) >> 8);
data.add(d2 & 0xff);
//useCountLimit 2
final double useCountLimitDouble = useCountLimit / 256;
final int useCountLimit1 = useCountLimitDouble.toInt();
final int useCountLimit2 = useCountLimit % 256;
data.add(useCountLimit1);
data.add(useCountLimit2);
data.add(isRound);
data.add(weekRound);
data.add(startHour);
data.add(startMin);
data.add(endHour);
data.add(endMin);
// role 1
data.add(role);
//password 20
final int passwordLength = utf8.encode(password).length;
data.addAll(utf8.encode(password));
data = getFixedLengthList(data, 20 - passwordLength);
// token 4 Token 0 token失效或者第一次发送的时候token为0
data.addAll(token);
final List<int> authCodeData = <int>[];
authCodeData.addAll(utf8.encode(authUserId));
authCodeData.addAll(utf8.encode(keyId));
authCodeData.addAll(token);
authCodeData.addAll(publicKey);
// authUserIDKeyIDtokenmd5加密之后就是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);
}
}
AppLogger.debug("完整明文数据: $data");
// privateKey进行SM4 ECB加密
ebcData = SM4.encrypt(data, key: privateKey, mode: SM4CryptoMode.ECB);
return ebcData;
}
///
@override
bool shouldRetry(dynamic response) {
// BleAddAdminResponse类型且状态码为6token
if (response is BleAddAdminResponse) {
return response.statusCode == 0x06; // token
}
return false;
}
///
@override
BaseBleCommand<BleAddAdminResponse> createRetryCommand(dynamic response) {
if (response is BleAddAdminResponse) {
// 0x06tokentoken字段是有效的使token
List<int> token = response.token;
AppLogger.debug('🔐 获取到token用于重发: $token');
// token
return BleCmdAddAdmin(
encryptType: this.getEncryptType(),
lockId: this.lockId,
authUserId: this.authUserId,
keyId: this.keyId,
userId: this.userId,
openMode: this.openMode,
keyType: this.keyType,
startDate: this.startDate,
expireDate: this.expireDate,
useCountLimit: this.useCountLimit,
isRound: this.isRound,
weekRound: this.weekRound,
startHour: this.startHour,
startMin: this.startMin,
endHour: this.endHour,
endMin: this.endMin,
role: this.role,
password: this.password,
token: token,
// 使token
publicKey: this.publicKey,
privateKey: this.privateKey,
);
}
//
throw Exception('无法为响应创建重发命令: $response');
}
}

View File

@ -1,113 +0,0 @@
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() {
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 20
final int authUserIDLength = utf8.encode(authUserID!).length;
data.addAll(utf8.encode(authUserID));
data = getFixedLengthList(data, 20 - authUserIDLength);
//NowTime 4
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
authCodeData.add((nowTime & 0xff000000) >> 24);
authCodeData.add((nowTime & 0xff0000) >> 16);
authCodeData.add((nowTime & 0xff00) >> 8);
authCodeData.add(nowTime & 0xff);
authCodeData.addAll(publicKey);
// authUserIDKeyIDmd5加密之后就是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

@ -1,64 +0,0 @@
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);
if ((buffer.length % 16) != 0) {
final int add = 16 - buffer.length % 16;
for (int i = 0; i < add; i++) {
buffer.add(0);
}
}
return buffer;
}
}

View File

@ -1,87 +0,0 @@
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/ble/command/response/ble_cmd_read_lock_status_parser.dart';
import 'package:starwork_flutter/common/sm4_encipher/sm4.dart';
/// - BaseBleCommand
class BleCmdReadLockStatus extends BaseBleCommand<ReadLockStatusResponse> {
final String lockId;
final String userId;
final int timeStamp;
final int localUnix;
final List<int> privateKey;
final int _encryptType; //
/// ID: 0x3040
static const int cmdId = 0x3040;
///
BleCmdReadLockStatus({
int encryptType = BaseBleCommand.ENCRYPT_TYPE_SM4_DEVICE,
required this.lockId,
required this.userId,
required this.timeStamp,
required this.localUnix,
required this.privateKey,
}) : _encryptType = encryptType {
AppLogger.debug('🔑 BleCmdReadLockStatus 创建: LockID="$lockId", 加密类型=$_encryptType');
}
/// -
@override
int getEncryptType() {
return _encryptType;
}
/// BaseBleCommand的抽象方法 -
@override
List<int> buildData() {
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);
//userId 40
final int userIdLength = utf8.encode(userId).length;
data.addAll(utf8.encode(userId));
data = getFixedLengthList(data, 40 - userIdLength);
//timeStamp 4
data.add((timeStamp & 0xff000000) >> 24);
data.add((timeStamp & 0xff0000) >> 16);
data.add((timeStamp & 0xff00) >> 8);
data.add(timeStamp & 0xff);
//localUnix 4
data.add((localUnix & 0xff000000) >> 24);
data.add((localUnix & 0xff0000) >> 16);
data.add((localUnix & 0xff00) >> 8);
data.add(localUnix & 0xff);
if ((data.length % 16) != 0) {
final int add = 16 - data.length % 16;
for (int i = 0; i < add; i++) {
data.add(0);
}
}
// SM4 ECB加密
ebcData = SM4.encrypt(data, key:privateKey, mode: SM4CryptoMode.ECB);
return ebcData;
}
}

View File

@ -1,112 +0,0 @@
import 'dart:convert';
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_add_admin.dart';
import 'package:starwork_flutter/ble/command/request/ble_cmd_get_public_key.dart';
import 'package:starwork_flutter/ble/command/request/ble_cmd_read_lock_status.dart';
import 'package:starwork_flutter/ble/command/response/model/ble_add_admin_response.dart';
class BleCmdAddAdminParser extends BaseBleResponseParser {
@override
int get commandId => BleCmdAddAdmin.cmdId;
@override
String get commandName => '增加超级管理员命令';
///
@override
BleAddAdminResponse parseResponse(ParsedPacket parsedPacket, List<int> rawResponseData) {
try {
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. lockId
List<int> lockId = BaseBleResponseParser.extractBytes(rawResponseData, offset, 40);
String lockIdStr = _safeStringDecode(lockId).trim();
offset += 40;
// 3. token
List<int> token = BaseBleResponseParser.extractBytes(rawResponseData, offset, 4);
offset += 4;
// 4.
int statusCode = rawResponseData[offset++];
// 0x06tokentoken字段是有效的token
if (statusCode == 0x06) {
return BleAddAdminResponse(
commandId: commandId,
statusCode: statusCode,
token: token, // 0x06token
lockId: lockIdStr,
serialNo: '',
no: -1,
);
}
// 0token
if (statusCode == 0) {
return BleAddAdminResponse(
commandId: commandId,
statusCode: statusCode,
token: token, // 0token
lockId: lockIdStr,
serialNo: '',
no: -1,
);
}
//
return BleAddAdminResponse(
commandId: commandId,
statusCode: statusCode,
token: [], // token
lockId: lockIdStr,
serialNo: '',
no: -1,
);
} catch (e) {
AppLogger.error('❌ 解析添加管理员应答数据异常', error: e);
rethrow;
}
}
///
/// UTF-8
String _safeStringDecode(List<int> data) {
// 1. 0x00
int endIndex = 0;
for (int i = 0; i < data.length; i++) {
if (data[i] == 0) {
break;
}
endIndex = i + 1;
}
// 2.
List<int> validBytes = data.sublist(0, endIndex);
try {
// 3. 使 UTF-8
return utf8.decode(validBytes, allowMalformed: true).trim();
} catch (e) {
// 4.
return String.fromCharCodes(validBytes).trim();
}
}
/// 便 -
static BleAddAdminResponse? parseRawPacket(List<int> rawPacketData) {
BleCmdAddAdminParser parser = BleCmdAddAdminParser();
return parser.handleResponse(rawPacketData) as BleAddAdminResponse?;
}
}

View File

@ -1,145 +0,0 @@
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 {
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

@ -1,131 +0,0 @@
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

@ -1,299 +0,0 @@
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';
import 'package:starwork_flutter/ble/command/request/ble_cmd_read_lock_status.dart';
///
class ReadLockStatusResponse {
final int commandId;
final int statusCode;
final String vendor; // WDJT
final int product; // 1=2=3=4=
final String model; // singlegrip2
final String fwVersion; // 1.0.0.230828
final String hwVersion; // 1.0.0.230828
final String serialNum0; //
final String? serialNum1; //
final String btDeviceName; // TMH_c3570480da8d
final int electricQuantity; // (%)
final int electricQuantityStandby; //
final int restoreCounter; //
final int restoreDate; // UNIX
final String icPartNo; //
final int indate; // UNIX
final String mac; // MAC
final int featureValueLength; //
final List<int> featureValue; //
final String featureValueHexStr; // 16
final int featureEnValLength; //
final List<int> featureEnVal; //
final String featureEnValHexStr; // 16
final int featureParaTotal; //
final List<int> featureParaData; // ,FeatureParaTotal开始
ReadLockStatusResponse({
required this.commandId,
required this.statusCode,
required this.vendor,
required this.product,
required this.model,
required this.fwVersion,
required this.hwVersion,
required this.serialNum0,
required this.serialNum1,
required this.btDeviceName,
required this.electricQuantity,
required this.electricQuantityStandby,
required this.restoreCounter,
required this.restoreDate,
required this.icPartNo,
required this.indate,
required this.mac,
required this.featureValueLength,
required this.featureValue,
required this.featureValueHexStr,
required this.featureEnValLength,
required this.featureEnVal,
required this.featureEnValHexStr,
required this.featureParaTotal,
required this.featureParaData,
});
///
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 'ReadLockStatusResponse('
'commandId: 0x${commandId.toRadixString(16).padLeft(4, '0')}, '
'status: $statusDescription, '
'vendor: $vendor, '
'product: $product, '
'model: $model, '
'fwVersion: $fwVersion, '
'hwVersion: $hwVersion, '
'serialNum0: $serialNum0, '
'serialNum1: $serialNum1, '
'btDeviceName: $btDeviceName, '
'electricQuantity: $electricQuantity%, '
'electricQuantityStandby: $electricQuantityStandby%, '
'restoreCounter: $restoreCounter, '
'restoreDate: $restoreDate (${DateTime.fromMillisecondsSinceEpoch(restoreDate * 1000, isUtc: true)}), '
'icPartNo: $icPartNo, '
'indate: $indate (${DateTime.fromMillisecondsSinceEpoch(indate * 1000, isUtc: true)}), '
'mac: $mac, '
'featureValueLength: $featureValueLength,'
'featureValueHexStr: $featureValueHexStr,'
'featureEnValLength: $featureEnValLength,'
'featureEnValHexStr: $featureEnValHexStr,'
'featureParaTotal: $featureParaTotal,'
'featureParaData: $featureParaData'
')';
}
}
///
class BleCmdReadLockStatusParser extends BaseBleResponseParser {
@override
int get commandId => BleCmdReadLockStatus.cmdId;
@override
String get commandName => '读取锁状态命令';
///
@override
ReadLockStatusResponse parseResponse(ParsedPacket parsedPacket, List<int> rawResponseData) {
try {
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) {
return ReadLockStatusResponse(
commandId: commandId,
statusCode: statusCode,
vendor: '',
product: 0,
model: '',
fwVersion: '',
hwVersion: '',
serialNum0: '',
serialNum1: '',
btDeviceName: '',
electricQuantity: 0,
electricQuantityStandby: 0,
restoreCounter: 0,
restoreDate: 0,
icPartNo: '',
indate: 0,
mac: '',
featureValueLength: 0,
featureValue: [],
featureEnValLength: 0,
featureValueHexStr: '',
featureEnVal: [],
featureEnValHexStr: '',
featureParaTotal: -1,
featureParaData: [],
);
}
// 3. (20 bytes, UTF-8)
List<int> vendorBytes = BaseBleResponseParser.extractBytes(rawResponseData, offset, 20);
String vendor = utf8String(vendorBytes).trim();
offset += 20;
// 4. (1 byte)
int product = rawResponseData[offset++];
// 5. (20 bytes, UTF-8)
List<int> modelBytes = BaseBleResponseParser.extractBytes(rawResponseData, offset, 20);
String model = utf8String(modelBytes).trim();
offset += 20;
// 6. (20 bytes, UTF-8)
List<int> fwVersionBytes = BaseBleResponseParser.extractBytes(rawResponseData, offset, 20);
String fwVersion = utf8String(fwVersionBytes).trim();
offset += 20;
// 7. (20 bytes, UTF-8)
List<int> hwVersionBytes = BaseBleResponseParser.extractBytes(rawResponseData, offset, 20);
String hwVersion = utf8String(hwVersionBytes).trim();
offset += 20;
// 8. (16 bytes, UTF-8)
List<int> serialNum0Bytes = BaseBleResponseParser.extractBytes(rawResponseData, offset, 16);
String serialNum0 = utf8String(serialNum0Bytes).trim();
offset += 16;
// 9. (16 bytes, UTF-8, )
List<int> serialNum1Bytes = BaseBleResponseParser.extractBytes(rawResponseData, offset, 16);
String serialNum1 = utf8String(serialNum1Bytes).trim();
offset += 16;
// 10. (16 bytes, UTF-8)
List<int> btDeviceNameBytes = BaseBleResponseParser.extractBytes(rawResponseData, offset, 16);
String btDeviceName = utf8String(btDeviceNameBytes).trim();
offset += 16;
// 11. (1 byte)
int electricQuantity = rawResponseData[offset++];
// 12. (1 byte)
int electricQuantityStandby = rawResponseData[offset++];
// 13. (2 bytes, big-endian)
int restoreCounter = BaseBleResponseParser.extractInt(rawResponseData, offset, 2);
offset += 2;
// 14. (4 bytes, big-endian, )
int restoreDateSeconds = BaseBleResponseParser.extractInt(rawResponseData, offset, 4);
offset += 4;
// 15. (10 bytes, UTF-8)
List<int> icPartNoBytes = BaseBleResponseParser.extractBytes(rawResponseData, offset, 10);
String icPartNo = utf8String(icPartNoBytes).trim();
offset += 10;
// 16. (4 bytes, big-endian, )
int indateSeconds = BaseBleResponseParser.extractInt(rawResponseData, offset, 4);
offset += 4;
// 17. MAC (20 bytes, UTF-8)
List<int> macBytes = BaseBleResponseParser.extractBytes(rawResponseData, offset, 20);
String mac = utf8String(macBytes).trim();
offset += 20;
// 18. (1 byte)
int featureValueLength = rawResponseData[offset++];
// 19.
List<int> featureValue = BaseBleResponseParser.extractBytes(rawResponseData, offset, featureValueLength);
String featureValueStr = asciiString(featureValue);
offset += featureValueLength;
// 20.
int featureEnValLength = rawResponseData[offset++];
// 21.
List<int> featureEnVal = BaseBleResponseParser.extractBytes(rawResponseData, offset, featureEnValLength);
String featureEnValStr = asciiString(featureValue);
offset += featureEnValLength;
// 22.
int featureParaTotal = rawResponseData[offset++];
// 23.
List<int> featureParaData = rawResponseData.sublist(offset);
//
return ReadLockStatusResponse(
commandId: commandId,
statusCode: statusCode,
vendor: vendor,
product: product,
model: model,
fwVersion: fwVersion,
hwVersion: hwVersion,
serialNum0: serialNum0,
serialNum1: serialNum1,
btDeviceName: btDeviceName,
electricQuantity: electricQuantity,
electricQuantityStandby: electricQuantityStandby,
restoreCounter: restoreCounter,
restoreDate: restoreDateSeconds,
// UNIX
icPartNo: icPartNo,
indate: indateSeconds,
// UNIX
mac: mac,
featureValueLength: featureValueLength,
featureValue: featureValue,
featureValueHexStr: featureValueStr,
featureEnValLength: featureEnValLength,
featureEnVal: featureEnVal,
featureEnValHexStr: featureEnValStr,
featureParaTotal: featureParaTotal,
featureParaData: featureParaData,
);
} catch (e) {
AppLogger.error('❌ 解析读取锁状态答数据异常', error: e);
rethrow;
}
}
/// 便 -
static ReadLockStatusResponse? parseRawPacket(List<int> rawPacketData) {
BleCmdReadLockStatusParser parser = BleCmdReadLockStatusParser();
return parser.handleResponse(rawPacketData) as ReadLockStatusResponse?;
}
}

View File

@ -1,43 +0,0 @@
class BleAddAdminResponse {
final int commandId;
final int statusCode;
final int no;
final String serialNo;
final String lockId;
final List<int> token;
BleAddAdminResponse({
required this.commandId,
required this.statusCode,
required this.token,
required this.lockId,
required this.serialNo,
required this.no,
});
///
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 'BleAddAdminResponse{commandId: $commandId, statusCode: $statusCode, no: $no, serialNo: $serialNo, lockId: $lockId, token: $token}';
}
}

View File

@ -1,19 +0,0 @@
class LockBleConstant {
/// KeyType:
/// 0:
/// 1:
/// 2:
/// 255:
static const int keyTypeNormal = 0;
static const int keyTypeTemporary = 1;
static const int keyTypeBluetoothRemote = 2;
static const int keyTypeTest = 255;
/// Role:
/// 0:
/// 1:
/// 255 (0xFF):
static const int roleNormalUser = 0;
static const int roleAdmin = 1;
static const int roleSuperAdmin = 255; // 0xFF
}

View File

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

View File

@ -1,26 +0,0 @@
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
class ScanDeviceInfo {
//
final String advName;
//
final bool isBinding;
//
final bool hasNewEvent;
//
final ScanResult rawDeviceInfo;
ScanDeviceInfo({
required this.advName,
this.isBinding = false, // false
this.hasNewEvent = false, // false
required this.rawDeviceInfo,
});
@override
String toString() {
return 'ScanDeviceInfo{advName: $advName, isBinding: $isBinding, hasNewEvent: $hasNewEvent,rawDeviceInfo:$rawDeviceInfo}';
}
}

View File

@ -9,17 +9,6 @@ import 'package:starcloud/sdk/starcloud.dart';
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_add_admin.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/request/ble_cmd_read_lock_status.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/command/response/ble_cmd_read_lock_status_parser.dart';
import 'package:starwork_flutter/ble/command/response/model/ble_add_admin_response.dart';
import 'package:starwork_flutter/ble/model/scan_device_info.dart';
import 'package:starwork_flutter/common/constant/app_toast_messages.dart';
import 'package:starwork_flutter/common/constant/cache_keys.dart';
import 'package:starwork_flutter/common/utils/shared_preferences_utils.dart';

View File

@ -4,7 +4,6 @@ import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:starcloud/entity/star_cloud_scan_result.dart';
import 'package:starwork_flutter/ble/model/scan_device_info.dart';
import 'package:starwork_flutter/views/device/searchDevice/search_device_controller.dart';
class SearchDeviceView extends GetView<SearchDeviceController> {
@ -173,7 +172,6 @@ class SearchDeviceView extends GetView<SearchDeviceController> {
}
_buildItem({required StarCloudScanResult device, required int index}) {
return GestureDetector(
onTap: () async {
controller.connectingDevices(device);

View File

@ -4,8 +4,10 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:starwork_flutter/api/model/team/request/change_current_team_request.dart';
import 'package:starwork_flutter/api/model/team/response/team_info_response.dart';
import 'package:starwork_flutter/api/service/team_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/app_images.dart';
import 'package:starwork_flutter/routes/app_routes.dart';
@ -115,4 +117,21 @@ class MainController extends BaseController {
selectedTeam.value = myTeamList.first;
}
}
///
void requestChangeCurrentTeam(TeamInfoResponse teamInfo) async {
var teamNo = teamInfo.teamNo;
if (teamNo == null) {
return;
}
var changeCurrentTeamResponse = await teamApi.requestChangeCurrentTeam(
request: ChangeCurrentTeamRequest(
teamNo: teamNo,
),
);
if (changeCurrentTeamResponse.isSuccess) {
AppLogger.highlight('切换当前团队成功');
selectedTeam.value = teamInfo;
}
}
}

View File

@ -38,8 +38,8 @@ class MainView extends GetView<MainController> {
teamList: controller.myTeamList,
selectedTeam: controller.selectedTeam.value,
onTeamSelected: (team) {
controller.selectedTeam.value = team;
AppLogger.highlight('message:${team}');
controller.requestChangeCurrentTeam(team);
},
onRefreshList: () {
controller.requestAllTeamInfoList();

View File

@ -1,5 +1,7 @@
import 'package:flutter/widgets.dart';
import 'package:get/get.dart';
import 'package:starcloud/sdk/entity/cloud_user_info.dart';
import 'package:starcloud/sdk/starcloud.dart';
import 'package:starwork_flutter/api/model/team/request/create_team_request.dart';
import 'package:starwork_flutter/api/model/team/response/scene_info_response.dart';
import 'package:starwork_flutter/api/service/team_api_service.dart';
@ -28,19 +30,41 @@ class UseCaseSettingController extends BaseController {
/// 使
void requestAllSceneInfoList() async {
showLoading();
var sceneList = await teamApi.requestAllSceneInfoList();
if (sceneList.isSuccess) {
// 使
useCases.value = sceneList.data?.list ?? [];
useCases.refresh();
}
hideLoading();
}
void requestCreateTeam() async {
showLoading();
var cacheStarCloudUserName = await SharedPreferencesUtils.getString(CacheKeys.starCloudUserName);
var cacheStarCloudPassword = await SharedPreferencesUtils.getString(CacheKeys.starCloudPassword);
var cacheStarCloudUid = await SharedPreferencesUtils.getString(CacheKeys.starCloudUid);
if (cacheStarCloudUserName == null || cacheStarCloudPassword == null || cacheStarCloudUid == null) {
await StarCloudSDK.instance.createCloudUser(
onError: (err) {
AppLogger.error('err:${err}');
hideLoading();
},
onSuccess: (userInfo) {
SharedPreferencesUtils.setString(CacheKeys.starCloudUserName, userInfo.username);
SharedPreferencesUtils.setString(CacheKeys.starCloudPassword, userInfo.password);
SharedPreferencesUtils.setString(CacheKeys.starCloudUid, userInfo.uid.toString());
},
);
}
StarCloudSDK.instance.setCloudAccounts(
[
CloudUserInfo(
username: cacheStarCloudUserName!,
password: cacheStarCloudPassword!,
uid: int.parse(cacheStarCloudUid!),
)
],
);
var createTeamResponse = await teamApi.requestCreateTeam(
request: CreateTeamRequest(
teamName: teamNameInputController.text,