Initial commit: DuooomiBleSDK test app with XcodeGen
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Xcode
|
||||||
|
*.xcodeproj/
|
||||||
|
*.xcworkspace/
|
||||||
|
!*.xcworkspace/contents.xcworkspacedata
|
||||||
|
|
||||||
|
# XcodeGen generates xcodeproj from project.yml, no need to track it
|
||||||
|
# To regenerate: xcodegen generate
|
||||||
|
|
||||||
|
# User-specific Xcode data
|
||||||
|
xcuserdata/
|
||||||
|
*.xcuserstate
|
||||||
|
|
||||||
|
# Build
|
||||||
|
build/
|
||||||
|
DerivedData/
|
||||||
|
*.ipa
|
||||||
|
*.dSYM.zip
|
||||||
|
*.dSYM
|
||||||
|
|
||||||
|
# CocoaPods (if used)
|
||||||
|
Pods/
|
||||||
|
|
||||||
|
# SPM
|
||||||
|
.build/
|
||||||
|
Package.resolved
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
**/.DS_Store
|
||||||
|
|
||||||
|
# Fastlane
|
||||||
|
fastlane/report.xml
|
||||||
|
fastlane/Preview.html
|
||||||
|
fastlane/screenshots/**/*.png
|
||||||
|
fastlane/test_output
|
||||||
|
|
||||||
|
# Code Injection
|
||||||
|
iOSInjectionProject/
|
||||||
28
DuooomiTest/Info.plist
Normal file
28
DuooomiTest/Info.plist
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Duooomi Test</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0.0</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||||
|
<string>This app needs Bluetooth to communicate with your device</string>
|
||||||
|
<key>UILaunchScreen</key>
|
||||||
|
<dict/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
205
DuooomiTest/Sources/ContentView.swift
Normal file
205
DuooomiTest/Sources/ContentView.swift
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ContentView: View {
|
||||||
|
@EnvironmentObject var sdk: SDKManager
|
||||||
|
@State private var userId = "test-user-001"
|
||||||
|
@State private var fileKey = ""
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationView {
|
||||||
|
List {
|
||||||
|
statusSection
|
||||||
|
scanSection
|
||||||
|
deviceSection
|
||||||
|
fileSection
|
||||||
|
logSection
|
||||||
|
}
|
||||||
|
.navigationTitle("BLE SDK Test")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Status
|
||||||
|
|
||||||
|
private var statusSection: some View {
|
||||||
|
Section("Status") {
|
||||||
|
HStack {
|
||||||
|
Text("Connection")
|
||||||
|
Spacer()
|
||||||
|
Text(sdk.btState)
|
||||||
|
.foregroundStyle(sdk.btState == "connected" ? .green : .secondary)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let error = sdk.error {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "exclamationmark.triangle.fill")
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
Text(error)
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("SDK Ready")
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: sdk.isSDKReady ? "checkmark.circle.fill" : "xmark.circle")
|
||||||
|
.foregroundStyle(sdk.isSDKReady ? .green : .red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Scan
|
||||||
|
|
||||||
|
private var scanSection: some View {
|
||||||
|
Section("Scan") {
|
||||||
|
HStack {
|
||||||
|
Button("Scan") { sdk.scan() }
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.disabled(sdk.btState == "scanning")
|
||||||
|
|
||||||
|
Button("Stop") { sdk.stopScan() }
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.disabled(sdk.btState != "scanning")
|
||||||
|
}
|
||||||
|
|
||||||
|
if sdk.discoveredDevices.isEmpty && sdk.btState == "scanning" {
|
||||||
|
HStack {
|
||||||
|
ProgressView()
|
||||||
|
Text("Searching...")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ForEach(sdk.discoveredDevices) { device in
|
||||||
|
Button {
|
||||||
|
sdk.stopScan()
|
||||||
|
sdk.connect(deviceId: device.id)
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text(device.name)
|
||||||
|
.font(.body)
|
||||||
|
Text(device.id)
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
Text("\(device.rssi) dBm")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Device
|
||||||
|
|
||||||
|
private var deviceSection: some View {
|
||||||
|
Section("Device") {
|
||||||
|
if sdk.btState == "connected" {
|
||||||
|
if let device = sdk.connectedDevice {
|
||||||
|
LabeledContent("Name", value: device["name"] as? String ?? "—")
|
||||||
|
LabeledContent("ID", value: device["id"] as? String ?? "—")
|
||||||
|
}
|
||||||
|
|
||||||
|
LabeledContent("Version", value: sdk.version.isEmpty ? "—" : sdk.version)
|
||||||
|
|
||||||
|
if let info = sdk.deviceInfo {
|
||||||
|
LabeledContent("Brand", value: info.brand)
|
||||||
|
LabeledContent("Screen", value: info.screenSize)
|
||||||
|
LabeledContent("Battery", value: "\(info.batteryLevel)%")
|
||||||
|
LabeledContent("Storage", value: formatStorage(free: info.freeSpace, total: info.totalSpace))
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Button("Get Info") { sdk.getDeviceInfo() }
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
Button("Get Version") { sdk.getVersion() }
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
TextField("User ID", text: $userId)
|
||||||
|
.textFieldStyle(.roundedBorder)
|
||||||
|
Button("Bind") { sdk.bind(userId: userId) }
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Button("Unbind") { sdk.unbind(userId: userId) }
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
Button("Disconnect") { sdk.disconnect() }
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.tint(.red)
|
||||||
|
}
|
||||||
|
} else if sdk.btState == "connecting" {
|
||||||
|
HStack {
|
||||||
|
ProgressView()
|
||||||
|
Text("Connecting...")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text("No device connected")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - File
|
||||||
|
|
||||||
|
private var fileSection: some View {
|
||||||
|
Section("File Transfer") {
|
||||||
|
if sdk.btState == "connected" {
|
||||||
|
if sdk.transferProgress > 0 {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("Transfer: \(sdk.transferProgress)%")
|
||||||
|
.font(.caption)
|
||||||
|
ProgressView(value: Double(sdk.transferProgress), total: 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
TextField("File key to delete", text: $fileKey)
|
||||||
|
.textFieldStyle(.roundedBorder)
|
||||||
|
Button("Delete") { sdk.deleteFile(key: fileKey) }
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.tint(.red)
|
||||||
|
.disabled(fileKey.isEmpty)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text("Connect a device first")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Log
|
||||||
|
|
||||||
|
private var logSection: some View {
|
||||||
|
Section("Event Log (\(sdk.logs.count))") {
|
||||||
|
ForEach(sdk.logs.prefix(50)) { entry in
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(entry.message)
|
||||||
|
.font(.caption)
|
||||||
|
Text(entry.time, style: .time)
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.tertiary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sdk.logs.isEmpty {
|
||||||
|
Text("No events yet")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
|
||||||
|
private func formatStorage(free: Int, total: Int) -> String {
|
||||||
|
let freeMB = free / 1_048_576
|
||||||
|
let totalMB = total / 1_048_576
|
||||||
|
return "\(freeMB)MB / \(totalMB)MB"
|
||||||
|
}
|
||||||
|
}
|
||||||
24
DuooomiTest/Sources/DuooomiTestApp.swift
Normal file
24
DuooomiTest/Sources/DuooomiTestApp.swift
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import DuooomiBleSDK
|
||||||
|
|
||||||
|
class AppDelegate: ExpoBrownfieldAppDelegate {
|
||||||
|
override func application(
|
||||||
|
_ application: UIApplication,
|
||||||
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
||||||
|
) -> Bool {
|
||||||
|
SDKManager.shared.initialize()
|
||||||
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct DuooomiTestApp: App {
|
||||||
|
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||||
|
|
||||||
|
var body: some Scene {
|
||||||
|
WindowGroup {
|
||||||
|
ContentView()
|
||||||
|
.environmentObject(SDKManager.shared)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
229
DuooomiTest/Sources/SDKManager.swift
Normal file
229
DuooomiTest/Sources/SDKManager.swift
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import DuooomiBleSDK
|
||||||
|
|
||||||
|
struct DiscoveredDevice: Identifiable {
|
||||||
|
let id: String
|
||||||
|
let name: String
|
||||||
|
let rssi: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DeviceInfo {
|
||||||
|
let brand: String
|
||||||
|
let screenSize: String
|
||||||
|
let freeSpace: Int
|
||||||
|
let totalSpace: Int
|
||||||
|
let batteryLevel: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LogEntry: Identifiable {
|
||||||
|
let id = UUID()
|
||||||
|
let time: Date
|
||||||
|
let message: String
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class SDKManager: ObservableObject {
|
||||||
|
static let shared = SDKManager()
|
||||||
|
|
||||||
|
// State
|
||||||
|
@Published var btState = "idle"
|
||||||
|
@Published var connectedDevice: [String: Any]? = nil
|
||||||
|
@Published var deviceInfo: DeviceInfo? = nil
|
||||||
|
@Published var version = ""
|
||||||
|
@Published var isActivated = false
|
||||||
|
@Published var transferProgress: Int = 0
|
||||||
|
@Published var error: String? = nil
|
||||||
|
@Published var discoveredDevices: [DiscoveredDevice] = []
|
||||||
|
@Published var logs: [LogEntry] = []
|
||||||
|
@Published var isSDKReady = false
|
||||||
|
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
private var listenerIds: [String] = []
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
func initialize() {
|
||||||
|
ReactNativeHostManager.shared.initialize()
|
||||||
|
|
||||||
|
// 加载 RN 视图启动 JS 运行时
|
||||||
|
do {
|
||||||
|
let view = try ReactNativeHostManager.shared.loadView(
|
||||||
|
moduleName: "main", initialProps: nil, launchOptions: nil)
|
||||||
|
// 保持引用防止释放
|
||||||
|
_rnView = view
|
||||||
|
log("SDK initialized, RN runtime started")
|
||||||
|
isSDKReady = true
|
||||||
|
} catch {
|
||||||
|
log("SDK init failed: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
|
||||||
|
setupSubscriptions()
|
||||||
|
setupMessageListener()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _rnView: UIView?
|
||||||
|
|
||||||
|
// MARK: - State Subscriptions
|
||||||
|
|
||||||
|
private func setupSubscriptions() {
|
||||||
|
BrownfieldState.subscribe("btState") { [weak self] value in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let state = value as? String ?? "idle"
|
||||||
|
self?.btState = state
|
||||||
|
self?.log("btState → \(state)")
|
||||||
|
}
|
||||||
|
}.store(in: &cancellables)
|
||||||
|
|
||||||
|
BrownfieldState.subscribe("connectedDevice") { [weak self] value in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self?.connectedDevice = value as? [String: Any]
|
||||||
|
if let device = value as? [String: Any] {
|
||||||
|
self?.log("connectedDevice → \(device["name"] ?? "unknown")")
|
||||||
|
} else {
|
||||||
|
self?.log("connectedDevice → nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.store(in: &cancellables)
|
||||||
|
|
||||||
|
BrownfieldState.subscribe("deviceInfo") { [weak self] value in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let dict = value as? [String: Any] {
|
||||||
|
self?.deviceInfo = DeviceInfo(
|
||||||
|
brand: dict["brand"] as? String ?? "",
|
||||||
|
screenSize: dict["screenSize"] as? String ?? "",
|
||||||
|
freeSpace: dict["freeSpace"] as? Int ?? 0,
|
||||||
|
totalSpace: dict["totalSpace"] as? Int ?? 0,
|
||||||
|
batteryLevel: dict["batteryLevel"] as? Int ?? 0
|
||||||
|
)
|
||||||
|
self?.log("deviceInfo updated")
|
||||||
|
} else {
|
||||||
|
self?.deviceInfo = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.store(in: &cancellables)
|
||||||
|
|
||||||
|
BrownfieldState.subscribe("version") { [weak self] value in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self?.version = value as? String ?? ""
|
||||||
|
if let v = value as? String {
|
||||||
|
self?.log("version → \(v)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.store(in: &cancellables)
|
||||||
|
|
||||||
|
BrownfieldState.subscribe("isActivated") { [weak self] value in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self?.isActivated = value as? Bool ?? false
|
||||||
|
}
|
||||||
|
}.store(in: &cancellables)
|
||||||
|
|
||||||
|
BrownfieldState.subscribe("transferProgress") { [weak self] value in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self?.transferProgress = value as? Int ?? 0
|
||||||
|
}
|
||||||
|
}.store(in: &cancellables)
|
||||||
|
|
||||||
|
BrownfieldState.subscribe("error") { [weak self] value in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self?.error = value as? String
|
||||||
|
if let e = value as? String {
|
||||||
|
self?.log("ERROR: \(e)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.store(in: &cancellables)
|
||||||
|
|
||||||
|
BrownfieldState.subscribe("discoveredDevices") { [weak self] value in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let arr = value as? [[String: Any]] {
|
||||||
|
self?.discoveredDevices = arr.compactMap { dict in
|
||||||
|
guard let id = dict["id"] as? String else { return nil }
|
||||||
|
return DiscoveredDevice(
|
||||||
|
id: id,
|
||||||
|
name: dict["name"] as? String ?? "Unknown",
|
||||||
|
rssi: dict["rssi"] as? Int ?? 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self?.discoveredDevices = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Message Listener
|
||||||
|
|
||||||
|
private func setupMessageListener() {
|
||||||
|
let id = BrownfieldMessaging.addListener { [weak self] message in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let event = message["event"] as? String ?? "unknown"
|
||||||
|
self?.log("Event: \(event) — \(message)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listenerIds.append(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Actions
|
||||||
|
|
||||||
|
func scan() {
|
||||||
|
log("→ scan")
|
||||||
|
BrownfieldMessaging.sendMessage(["action": "scan"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopScan() {
|
||||||
|
log("→ stopScan")
|
||||||
|
BrownfieldMessaging.sendMessage(["action": "stopScan"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func connect(deviceId: String) {
|
||||||
|
log("→ connect \(deviceId)")
|
||||||
|
BrownfieldMessaging.sendMessage(["action": "connect", "deviceId": deviceId])
|
||||||
|
}
|
||||||
|
|
||||||
|
func disconnect() {
|
||||||
|
log("→ disconnect")
|
||||||
|
BrownfieldMessaging.sendMessage(["action": "disconnect"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func bind(userId: String) {
|
||||||
|
log("→ bind \(userId)")
|
||||||
|
BrownfieldMessaging.sendMessage(["action": "bind", "userId": userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
func unbind(userId: String) {
|
||||||
|
log("→ unbind \(userId)")
|
||||||
|
BrownfieldMessaging.sendMessage(["action": "unbind", "userId": userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDeviceInfo() {
|
||||||
|
log("→ getDeviceInfo")
|
||||||
|
BrownfieldMessaging.sendMessage(["action": "getDeviceInfo"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func getVersion() {
|
||||||
|
log("→ getVersion")
|
||||||
|
BrownfieldMessaging.sendMessage(["action": "getVersion"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteFile(key: String) {
|
||||||
|
log("→ deleteFile \(key)")
|
||||||
|
BrownfieldMessaging.sendMessage(["action": "deleteFile", "key": key])
|
||||||
|
}
|
||||||
|
|
||||||
|
func transferFile(fileUri: String, commandType: Int) {
|
||||||
|
log("→ transferFile \(fileUri)")
|
||||||
|
BrownfieldMessaging.sendMessage([
|
||||||
|
"action": "transferFile",
|
||||||
|
"fileUri": fileUri,
|
||||||
|
"commandType": commandType
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Logging
|
||||||
|
|
||||||
|
private func log(_ message: String) {
|
||||||
|
let entry = LogEntry(time: Date(), message: message)
|
||||||
|
logs.insert(entry, at: 0)
|
||||||
|
if logs.count > 200 { logs.removeLast() }
|
||||||
|
}
|
||||||
|
}
|
||||||
52
README.md
Normal file
52
README.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# DuooomiTest
|
||||||
|
|
||||||
|
DuooomiBleSDK 的 iOS 测试应用,用于验证蓝牙设备的扫描、连接、绑定、文件传输等功能。
|
||||||
|
|
||||||
|
## 环境要求
|
||||||
|
|
||||||
|
- Xcode 26+
|
||||||
|
- iOS 16.0+
|
||||||
|
- Swift 6.0
|
||||||
|
- [XcodeGen](https://github.com/yonaskolb/XcodeGen)
|
||||||
|
|
||||||
|
## 项目依赖
|
||||||
|
|
||||||
|
- `DuooomiBleSDK.xcframework` — 蓝牙通信 SDK
|
||||||
|
- `hermesvm.xcframework` — JS 运行时
|
||||||
|
|
||||||
|
依赖文件位于 `../duooomi-ble-sdk/artifacts/`,请确保该路径下存在对应的 xcframework。
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 安装 XcodeGen(如果还没有)
|
||||||
|
brew install xcodegen
|
||||||
|
|
||||||
|
# 2. 生成 Xcode 项目
|
||||||
|
xcodegen generate
|
||||||
|
|
||||||
|
# 3. 打开项目
|
||||||
|
open DuooomiTest.xcodeproj
|
||||||
|
```
|
||||||
|
|
||||||
|
在 Xcode 中设置你的 `DEVELOPMENT_TEAM`,然后运行到真机即可(蓝牙功能需要真机调试)。
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
├── project.yml # XcodeGen 配置
|
||||||
|
├── DuooomiTest/
|
||||||
|
│ ├── Info.plist
|
||||||
|
│ └── Sources/
|
||||||
|
│ ├── DuooomiTestApp.swift # App 入口
|
||||||
|
│ ├── ContentView.swift # 测试界面
|
||||||
|
│ └── SDKManager.swift # SDK 封装层
|
||||||
|
```
|
||||||
|
|
||||||
|
## 功能
|
||||||
|
|
||||||
|
- 蓝牙设备扫描与连接
|
||||||
|
- 设备信息查询(品牌、屏幕、电量、存储)
|
||||||
|
- 用户绑定/解绑
|
||||||
|
- 文件传输与删除
|
||||||
|
- 实时事件日志
|
||||||
38
project.yml
Normal file
38
project.yml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: DuooomiTest
|
||||||
|
options:
|
||||||
|
bundleIdPrefix: com.duooomi
|
||||||
|
deploymentTarget:
|
||||||
|
iOS: "16.0"
|
||||||
|
xcodeVersion: "26.0"
|
||||||
|
generateEmptyDirectories: true
|
||||||
|
|
||||||
|
settings:
|
||||||
|
base:
|
||||||
|
SWIFT_VERSION: "6.0"
|
||||||
|
CODE_SIGN_STYLE: Automatic
|
||||||
|
DEVELOPMENT_TEAM: ""
|
||||||
|
|
||||||
|
targets:
|
||||||
|
DuooomiTest:
|
||||||
|
type: application
|
||||||
|
platform: iOS
|
||||||
|
sources:
|
||||||
|
- path: DuooomiTest/Sources
|
||||||
|
info:
|
||||||
|
path: DuooomiTest/Info.plist
|
||||||
|
properties:
|
||||||
|
CFBundleDisplayName: Duooomi Test
|
||||||
|
CFBundleShortVersionString: "1.0.0"
|
||||||
|
CFBundleVersion: "1"
|
||||||
|
NSBluetoothAlwaysUsageDescription: "This app needs Bluetooth to communicate with your device"
|
||||||
|
UILaunchScreen: {}
|
||||||
|
settings:
|
||||||
|
base:
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER: com.duooomi.test
|
||||||
|
PRODUCT_NAME: DuooomiTest
|
||||||
|
LD_RUNPATH_SEARCH_PATHS: "$(inherited) @executable_path/Frameworks"
|
||||||
|
dependencies:
|
||||||
|
- framework: ../duooomi-ble-sdk/artifacts/DuooomiBleSDK.xcframework
|
||||||
|
embed: true
|
||||||
|
- framework: ../duooomi-ble-sdk/artifacts/hermesvm.xcframework
|
||||||
|
embed: true
|
||||||
Reference in New Issue
Block a user