import SwiftUI import DuooomiBleSDK // MARK: - CDN Helper private enum CDNHelper { static var cdnHost = "https://cdn.bowong.cc/" static func ensureFullUrl(_ keyOrUrl: String) -> String { if keyOrUrl.hasPrefix("http://") || keyOrUrl.hasPrefix("https://") { return keyOrUrl } let key = keyOrUrl.hasPrefix("/") ? String(keyOrUrl.dropFirst()) : keyOrUrl return cdnHost + key } static func isImage(_ url: String) -> Bool { let ext = URL(string: url)?.pathExtension.lowercased() ?? "" return ["jpg", "jpeg", "png", "webp", "gif"].contains(ext) } } // MARK: - View // // Demo 演示 SDK 全部功能: // 1. Scan & Connect — sdk.scan() / sdk.connect(deviceId:) / sdk.disconnect() // 2. Device — sdk.getDeviceInfo() / sdk.getVersion() / sdk.bind(userId:) / sdk.unbind(userId:) // 3. Transfer — sdk.transferMedia(fileUrl:) 一步传输(内部自动 ANI 转换 + prepare + transfer) // 4. Firmware — sdk.fetchLatestFirmware() 查询 + sdk.transferFile(fileUri:commandType:.otaPackage) OTA // 5. Device Files — sdk.deleteFile(key:) 删除设备文件 // // 可观察状态:btState / connectedDevice / deviceInfo / version / transferProgress / error struct WrapperTestView: View { @EnvironmentObject var sdk: DuooomiBleSDK @State private var userId = "test-user-001" @State private var fileUrl = "https://cdn.bowong.cc/material/569f48a8e29f47859b3a9808be37f94c.mp4" // Firmware @State private var firmwareBrand = "duomi" @State private var firmwareStatus = "DRAFT" @State private var firmwareInfo: FirmwareInfo? = nil @State private var firmwareLoading = false @State private var firmwareError: String? = nil // State @State private var isBusy = false @State private var connectingDeviceId: String? @State private var deviceFiles: [String] = [] @State private var logs: [LogLine] = [] struct LogLine: Identifiable { let id = UUID() let level: Level let message: String enum Level { case info, success, error } } var body: some View { List { statusSection scanSection deviceSection transferSection firmwareSection deviceFilesSection logSection } .navigationTitle("SDK Demo") .scrollDismissesKeyboard(.interactively) .onAppear { CDNHelper.cdnHost = sdk.config.cdnHost firmwareBrand = sdk.config.firmwareIdentifier firmwareStatus = sdk.config.firmwareStatus } } // MARK: - Sections private var statusSection: some View { Section("Status") { LabeledContent("BLE", value: sdk.btState.rawValue) LabeledContent("Device", value: sdk.connectedDevice?.name ?? "—") LabeledContent("Version", value: sdk.version.isEmpty ? "—" : sdk.version) if sdk.transferProgress > 0 { ProgressView(value: Double(sdk.transferProgress), total: 100) { Text("\(sdk.transferProgress)%").font(.caption) } } if let err = sdk.error { Text(err).foregroundStyle(.red).font(.caption) } } } private var scanSection: some View { Section("Scan & Connect") { HStack { Button("Scan") { sdk.scan() log("→ scan()") } .buttonStyle(.borderedProminent) Button("Stop") { sdk.stopScan() log("→ stopScan()") } .buttonStyle(.bordered) } ForEach(sdk.discoveredDevices) { device in HStack { VStack(alignment: .leading) { Text(device.name ?? "Unknown") Text(device.id).font(.caption2).foregroundStyle(.secondary) } Spacer() Text("\(device.rssi) dBm").font(.caption).foregroundStyle(.secondary) if sdk.connectedDevice?.id == device.id { Button("Disconnect") { Task { await runDisconnect() } } .buttonStyle(.bordered) .controlSize(.small) .tint(.red) } else if connectingDeviceId == device.id { ProgressView().controlSize(.small) } else { Button("Connect") { Task { await runConnect(deviceId: device.id) } } .buttonStyle(.bordered) .controlSize(.small) .disabled(connectingDeviceId != nil) } } } } } private var deviceSection: some View { Section("Device") { LabeledContent("Brand", value: sdk.deviceInfo?.brand ?? "—") LabeledContent("Size", value: sdk.deviceInfo?.size ?? "—") LabeledContent("Power", value: sdk.deviceInfo.map { "\($0.powerlevel)%" } ?? "—") LabeledContent("Storage", value: sdk.deviceInfo.map { "\($0.freespace)/\($0.allspace)" } ?? "—") HStack { Button("getDeviceInfo") { Task { await runGetDeviceInfo() } } Button("getVersion") { Task { await runGetVersion() } } } .buttonStyle(.bordered) HStack { TextField("userId", text: $userId) .textFieldStyle(.roundedBorder) Button("bind") { Task { await runBind() } } .buttonStyle(.bordered) Button("unbind") { Task { await runUnbind() } } .buttonStyle(.bordered) } } } private var transferSection: some View { Section("Transfer") { TextField("File URL (mp4/jpg/png/ani)", text: $fileUrl) .textFieldStyle(.roundedBorder) .autocapitalization(.none) .disableAutocorrection(true) Button("Transfer") { Task { await runTransfer() } } .buttonStyle(.borderedProminent) .disabled(fileUrl.isEmpty || sdk.connectedDevice == nil || isBusy) } } private static let statusOptions = ["DRAFT", "PUBLISHED"] private var firmwareSection: some View { Section("Firmware Update") { LabeledContent("Current Version", value: sdk.version.isEmpty ? "—" : sdk.version) HStack { Text("Identifier") TextField("duomi", text: $firmwareBrand) .textFieldStyle(.roundedBorder) .autocapitalization(.none) .disableAutocorrection(true) } Picker("Status", selection: $firmwareStatus) { ForEach(Self.statusOptions, id: \.self) { Text($0) } } .pickerStyle(.segmented) if let info = firmwareInfo { VStack(alignment: .leading, spacing: 6) { LabeledContent("Latest", value: info.version) if let size = info.fileSize { let bytes = Int(size) ?? 0 LabeledContent("Size", value: ByteCountFormatter.string(fromByteCount: Int64(bytes), countStyle: .file)) } if let md5 = info.fileMd5, !md5.isEmpty { LabeledContent("MD5", value: md5).font(.caption2) } if let desc = info.description, !desc.isEmpty { Text(desc).font(.caption).foregroundStyle(.secondary) } Text(info.fileUrl).font(.caption2).foregroundStyle(.secondary).lineLimit(2) } } if let err = firmwareError { Text(err).font(.caption).foregroundStyle(.red) } if firmwareLoading { HStack { ProgressView().controlSize(.small) Text("Fetching...").font(.caption).foregroundStyle(.secondary) } } HStack { Button("Get Update Info") { Task { await runFetchFirmwareInfo() } } .buttonStyle(.bordered) .disabled(firmwareLoading || firmwareBrand.isEmpty) Button("Upgrade") { Task { await runUpgradeFirmware() } } .buttonStyle(.borderedProminent) .disabled(firmwareInfo?.fileUrl.isEmpty != false || sdk.connectedDevice == nil || isBusy || firmwareLoading) } } } private var deviceFilesSection: some View { Section("Device Files (\(deviceFiles.count))") { if deviceFiles.isEmpty { Text("Bind 后显示设备文件") .font(.caption) .foregroundStyle(.secondary) } ForEach(deviceFiles, id: \.self) { rawKey in let fileUrl = CDNHelper.ensureFullUrl(rawKey) HStack { if CDNHelper.isImage(fileUrl), let url = URL(string: fileUrl) { AsyncImage(url: url) { phase in switch phase { case .success(let image): image.resizable().aspectRatio(contentMode: .fill) case .failure: Image(systemName: "photo").foregroundStyle(.secondary) default: ProgressView() } } .frame(width: 50, height: 50) .clipShape(RoundedRectangle(cornerRadius: 6)) } else { Image(systemName: "film") .frame(width: 50, height: 50) .foregroundStyle(.secondary) } Text(rawKey) .font(.caption2) .lineLimit(2) Spacer() Button(role: .destructive) { Task { await runDeleteFile(rawKey) } } label: { Image(systemName: "trash") } .buttonStyle(.borderless) .disabled(isBusy) } } } } private var logSection: some View { Section("Log (\(logs.count))") { ForEach(logs.prefix(80)) { line in Text(line.message) .font(.caption) .foregroundStyle(line.level == .error ? .red : line.level == .success ? .green : .primary) } } } // MARK: - Logging private func log(_ message: String, level: LogLine.Level = .info) { logs.insert(LogLine(level: level, message: message), at: 0) if logs.count > 200 { logs.removeLast() } } // MARK: - Actions // 每个 run* 方法对应一个 SDK API 调用,展示输入/输出和错误处理 /// sdk.connect(deviceId:) → DiscoveredDevice private func runConnect(deviceId: String) async { connectingDeviceId = deviceId defer { connectingDeviceId = nil } log("→ connect(\(deviceId))") do { let device = try await sdk.connect(deviceId: deviceId) log("connect ✓ \(device.name ?? device.id)", level: .success) } catch { log("connect ✗ \(error.localizedDescription)", level: .error) } } /// sdk.disconnect() private func runDisconnect() async { log("→ disconnect") do { try await sdk.disconnect() deviceFiles = [] log("disconnect ✓", level: .success) } catch { log("disconnect ✗ \(error.localizedDescription)", level: .error) } } /// sdk.getDeviceInfo() → DeviceInfo private func runGetDeviceInfo() async { isBusy = true defer { isBusy = false } log("→ getDeviceInfo") do { let info = try await sdk.getDeviceInfo() log("getDeviceInfo ✓ brand=\(info.brand) power=\(info.powerlevel)%", level: .success) } catch { log("getDeviceInfo ✗ \(error.localizedDescription)", level: .error) } } /// sdk.getVersion() → VersionInfo private func runGetVersion() async { isBusy = true defer { isBusy = false } log("→ getVersion") do { let info = try await sdk.getVersion() log("getVersion ✓ \(info.version)", level: .success) } catch { log("getVersion ✗ \(error.localizedDescription)", level: .error) } } /// sdk.bind(userId:) → BindingResponse (sn + contents)。绑定后才能查看设备文件 private func runBind() async { isBusy = true defer { isBusy = false } log("→ bind(\(userId))") do { let result = try await sdk.bind(userId: userId) log("bind ✓ sn=\(result.sn) contents=\(result.contents.count)", level: .success) deviceFiles = result.contents } catch { log("bind ✗ \(error.localizedDescription)", level: .error) } } /// sdk.unbind(userId:) → UnbindResponse。解绑后该账号的设备文件将被删除 private func runUnbind() async { isBusy = true defer { isBusy = false } log("→ unbind(\(userId))") do { _ = try await sdk.unbind(userId: userId) deviceFiles = [] log("unbind ✓", level: .success) } catch { log("unbind ✗ \(error.localizedDescription)", level: .error) } } /// sdk.transferMedia(fileUrl:) — 一步完成:自动转换 ANI + prepare + transfer private func runTransfer() async { isBusy = true defer { isBusy = false } log("→ transferMedia(\(fileUrl))") do { try await sdk.transferMedia(fileUrl: fileUrl) log("transfer ✓", level: .success) } catch { log("transfer ✗ \(error.localizedDescription)", level: .error) } } /// sdk.deleteFile(key:) → DeleteFileResponse private func runDeleteFile(_ rawKey: String) async { isBusy = true defer { isBusy = false } log("→ deleteFile(\(rawKey))") do { _ = try await sdk.deleteFile(key: rawKey) log("deleteFile ✓", level: .success) deviceFiles.removeAll { $0 == rawKey } } catch { log("deleteFile ✗ \(error.localizedDescription)", level: .error) } } /// sdk.fetchLatestFirmware(identifier:status:) → FirmwareInfo? private func runFetchFirmwareInfo() async { let brand = firmwareBrand.trimmingCharacters(in: .whitespacesAndNewlines) guard !brand.isEmpty else { firmwareError = "No identifier specified" return } firmwareLoading = true firmwareError = nil defer { firmwareLoading = false } log("→ fetchLatestFirmware(\(brand), \(firmwareStatus))") do { let info = try await sdk.fetchLatestFirmware(identifier: brand, status: firmwareStatus) firmwareInfo = info if let info { log("firmware latest: \(info.version)", level: .success) } else { log("no firmware available", level: .info) } } catch { firmwareError = error.localizedDescription log("firmware info ✗ \(error.localizedDescription)", level: .error) } } /// 固件升级第二步:sdk.upgradeFirmware(fileUrl:) private func runUpgradeFirmware() async { guard let info = firmwareInfo, !info.fileUrl.isEmpty else { return } isBusy = true defer { isBusy = false } log("→ upgradeFirmware \(info.version)") do { try await sdk.upgradeFirmware(fileUrl: info.fileUrl) log("OTA ✓", level: .success) } catch { log("OTA ✗ \(error.localizedDescription)", level: .error) } } }