Files
duooomi-ios-sdk/demo/Sources/WrapperTestView.swift
2026-04-13 18:41:46 +08:00

458 lines
17 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 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)
LabeledContent("Has Update", value: sdk.hasNewerFirmware(deviceVersion: sdk.version, serverVersion: info.version) ? "Yes" : "No")
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 {
let hasUpdate = sdk.hasNewerFirmware(deviceVersion: sdk.version, serverVersion: info.version)
log("firmware latest: \(info.version), hasUpdate=\(hasUpdate)", 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)
}
}
}