feat: 重写 SDK 支持 iOS 12.2,移除 async/await 和 Combine
- 所有公开 API 改为 completion handler (Result<T, Error>) - 状态通知改为 DuooomiBleSDKDelegate 协议 - 移除 Sendable、@MainActor、AsyncStream、CheckedContinuation - 网络请求改为 URLSession.dataTask 回调 - BLE 通信改为 delegate + 闭包模式 - deployment target 降至 iOS 12.2 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@ final class AniConverter {
|
||||
private let config: DuooomiBleConfig
|
||||
|
||||
private var endpoint: URL {
|
||||
config.apiHost.appendingPathComponent("api/auth/loomart/file/convert-to-ani")
|
||||
URL(string: "\(config.apiHost)/api/auth/loomart/file/convert-to-ani")!
|
||||
}
|
||||
|
||||
private let session: URLSession = {
|
||||
@@ -26,15 +26,23 @@ final class AniConverter {
|
||||
}
|
||||
|
||||
/// 调用 ANI 转换接口,返回 ani 文件的 CDN URL。
|
||||
func convert(fileUrl: String) async throws -> String {
|
||||
do {
|
||||
return try await performConvert(fileUrl: fileUrl)
|
||||
} catch let error as NSError where error.code == NSURLErrorNetworkConnectionLost {
|
||||
return try await performConvert(fileUrl: fileUrl)
|
||||
func convert(fileUrl: String, completion: @escaping (Result<String, Error>) -> Void) {
|
||||
performConvert(fileUrl: fileUrl) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(result)
|
||||
case .failure(let error):
|
||||
// Retry once on connection lost
|
||||
if (error as NSError).code == NSURLErrorNetworkConnectionLost {
|
||||
self.performConvert(fileUrl: fileUrl, completion: completion)
|
||||
} else {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func performConvert(fileUrl: String) async throws -> String {
|
||||
private func performConvert(fileUrl: String, completion: @escaping (Result<String, Error>) -> Void) {
|
||||
var request = URLRequest(url: endpoint)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "content-type")
|
||||
@@ -47,32 +55,49 @@ final class AniConverter {
|
||||
"height": config.aniHeight,
|
||||
"fps": config.aniFps,
|
||||
]
|
||||
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
||||
|
||||
let (data, response) = try await session.data(for: request)
|
||||
|
||||
if let http = response as? HTTPURLResponse, !(200..<300).contains(http.statusCode) {
|
||||
throw AniConverterError(message: "HTTP \(http.statusCode)")
|
||||
do {
|
||||
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||||
throw AniConverterError(message: "Invalid JSON response")
|
||||
}
|
||||
session.dataTask(with: request) { data, response, error in
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
guard (json["success"] as? Bool) == true,
|
||||
let outer = json["data"] as? [String: Any] else {
|
||||
throw AniConverterError(message: "ANI API reported failure")
|
||||
}
|
||||
if let http = response as? HTTPURLResponse, !(200..<300).contains(http.statusCode) {
|
||||
completion(.failure(AniConverterError(message: "HTTP \(http.statusCode)")))
|
||||
return
|
||||
}
|
||||
|
||||
if (outer["status"] as? Bool) != true {
|
||||
let msg = (outer["msg"] as? String) ?? "ANI conversion failed"
|
||||
throw AniConverterError(message: msg)
|
||||
}
|
||||
guard let data = data,
|
||||
let json = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] else {
|
||||
completion(.failure(AniConverterError(message: "Invalid JSON response")))
|
||||
return
|
||||
}
|
||||
|
||||
guard let aniUrl = outer["data"] as? String, !aniUrl.isEmpty else {
|
||||
throw AniConverterError(message: "Missing ani url in response")
|
||||
}
|
||||
guard (json["success"] as? Bool) == true,
|
||||
let outer = json["data"] as? [String: Any] else {
|
||||
completion(.failure(AniConverterError(message: "ANI API reported failure")))
|
||||
return
|
||||
}
|
||||
|
||||
return aniUrl
|
||||
if (outer["status"] as? Bool) != true {
|
||||
let msg = (outer["msg"] as? String) ?? "ANI conversion failed"
|
||||
completion(.failure(AniConverterError(message: msg)))
|
||||
return
|
||||
}
|
||||
|
||||
guard let aniUrl = outer["data"] as? String, !aniUrl.isEmpty else {
|
||||
completion(.failure(AniConverterError(message: "Missing ani url in response")))
|
||||
return
|
||||
}
|
||||
|
||||
completion(.success(aniUrl))
|
||||
}.resume()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import Foundation
|
||||
|
||||
/// 协议服务内部回调
|
||||
protocol BleProtocolServiceDelegate: AnyObject {
|
||||
func protocolService(_ service: BleProtocolService, didReceiveMessage commandType: UInt8, data: Data)
|
||||
}
|
||||
|
||||
/// 协议通信服务:帧收发、分包重组
|
||||
final class BleProtocolService: @unchecked Sendable {
|
||||
final class BleProtocolService: BleClientDelegate {
|
||||
|
||||
weak var delegate: BleProtocolServiceDelegate?
|
||||
private let client: BleClient
|
||||
private var isListening = false
|
||||
|
||||
/// 分包重组会话
|
||||
private struct FragmentSession {
|
||||
@@ -11,63 +19,36 @@ final class BleProtocolService: @unchecked Sendable {
|
||||
}
|
||||
|
||||
private var fragments: [String: FragmentSession] = [:]
|
||||
private var listenerTask: Task<Void, Never>?
|
||||
|
||||
private var messageContinuation: AsyncStream<(commandType: UInt8, data: Data)>.Continuation?
|
||||
private(set) var incomingMessages: AsyncStream<(commandType: UInt8, data: Data)>!
|
||||
|
||||
init(client: BleClient) {
|
||||
self.client = client
|
||||
incomingMessages = AsyncStream { continuation in
|
||||
self.messageContinuation = continuation
|
||||
}
|
||||
BleLog.i("Protocol service created", "Protocol")
|
||||
}
|
||||
|
||||
// MARK: - Start/Stop Listening
|
||||
|
||||
/// 开始监听底层通知并进行帧解析与重组。
|
||||
/// - Note: 重新创建消息流并清空历史分片缓存。
|
||||
func startListening() {
|
||||
// Recreate the stream in case it was previously finished
|
||||
incomingMessages = AsyncStream { continuation in
|
||||
self.messageContinuation = continuation
|
||||
}
|
||||
|
||||
isListening = true
|
||||
client.delegate = self
|
||||
fragments.removeAll()
|
||||
|
||||
listenerTask = Task { [weak self] in
|
||||
guard let self else { return }
|
||||
BleLog.i("Start listening for notifications", "Protocol")
|
||||
for await data in self.client.notifications() {
|
||||
self.handleRawData(data)
|
||||
}
|
||||
}
|
||||
BleLog.i("Start listening for notifications", "Protocol")
|
||||
}
|
||||
|
||||
/// 停止监听,结束消息流并清理缓存。
|
||||
func stopListening() {
|
||||
listenerTask?.cancel()
|
||||
listenerTask = nil
|
||||
messageContinuation?.finish()
|
||||
messageContinuation = nil
|
||||
isListening = false
|
||||
fragments.removeAll()
|
||||
BleLog.i("Stop listening; fragments cleared", "Protocol")
|
||||
}
|
||||
|
||||
// MARK: - Send
|
||||
|
||||
/// 发送二进制数据(自动拆分为协议帧)。
|
||||
/// - Parameters:
|
||||
/// - type: 命令类型值。
|
||||
/// - data: 需要发送的原始负载。
|
||||
/// - onProgress: 发送进度回调(0.0~1.0)。
|
||||
/// - Throws: 写入失败或负载过大等错误。
|
||||
/// 发送二进制数据(自动拆分为协议帧),使用回调。
|
||||
func send(
|
||||
type: UInt8,
|
||||
data: Data,
|
||||
onProgress: ((Double) -> Void)? = nil
|
||||
) async throws {
|
||||
onProgress: ((Double) -> Void)? = nil,
|
||||
completion: @escaping (Result<Void, Error>) -> Void
|
||||
) {
|
||||
let maxWriteLen = client.maximumWriteLength
|
||||
let maxDataSize = min(
|
||||
maxWriteLen - FrameConstants.headerSize - FrameConstants.footerSize,
|
||||
@@ -84,27 +65,84 @@ final class BleProtocolService: @unchecked Sendable {
|
||||
|
||||
guard !frames.isEmpty else {
|
||||
BleLog.e("Payload too large; frames empty", "Protocol")
|
||||
throw DuooomiBleError.transferFailed("Payload too large: exceeds 65535-page limit")
|
||||
completion(.failure(DuooomiBleError.transferFailed("Payload too large: exceeds 65535-page limit")))
|
||||
return
|
||||
}
|
||||
|
||||
let total = frames.count
|
||||
BleLog.d("Sending frames: total=\(total), maxChunk=\(safeMaxDataSize)", "Protocol")
|
||||
for (index, frame) in frames.enumerated() {
|
||||
try client.writeWithoutResponse(frame)
|
||||
try await Task.sleep(nanoseconds: FrameConstants.frameIntervalNanos)
|
||||
|
||||
func sendFrame(at index: Int) {
|
||||
guard index < total else {
|
||||
onProgress?(1.0)
|
||||
BleLog.i("Frames sent: total=\(total)", "Protocol")
|
||||
completion(.success(()))
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
try self.client.writeWithoutResponse(frames[index])
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
onProgress?(Double(index + 1) / Double(total))
|
||||
|
||||
if index + 1 < total {
|
||||
// Inter-frame delay 35ms
|
||||
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 0.035) {
|
||||
sendFrame(at: index + 1)
|
||||
}
|
||||
} else {
|
||||
onProgress?(1.0)
|
||||
BleLog.i("Frames sent: total=\(total)", "Protocol")
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
BleLog.i("Frames sent: total=\(total)", "Protocol")
|
||||
|
||||
sendFrame(at: 0)
|
||||
}
|
||||
|
||||
/// 发送 JSON 命令,内部完成编码与帧拆分。
|
||||
/// - Parameters:
|
||||
/// - type: 命令类型。
|
||||
/// - payload: 可编码的命令体。
|
||||
/// - Throws: 编码/发送失败时抛出。
|
||||
func sendJSON<T: Encodable>(type: CommandType, payload: T) async throws {
|
||||
/// 发送 JSON 命令(带回调)
|
||||
func sendJSON<T: Encodable>(type: CommandType, payload: T, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
do {
|
||||
let data = try JSONEncoder().encode(payload)
|
||||
send(type: type.rawValue, data: data, completion: completion)
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
/// 发送 JSON 命令(同步,fire-and-forget,仅发第一帧,用于简单命令)
|
||||
func sendJSON<T: Encodable>(type: CommandType, payload: T) throws {
|
||||
let jsonData = try JSONEncoder().encode(payload)
|
||||
try await send(type: type.rawValue, data: jsonData)
|
||||
let maxWriteLen = client.maximumWriteLength
|
||||
let maxDataSize = min(
|
||||
maxWriteLen - FrameConstants.headerSize - FrameConstants.footerSize,
|
||||
FrameConstants.maxDataSize
|
||||
)
|
||||
let safeMaxDataSize = max(1, maxDataSize)
|
||||
let frames = ProtocolManager.createFrames(
|
||||
type: type.rawValue,
|
||||
data: jsonData,
|
||||
head: .appToDevice,
|
||||
maxDataSize: safeMaxDataSize
|
||||
)
|
||||
for frame in frames {
|
||||
try client.writeWithoutResponse(frame)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - BleClientDelegate
|
||||
|
||||
func bleClient(_ client: BleClient, didDiscoverDevice device: DiscoveredDevice) {
|
||||
// Not handled here — scan results forwarded by SDK
|
||||
}
|
||||
|
||||
func bleClient(_ client: BleClient, didReceiveData data: Data) {
|
||||
guard isListening else { return }
|
||||
handleRawData(data)
|
||||
}
|
||||
|
||||
// MARK: - Receive & Reassemble
|
||||
@@ -119,15 +157,12 @@ final class BleProtocolService: @unchecked Sendable {
|
||||
handleFragment(frame)
|
||||
} else {
|
||||
BleLog.d("Single frame received: type=\(frame.type), len=\(frame.data.count)", "Protocol")
|
||||
messageContinuation?.yield((commandType: frame.type, data: frame.data))
|
||||
delegate?.protocolService(self, didReceiveMessage: frame.type, data: frame.data)
|
||||
}
|
||||
}
|
||||
|
||||
private func handleFragment(_ frame: ProtocolFrame) {
|
||||
// Match TS reference behavior: scope reassembly per peripheral so concurrent transfers
|
||||
// (or stale fragments from a previous device after reconnect) cannot pollute each other.
|
||||
let deviceKey = client.connectedPeripheral?.identifier.uuidString ?? "unknown"
|
||||
let key = "\(deviceKey)_\(frame.type)"
|
||||
let key = "\(frame.type)"
|
||||
|
||||
if fragments[key] == nil {
|
||||
fragments[key] = FragmentSession(total: Int(frame.subpageTotal), frames: [:])
|
||||
@@ -153,6 +188,6 @@ final class BleProtocolService: @unchecked Sendable {
|
||||
|
||||
fragments.removeValue(forKey: key)
|
||||
BleLog.i("Fragment reassembled: key=\(key), size=\(combined.count)", "Protocol")
|
||||
messageContinuation?.yield((commandType: frame.type, data: combined))
|
||||
delegate?.protocolService(self, didReceiveMessage: frame.type, data: combined)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
/// 设备命令服务:发送 JSON 命令,等待协议响应
|
||||
/// 设备命令服务:发送 JSON 命令(fire-and-forget,响应由协议层回调处理)
|
||||
final class DeviceInfoService {
|
||||
private let protocolService: BleProtocolService
|
||||
|
||||
@@ -10,61 +10,43 @@ final class DeviceInfoService {
|
||||
|
||||
// MARK: - Commands
|
||||
|
||||
/// 发送获取设备信息命令。
|
||||
/// - Throws: 发送失败时抛出。
|
||||
func getDeviceInfo() async throws {
|
||||
try await protocolService.sendJSON(
|
||||
func getDeviceInfo() throws {
|
||||
try protocolService.sendJSON(
|
||||
type: .getDeviceInfo,
|
||||
payload: CommandPayload(type: CommandType.getDeviceInfo.rawValue)
|
||||
)
|
||||
}
|
||||
|
||||
/// 发送获取设备版本命令。
|
||||
/// - Throws: 发送失败时抛出。
|
||||
func getDeviceVersion() async throws {
|
||||
try await protocolService.sendJSON(
|
||||
func getDeviceVersion() throws {
|
||||
try protocolService.sendJSON(
|
||||
type: .getDeviceVersion,
|
||||
payload: CommandPayload(type: CommandType.getDeviceVersion.rawValue)
|
||||
)
|
||||
}
|
||||
|
||||
/// 发送绑定命令。
|
||||
/// - Parameter userId: 用户唯一标识。
|
||||
/// - Throws: 发送失败时抛出。
|
||||
func bindDevice(userId: String) async throws {
|
||||
try await protocolService.sendJSON(
|
||||
func bindDevice(userId: String) throws {
|
||||
try protocolService.sendJSON(
|
||||
type: .bindDevice,
|
||||
payload: BindPayload(type: CommandType.bindDevice.rawValue, userId: userId)
|
||||
)
|
||||
}
|
||||
|
||||
/// 发送解绑命令。
|
||||
/// - Parameter userId: 用户唯一标识。
|
||||
/// - Throws: 发送失败时抛出。
|
||||
func unbindDevice(userId: String) async throws {
|
||||
try await protocolService.sendJSON(
|
||||
func unbindDevice(userId: String) throws {
|
||||
try protocolService.sendJSON(
|
||||
type: .unbindDevice,
|
||||
payload: BindPayload(type: CommandType.unbindDevice.rawValue, userId: userId)
|
||||
)
|
||||
}
|
||||
|
||||
/// 发送删除文件命令。
|
||||
/// - Parameter key: 文件 key 或完整 URL。
|
||||
/// - Throws: 发送失败时抛出。
|
||||
func deleteFile(key: String) async throws {
|
||||
try await protocolService.sendJSON(
|
||||
func deleteFile(key: String) throws {
|
||||
try protocolService.sendJSON(
|
||||
type: .deleteFile,
|
||||
payload: FileKeyPayload(type: CommandType.deleteFile.rawValue, key: key)
|
||||
)
|
||||
}
|
||||
|
||||
/// 发送传输准备命令。
|
||||
/// - Parameters:
|
||||
/// - key: 文件标识 key。
|
||||
/// - size: 文件大小(字节)。
|
||||
/// - Throws: 发送失败时抛出。
|
||||
func prepareTransfer(key: String, size: Int) async throws {
|
||||
try await protocolService.sendJSON(
|
||||
func prepareTransfer(key: String, size: Int) throws {
|
||||
try protocolService.sendJSON(
|
||||
type: .prepareTransfer,
|
||||
payload: PrepareTransferPayload(
|
||||
type: CommandType.prepareTransfer.rawValue,
|
||||
|
||||
@@ -9,40 +9,60 @@ final class FileTransferService {
|
||||
}
|
||||
|
||||
/// 传输文件到设备
|
||||
/// - Parameters:
|
||||
/// - fileUri: 本地 file:// URL 或远程 https:// URL
|
||||
/// - commandType: 传输命令类型
|
||||
/// - onProgress: 进度回调 0.0 ~ 1.0
|
||||
func transferFile(
|
||||
fileUri: String,
|
||||
commandType: CommandType,
|
||||
onProgress: ((Double) -> Void)? = nil
|
||||
) async throws {
|
||||
onProgress: ((Double) -> Void)? = nil,
|
||||
completion: @escaping (Result<Void, Error>) -> Void
|
||||
) {
|
||||
BleLog.i("Load file: \(fileUri)", "Transfer")
|
||||
let data = try await loadFileData(from: fileUri)
|
||||
BleLog.d("File loaded: size=\(data.count) bytes", "Transfer")
|
||||
|
||||
try await protocolService.send(
|
||||
type: commandType.rawValue,
|
||||
data: data,
|
||||
onProgress: onProgress
|
||||
)
|
||||
loadFileData(from: fileUri) { [weak self] result in
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
case .success(let data):
|
||||
BleLog.d("File loaded: size=\(data.count) bytes", "Transfer")
|
||||
self?.protocolService.send(
|
||||
type: commandType.rawValue,
|
||||
data: data,
|
||||
onProgress: onProgress,
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func loadFileData(from uri: String) async throws -> Data {
|
||||
private func loadFileData(from uri: String, completion: @escaping (Result<Data, Error>) -> Void) {
|
||||
guard let url = URL(string: uri) else {
|
||||
throw DuooomiBleError.transferFailed("Invalid URL: \(uri)")
|
||||
completion(.failure(DuooomiBleError.transferFailed("Invalid URL: \(uri)")))
|
||||
return
|
||||
}
|
||||
|
||||
if url.isFileURL {
|
||||
return try Data(contentsOf: url)
|
||||
} else {
|
||||
let (data, response) = try await URLSession.shared.data(from: url)
|
||||
do {
|
||||
let data = try Data(contentsOf: url)
|
||||
completion(.success(data))
|
||||
} catch {
|
||||
completion(.failure(DuooomiBleError.transferFailed("Failed to read local file: \(error.localizedDescription)")))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
URLSession.shared.dataTask(with: url) { data, response, error in
|
||||
if let error = error {
|
||||
completion(.failure(DuooomiBleError.transferFailed("Download failed: \(error.localizedDescription)")))
|
||||
return
|
||||
}
|
||||
guard let httpResponse = response as? HTTPURLResponse,
|
||||
(200...299).contains(httpResponse.statusCode) else {
|
||||
throw DuooomiBleError.transferFailed("Failed to download file: \(uri)")
|
||||
completion(.failure(DuooomiBleError.transferFailed("Failed to download file: \(uri)")))
|
||||
return
|
||||
}
|
||||
return data
|
||||
}
|
||||
guard let data = data, !data.isEmpty else {
|
||||
completion(.failure(DuooomiBleError.transferFailed("Empty download response")))
|
||||
return
|
||||
}
|
||||
completion(.success(data))
|
||||
}.resume()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Foundation
|
||||
|
||||
/// 固件信息
|
||||
public struct FirmwareInfo: Codable, Equatable, Sendable {
|
||||
public struct FirmwareInfo: Codable, Equatable {
|
||||
public let version: String
|
||||
public let fileUrl: String
|
||||
public let description: String?
|
||||
@@ -25,20 +25,30 @@ final class FirmwareService {
|
||||
self.config = config
|
||||
}
|
||||
|
||||
func fetchLatest(identifier: String? = nil, status: String? = nil) async throws -> FirmwareInfo? {
|
||||
func fetchLatest(
|
||||
identifier: String? = nil,
|
||||
status: String? = nil,
|
||||
completion: @escaping (Result<FirmwareInfo?, Error>) -> Void
|
||||
) {
|
||||
let id = (identifier ?? config.firmwareIdentifier).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let st = status ?? config.firmwareStatus
|
||||
guard !id.isEmpty else {
|
||||
throw DuooomiBleError.transferFailed("Invalid firmware identifier")
|
||||
completion(.failure(DuooomiBleError.transferFailed("Invalid firmware identifier")))
|
||||
return
|
||||
}
|
||||
|
||||
var comps = URLComponents(url: config.apiHost.appendingPathComponent(latestPath), resolvingAgainstBaseURL: false)!
|
||||
let urlString = "\(config.apiHost)/\(latestPath)"
|
||||
guard var comps = URLComponents(string: urlString) else {
|
||||
completion(.failure(DuooomiBleError.transferFailed("Invalid firmware URL")))
|
||||
return
|
||||
}
|
||||
comps.queryItems = [
|
||||
URLQueryItem(name: "identifier", value: id),
|
||||
URLQueryItem(name: "status", value: st)
|
||||
]
|
||||
guard let url = comps.url else {
|
||||
throw DuooomiBleError.transferFailed("Invalid firmware URL")
|
||||
completion(.failure(DuooomiBleError.transferFailed("Invalid firmware URL")))
|
||||
return
|
||||
}
|
||||
|
||||
var req = URLRequest(url: url)
|
||||
@@ -46,15 +56,30 @@ final class FirmwareService {
|
||||
req.setValue(config.apiKey, forHTTPHeaderField: "x-api-key")
|
||||
req.setValue("application/json", forHTTPHeaderField: "accept")
|
||||
|
||||
let (data, resp) = try await URLSession.shared.data(for: req)
|
||||
guard let http = resp as? HTTPURLResponse else {
|
||||
throw DuooomiBleError.transferFailed("Invalid response")
|
||||
}
|
||||
guard (200...299).contains(http.statusCode) else {
|
||||
throw DuooomiBleError.transferFailed("HTTP \(http.statusCode)")
|
||||
}
|
||||
URLSession.shared.dataTask(with: req) { data, resp, error in
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
guard let http = resp as? HTTPURLResponse else {
|
||||
completion(.failure(DuooomiBleError.transferFailed("Invalid response")))
|
||||
return
|
||||
}
|
||||
guard (200...299).contains(http.statusCode) else {
|
||||
completion(.failure(DuooomiBleError.transferFailed("HTTP \(http.statusCode)")))
|
||||
return
|
||||
}
|
||||
guard let data = data else {
|
||||
completion(.failure(DuooomiBleError.transferFailed("Empty response")))
|
||||
return
|
||||
}
|
||||
|
||||
let decoded = try JSONDecoder().decode(FirmwareResponse.self, from: data)
|
||||
return decoded.data
|
||||
do {
|
||||
let decoded = try JSONDecoder().decode(FirmwareResponse.self, from: data)
|
||||
completion(.success(decoded.data))
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user