feat: Sdk 封装
This commit is contained in:
@@ -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")
|
||||
|
||||
// prepareTransfer(key 用原始文件地址)
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user