feat: Sdk 封装

This commit is contained in:
km2023
2026-04-10 16:15:53 +08:00
parent 7d9ff3081d
commit ba0a80e921
10 changed files with 431 additions and 486 deletions

View File

@@ -20,12 +20,18 @@ public final class DuooomiBleSDK: ObservableObject {
@Published public private(set) var error: String? = nil
@Published public private(set) var discoveredDevices: [DiscoveredDevice] = []
// MARK: - Configuration
public let config: DuooomiBleConfig
// MARK: - Internal Services
private let bleClient: BleClient
private let protocolService: BleProtocolService
private let deviceInfoService: DeviceInfoService
private let fileTransferService: FileTransferService
private let aniConverter: AniConverter
private let firmwareService: FirmwareService
// MARK: - Request-Response
@@ -41,13 +47,16 @@ public final class DuooomiBleSDK: ObservableObject {
// MARK: - Init
public init() {
public init(config: DuooomiBleConfig) {
self.config = config
bleClient = BleClient()
protocolService = BleProtocolService(client: bleClient)
deviceInfoService = DeviceInfoService(protocolService: protocolService)
fileTransferService = FileTransferService(protocolService: protocolService)
aniConverter = AniConverter(config: config)
firmwareService = FirmwareService(config: config)
BleLog.i("SDK initialized", "SDK")
BleLog.i("SDK initialized (apiHost=\(config.apiHost), firmware=\(config.firmwareIdentifier)/\(config.firmwareStatus))", "SDK")
setupDisconnectHandler()
}
@@ -327,6 +336,97 @@ public final class DuooomiBleSDK: ObservableObject {
BleLog.i("Transfer completed", "Transfer")
}
// MARK: - High-Level APIs
///
///
/// / ANI prepareTransfer transferFile
/// - Parameter fileUrl: URL mp4/jpg/png ANI.ani
/// - Throws:
public func transferMedia(fileUrl: String) async throws {
try ensureConnected()
let url = fileUrl.trimmingCharacters(in: .whitespacesAndNewlines)
guard !url.isEmpty else { throw DuooomiBleError.transferFailed("Empty file URL") }
let isAni = url.lowercased().hasSuffix(".ani")
let aniUrl: String
if isAni {
aniUrl = url
BleLog.i("Direct ANI transfer: \(url)", "Transfer")
} else {
BleLog.i("Converting to ANI: \(url)", "Transfer")
do {
aniUrl = try await aniConverter.convert(fileUrl: url)
} catch {
throw DuooomiBleError.transferFailed("ANI conversion failed: \(error.localizedDescription)")
}
BleLog.i("ANI ready: \(aniUrl)", "Transfer")
}
//
let aniSize = try await getRemoteFileSize(url: aniUrl)
BleLog.d("ANI size: \(aniSize) bytes", "Transfer")
// prepareTransferkey
let key = url
_ = try await prepareTransfer(key: key, size: aniSize)
//
try await transferFile(fileUri: aniUrl, commandType: .transferAniVideo)
}
///
/// - Parameters:
/// - identifier: 使 config firmwareIdentifier
/// - status: 使 config firmwareStatus
/// - Returns: nil
public func fetchLatestFirmware(identifier: String? = nil, status: String? = nil) async throws -> FirmwareInfo? {
return try await firmwareService.fetchLatest(identifier: identifier, status: status)
}
/// OTA
/// - Parameter fileUrl: `fetchLatestFirmware()`
/// - Throws:
public func upgradeFirmware(fileUrl: String) async throws {
try ensureConnected()
BleLog.i("OTA upgrade start: \(fileUrl)", "Firmware")
try await transferFile(fileUri: fileUrl, commandType: .otaPackage)
BleLog.i("OTA upgrade completed", "Firmware")
}
// MARK: - Internal Helpers
private func getRemoteFileSize(url: String) async throws -> Int {
guard let fileUrl = URL(string: url) else {
throw DuooomiBleError.transferFailed("Invalid URL: \(url)")
}
var request = URLRequest(url: fileUrl)
request.httpMethod = "HEAD"
let (_, response) = try await URLSession.shared.data(for: request)
if let http = response as? HTTPURLResponse, http.expectedContentLength > 0 {
return Int(http.expectedContentLength)
}
// HEAD Content-Length
if let http = response as? HTTPURLResponse,
let lengthStr = http.value(forHTTPHeaderField: "Content-Length"),
let length = Int(lengthStr), length > 0 {
return length
}
// 退Range
var rangeReq = URLRequest(url: fileUrl)
rangeReq.httpMethod = "GET"
rangeReq.setValue("bytes=0-0", forHTTPHeaderField: "Range")
let (_, rangeResp) = try await URLSession.shared.data(for: rangeReq)
if let http = rangeResp as? HTTPURLResponse,
let rangeHeader = http.value(forHTTPHeaderField: "Content-Range"),
let totalStr = rangeHeader.split(separator: "/").last,
let total = Int(totalStr), total > 0 {
return total
}
throw DuooomiBleError.transferFailed("Cannot determine file size: \(url)")
}
// MARK: - Request-Response Pattern
private func sendAndWait(