Files
duooomi-ios-sdk/Sources/DuooomiBleSDK/Services/FirmwareUpgradeService.swift
km2023 97e5b8359a feat(sdk): 重命名 owner→brand 并在硬绑前校验设备品牌
- DuooomiBleConfig.owner → brand(语义:设备品牌名),HTTP header x-owner 不变
- bind() 前置 verifyBrand:缺 deviceInfo 先 getDeviceInfo,与 config.brand 严格匹配,不一致主动断连并抛 .brandMismatch
- README 抹除软绑/对账/UpgradeRecord 等内部实现细节,仅保留厂商面向的公开 API(功能代码完全保留)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 11:51:17 +08:00

172 lines
6.8 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
/// RPC / /
/// RN `FirmwareController.{checkUpgrade, reportUpgradeSuccessBySn, reportUpgradeFailureBySn}` 1:1
final class FirmwareUpgradeService {
private let config: DuooomiBleConfig
private let session: URLSession
init(config: DuooomiBleConfig, session: URLSession = .shared) {
self.config = config
self.session = session
}
///
func checkUpgrade(
sn: String,
currentVersion: String?,
completion: @escaping (Result<FirmwareUpgradeCheckResult, Error>) -> Void
) {
var body: [String: String] = ["sn": sn]
if let v = currentVersion, !v.isEmpty { body["currentVersion"] = v }
post(
path: "/api/auth/loomart/firmware/upgrade/check",
body: body
) { result in
switch result {
case .failure(let e): completion(.failure(e))
case .success(let data):
do {
let parsed = try JSONDecoder().decode(FirmwareUpgradeCheckResult.self, from: data)
completion(.success(parsed))
} catch {
completion(.failure(error))
}
}
}
}
func reportUpgradeSuccess(
sn: String,
firmwareId: String,
fromVersion: String,
bindUserId: String?,
completion: @escaping (Result<Void, Error>) -> Void
) {
var body: [String: String] = [
"sn": sn,
"firmwareId": firmwareId,
"fromVersion": fromVersion,
]
if let u = bindUserId, !u.isEmpty { body["bindUserId"] = u }
post(path: "/api/auth/loomart/firmware/upgrade/report-success", body: body) { result in
completion(result.map { _ in () })
}
}
func reportUpgradeFailure(
sn: String,
firmwareId: String,
fromVersion: String,
failureReason: String,
completion: @escaping (Result<Void, Error>) -> Void
) {
let body = [
"sn": sn,
"firmwareId": firmwareId,
"fromVersion": fromVersion,
"failureReason": failureReason,
]
post(path: "/api/auth/loomart/firmware/upgrade/report-failure", body: body) { result in
completion(result.map { _ in () })
}
}
// MARK: - HTTP
private func post(
path: String,
body: [String: String],
completion: @escaping (Result<Data, Error>) -> Void
) {
guard let url = URL(string: config.apiHost.absoluteString + path) else {
completion(.failure(DuooomiBleError.transferFailed("URL 拼装失败")))
return
}
var req = URLRequest(url: url)
req.httpMethod = "POST"
req.timeoutInterval = 10
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.setValue("application/json", forHTTPHeaderField: "Accept")
req.setValue(config.apiKey, forHTTPHeaderField: "x-api-key")
req.setValue(config.brand, forHTTPHeaderField: "x-owner")
do {
req.httpBody = try JSONEncoder().encode(body)
} catch {
completion(.failure(error))
return
}
session.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("无响应")))
return
}
guard (200...299).contains(http.statusCode) else {
let msg = Self.parseErrorMessage(data: data) ?? "HTTP \(http.statusCode)"
completion(.failure(DuooomiBleError.transferFailed(msg)))
return
}
// `{ success, data }` unwrap data
if let data = data, let unwrapped = Self.unwrapData(from: data) {
completion(.success(unwrapped))
} else {
completion(.success(data ?? Data()))
}
}.resume()
}
private static func unwrapData(from raw: Data) -> Data? {
guard let json = try? JSONSerialization.jsonObject(with: raw) as? [String: Any],
json["success"] as? Bool == true,
let inner = json["data"] else {
return nil
}
return try? JSONSerialization.data(withJSONObject: inner)
}
private static func parseErrorMessage(data: Data?) -> String? {
guard let data = data, let raw = String(data: data, encoding: .utf8) else { return nil }
var stripped = raw
if stripped.hasPrefix("API error:") {
stripped = String(stripped.dropFirst("API error:".count)).trimmingCharacters(in: .whitespaces)
}
if stripped.hasPrefix("{"),
let json = try? JSONSerialization.jsonObject(with: Data(stripped.utf8)) as? [String: Any] {
if let msg = json["message"] as? String { return msg }
if let err = json["error"] as? [String: Any], let msg = err["message"] as? String { return msg }
}
return stripped.isEmpty ? nil : stripped
}
}
// MARK: - Response Model
/// `/firmware/upgrade/check`
/// ** `targetFirmware` **SDK `upgradeAvailable` / `reason`
/// client """"
public struct FirmwareUpgradeCheckResult: Codable, Equatable {
public let currentSystemVersion: String?
public let targetFirmware: TargetFirmware?
public struct TargetFirmware: Codable, Equatable {
/// ID`UpgradeRecord.firmwareId`
public let id: String
/// "1.2.3"UI + `UpgradeRecord.targetVersion`
public let version: String
/// URL `upgradeFirmware(fileUrl:)`
public let fileUrl: String?
/// Int64 UI xx MB
public let fileSize: String?
/// MD5SDK
public let fileMd5: String?
/// `true` /
/// `false` / `nil`
/// Optional Codable
public let forceUpgrade: Bool?
}
}