- 增加了wrapper处理原生app的交互
This commit is contained in:
9
app.json
9
app.json
@@ -16,7 +16,14 @@
|
||||
"libraryName": "duooomi-ble-sdk",
|
||||
"group": "com.duooomi",
|
||||
"package": "com.duooomi.ble.sdk",
|
||||
"version": "1.0.0"
|
||||
"version": "1.0.0",
|
||||
"publishing": [
|
||||
{
|
||||
"type": "localDirectory",
|
||||
"name": "dist",
|
||||
"path": "./dist/android/maven"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -57,6 +57,7 @@ class BleSDK {
|
||||
|
||||
// 批量设备发现缓冲
|
||||
private pendingDevices: Array<{ id: string; name: string | null; rssi: number | null }> = []
|
||||
private discoveredDevices: Array<{ id: string; name: string | null; rssi: number | null }> = []
|
||||
private flushTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
initialize() {
|
||||
@@ -146,59 +147,84 @@ class BleSDK {
|
||||
// ====== 接收原生 App 的消息指令 ======
|
||||
private setupMessageHandler() {
|
||||
addMessageListener((event: Record<string, any>) => {
|
||||
const { action, ...params } = event
|
||||
console.log(`[BleSDK] Received action: ${action}`, params)
|
||||
void this.handleAction(event)
|
||||
})
|
||||
}
|
||||
|
||||
private async handleAction(event: Record<string, any>) {
|
||||
const { action, requestId, ...params } = event
|
||||
console.log(`[BleSDK] Received action: ${action}`, params)
|
||||
|
||||
try {
|
||||
let data: any = null
|
||||
|
||||
switch (action) {
|
||||
case 'scan':
|
||||
this.startScan()
|
||||
await this.startScan()
|
||||
data = { started: true }
|
||||
break
|
||||
case 'stopScan':
|
||||
this.stopScan()
|
||||
data = { stopped: true }
|
||||
break
|
||||
case 'connect':
|
||||
this.connect(params.deviceId)
|
||||
data = await this.connect(params.deviceId)
|
||||
break
|
||||
case 'disconnect':
|
||||
this.disconnect()
|
||||
data = await this.disconnect()
|
||||
break
|
||||
case 'getDeviceInfo':
|
||||
this.getDeviceInfo()
|
||||
data = await this.getDeviceInfo()
|
||||
break
|
||||
case 'getVersion':
|
||||
this.getVersion()
|
||||
data = await this.getVersion()
|
||||
break
|
||||
case 'bind':
|
||||
this.bind(params.userId)
|
||||
data = await this.bind(params.userId)
|
||||
break
|
||||
case 'unbind':
|
||||
this.unbind(params.userId)
|
||||
data = await this.unbind(params.userId)
|
||||
break
|
||||
case 'deleteFile':
|
||||
this.deleteFile(params.key)
|
||||
data = await this.deleteFile(params.key)
|
||||
break
|
||||
case 'transferFile':
|
||||
this.transferFile(params.fileUri, params.commandType)
|
||||
data = await this.transferFile(params.fileUri, params.commandType)
|
||||
break
|
||||
default:
|
||||
sendMessage({ event: 'error', type: 'unknownAction', message: `Unknown action: ${action}` })
|
||||
throw new Error(`Unknown action: ${action}`)
|
||||
}
|
||||
})
|
||||
|
||||
if (requestId) {
|
||||
this.sendResponse(requestId, action, data)
|
||||
}
|
||||
} catch (e: any) {
|
||||
const message = e instanceof Error ? e.message : String(e)
|
||||
if (requestId) {
|
||||
this.sendErrorResponse(requestId, action, message)
|
||||
return
|
||||
}
|
||||
|
||||
sendMessage({ event: 'error', type: action ?? 'unknownAction', message })
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 蓝牙操作实现 ======
|
||||
|
||||
private async startScan() {
|
||||
try {
|
||||
const hasPerms = await this.requestPermissions()
|
||||
if (!hasPerms) {
|
||||
sendMessage({ event: 'error', type: 'permission', message: 'Bluetooth permission denied' })
|
||||
return
|
||||
}
|
||||
const hasPerms = await this.requestPermissions()
|
||||
if (!hasPerms) {
|
||||
const error = new Error('Bluetooth permission denied')
|
||||
this.updateState(STATE_KEYS.ERROR, error.message)
|
||||
sendMessage({ event: 'error', type: 'permission', message: error.message })
|
||||
throw error
|
||||
}
|
||||
|
||||
try {
|
||||
this.updateState(STATE_KEYS.BT_STATE, 'scanning')
|
||||
this.updateState(STATE_KEYS.DISCOVERED_DEVICES, [])
|
||||
this.pendingDevices = []
|
||||
this.discoveredDevices = []
|
||||
|
||||
await this.bleClient.startScan(
|
||||
[BLE_UUIDS.SERVICE],
|
||||
@@ -223,6 +249,7 @@ class BleSDK {
|
||||
this.updateState(STATE_KEYS.BT_STATE, 'idle')
|
||||
this.updateState(STATE_KEYS.ERROR, e.message)
|
||||
sendMessage({ event: 'error', type: 'scan', message: e.message })
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,8 +262,7 @@ class BleSDK {
|
||||
|
||||
private async connect(deviceId: string) {
|
||||
if (!deviceId) {
|
||||
sendMessage({ event: 'error', type: 'connect', message: 'deviceId is required' })
|
||||
return
|
||||
throw new Error('deviceId is required')
|
||||
}
|
||||
|
||||
this.stopScan()
|
||||
@@ -258,10 +284,15 @@ class BleSDK {
|
||||
|
||||
// 自动获取设备信息
|
||||
this.getDeviceInfo().catch(() => {})
|
||||
return {
|
||||
id: device.id,
|
||||
name: device.name || device.localName || null,
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.updateState(STATE_KEYS.BT_STATE, 'idle')
|
||||
this.updateState(STATE_KEYS.ERROR, e.message)
|
||||
sendMessage({ event: 'error', type: 'connect', message: e.message })
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,68 +309,75 @@ class BleSDK {
|
||||
this.updateState(STATE_KEYS.BT_STATE, 'disconnected')
|
||||
|
||||
sendMessage({ event: 'disconnected' })
|
||||
return { success: true }
|
||||
} catch (e: any) {
|
||||
sendMessage({ event: 'error', type: 'disconnect', message: e.message })
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private async getDeviceInfo() {
|
||||
if (!this.connectedDeviceId) return
|
||||
if (!this.connectedDeviceId) throw new Error('No device connected')
|
||||
try {
|
||||
const promise = this.createWaitable<DeviceInfo>('deviceInfo')
|
||||
await this.deviceInfoService.getDeviceInfo(this.connectedDeviceId)
|
||||
return await promise
|
||||
} catch (e: any) {
|
||||
sendMessage({ event: 'error', type: 'deviceInfo', message: e.message })
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private async getVersion() {
|
||||
if (!this.connectedDeviceId) return
|
||||
if (!this.connectedDeviceId) throw new Error('No device connected')
|
||||
try {
|
||||
const promise = this.createWaitable<VersionInfo>('versionInfo')
|
||||
await this.deviceInfoService.getDeviceVersion(this.connectedDeviceId)
|
||||
return await promise
|
||||
} catch (e: any) {
|
||||
sendMessage({ event: 'error', type: 'version', message: e.message })
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private async bind(userId: string) {
|
||||
if (!this.connectedDeviceId) return
|
||||
if (!this.connectedDeviceId) throw new Error('No device connected')
|
||||
try {
|
||||
const promise = this.createWaitable<BindingResponse>('bindDevice')
|
||||
await this.deviceInfoService.bindDevice(this.connectedDeviceId, userId)
|
||||
return await promise
|
||||
} catch (e: any) {
|
||||
sendMessage({ event: 'error', type: 'bind', message: e.message })
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private async unbind(userId: string) {
|
||||
if (!this.connectedDeviceId) return
|
||||
if (!this.connectedDeviceId) throw new Error('No device connected')
|
||||
try {
|
||||
const promise = this.createWaitable<UnBindResponse>('unbindDevice')
|
||||
await this.deviceInfoService.unbindDevice(this.connectedDeviceId, userId)
|
||||
return await promise
|
||||
} catch (e: any) {
|
||||
sendMessage({ event: 'error', type: 'unbind', message: e.message })
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteFile(key: string) {
|
||||
if (!this.connectedDeviceId) return
|
||||
if (!this.connectedDeviceId) throw new Error('No device connected')
|
||||
try {
|
||||
const promise = this.createWaitable<DeleteFileResponse>('deleteFile')
|
||||
await this.deviceInfoService.deleteFile(this.connectedDeviceId, key)
|
||||
return await promise
|
||||
} catch (e: any) {
|
||||
sendMessage({ event: 'error', type: 'deleteFile', message: e.message })
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private async transferFile(fileUri: string, commandType?: number) {
|
||||
if (!this.connectedDeviceId) return
|
||||
if (!this.connectedDeviceId) throw new Error('No device connected')
|
||||
try {
|
||||
this.updateState(STATE_KEYS.TRANSFER_PROGRESS, 0)
|
||||
const type = commandType ?? COMMAND_TYPES.TRANSFER_ANI_VIDEO
|
||||
@@ -356,9 +394,11 @@ class BleSDK {
|
||||
)
|
||||
|
||||
sendMessage({ event: 'transferComplete' })
|
||||
return { success: true }
|
||||
} catch (e: any) {
|
||||
this.updateState(STATE_KEYS.ERROR, e.message)
|
||||
sendMessage({ event: 'error', type: 'transfer', message: e.message })
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,7 +413,9 @@ class BleSDK {
|
||||
}
|
||||
|
||||
private queueDevice(device: { id: string; name: string | null; rssi: number | null }) {
|
||||
if (this.pendingDevices.some((d) => d.id === device.id)) return
|
||||
if (this.discoveredDevices.some((d) => d.id === device.id)) return
|
||||
|
||||
this.discoveredDevices.push(device)
|
||||
this.pendingDevices.push(device)
|
||||
|
||||
if (this.flushTimer) clearTimeout(this.flushTimer)
|
||||
@@ -382,12 +424,38 @@ class BleSDK {
|
||||
|
||||
private flushDevices() {
|
||||
if (this.pendingDevices.length === 0) return
|
||||
const devices = [...this.pendingDevices]
|
||||
const devices = [...this.discoveredDevices]
|
||||
this.pendingDevices = []
|
||||
this.updateState(STATE_KEYS.DISCOVERED_DEVICES, devices)
|
||||
sendMessage({ event: 'devicesFound', devices })
|
||||
}
|
||||
|
||||
private sendResponse(requestId: string, action: string, data: any) {
|
||||
sendMessage({ event: 'response', requestId, action, success: true, data: this.serializeForNative(data) })
|
||||
}
|
||||
|
||||
private sendErrorResponse(requestId: string, action: string | undefined, message: string) {
|
||||
sendMessage({ event: 'response', requestId, action, success: false, error: message })
|
||||
}
|
||||
|
||||
private serializeForNative(value: any): any {
|
||||
if (typeof value === 'bigint') {
|
||||
return value.toString()
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => this.serializeForNative(item))
|
||||
}
|
||||
|
||||
if (value && typeof value === 'object') {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, nestedValue]) => [key, this.serializeForNative(nestedValue)])
|
||||
)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
private createWaitable<T>(opId: string, timeout = 10000): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
|
||||
@@ -6,8 +6,12 @@
|
||||
"start": "expo start",
|
||||
"android": "expo run:android",
|
||||
"ios": "expo run:ios",
|
||||
"build:ios": "npx expo-brownfield build:ios",
|
||||
"build:android": "npx expo-brownfield build:android",
|
||||
"build:brownfield:ios": "pnpx expo-brownfield build:ios",
|
||||
"build:brownfield:android": "pnpx expo-brownfield build:android",
|
||||
"build:ios": "node ./scripts/package-ios.mjs",
|
||||
"build:android": "node ./scripts/package-android.mjs",
|
||||
"package:ios": "node ./scripts/package-ios.mjs",
|
||||
"package:android": "node ./scripts/package-android.mjs",
|
||||
"lint": "expo lint"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
38
scripts/package-android.mjs
Normal file
38
scripts/package-android.mjs
Normal file
@@ -0,0 +1,38 @@
|
||||
import { cpSync, existsSync, mkdirSync, rmSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const rootDir = path.resolve(__dirname, '..')
|
||||
const distDir = path.join(rootDir, 'dist', 'android')
|
||||
const mavenDir = path.join(distDir, 'maven')
|
||||
const wrapperDir = path.join(distDir, 'wrapper')
|
||||
const wrapperTemplateDir = path.join(rootDir, 'wrappers', 'android')
|
||||
|
||||
function run(command, args) {
|
||||
const executable = process.platform === 'win32' ? `${command}.cmd` : command
|
||||
const result = spawnSync(executable, args, {
|
||||
cwd: rootDir,
|
||||
stdio: 'inherit',
|
||||
})
|
||||
|
||||
if (result.status !== 0) {
|
||||
process.exit(result.status ?? 1)
|
||||
}
|
||||
}
|
||||
|
||||
rmSync(distDir, { recursive: true, force: true })
|
||||
mkdirSync(distDir, { recursive: true })
|
||||
|
||||
run('pnpx', ['expo-brownfield', 'build:android', '--release'])
|
||||
|
||||
if (!existsSync(mavenDir)) {
|
||||
throw new Error(`Missing Android Maven output: ${mavenDir}`)
|
||||
}
|
||||
|
||||
cpSync(wrapperTemplateDir, wrapperDir, { recursive: true })
|
||||
|
||||
console.log(`Android wrapper package ready: ${wrapperDir}`)
|
||||
console.log(`Android Maven repo ready: ${mavenDir}`)
|
||||
64
scripts/package-ios.mjs
Normal file
64
scripts/package-ios.mjs
Normal file
@@ -0,0 +1,64 @@
|
||||
import { cpSync, existsSync, mkdirSync, readdirSync, rmSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const rootDir = path.resolve(__dirname, '..')
|
||||
const distDir = path.join(rootDir, 'dist', 'ios')
|
||||
const tempArtifactsDir = path.join(distDir, '.artifacts')
|
||||
const packageDir = path.join(distDir, 'DuooomiBleWrapper')
|
||||
const frameworksDir = path.join(packageDir, 'Frameworks')
|
||||
const wrapperTemplateDir = path.join(rootDir, 'wrappers', 'ios')
|
||||
|
||||
function run(command, args) {
|
||||
const executable = process.platform === 'win32' ? `${command}.cmd` : command
|
||||
const result = spawnSync(executable, args, {
|
||||
cwd: rootDir,
|
||||
stdio: 'inherit',
|
||||
})
|
||||
|
||||
if (result.status !== 0) {
|
||||
process.exit(result.status ?? 1)
|
||||
}
|
||||
}
|
||||
|
||||
function findDirectoryByName(dir, expectedName) {
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const fullPath = path.join(dir, entry.name)
|
||||
if (entry.isDirectory() && entry.name === expectedName) {
|
||||
return fullPath
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
rmSync(distDir, { recursive: true, force: true })
|
||||
mkdirSync(frameworksDir, { recursive: true })
|
||||
|
||||
run('pnpx', ['expo-brownfield', 'build:ios', '--release', '--artifacts', tempArtifactsDir])
|
||||
|
||||
const sdkFramework = findDirectoryByName(tempArtifactsDir, 'DuooomiBleSDK.xcframework')
|
||||
const hermesFramework = findDirectoryByName(tempArtifactsDir, 'hermesvm.xcframework')
|
||||
|
||||
if (!sdkFramework) {
|
||||
throw new Error(`Missing iOS artifact: ${path.join(tempArtifactsDir, 'DuooomiBleSDK.xcframework')}`)
|
||||
}
|
||||
|
||||
if (!hermesFramework) {
|
||||
throw new Error(`Missing iOS artifact: ${path.join(tempArtifactsDir, 'hermesvm.xcframework')}`)
|
||||
}
|
||||
|
||||
cpSync(wrapperTemplateDir, packageDir, { recursive: true })
|
||||
cpSync(sdkFramework, path.join(frameworksDir, 'DuooomiBleSDK.xcframework'), { recursive: true })
|
||||
cpSync(hermesFramework, path.join(frameworksDir, 'hermesvm.xcframework'), { recursive: true })
|
||||
|
||||
rmSync(tempArtifactsDir, { recursive: true, force: true })
|
||||
|
||||
if (!existsSync(path.join(packageDir, 'Package.swift'))) {
|
||||
throw new Error('Swift wrapper package is incomplete: missing Package.swift')
|
||||
}
|
||||
|
||||
console.log(`iOS wrapper package ready: ${packageDir}`)
|
||||
63
wrappers/android/README.md
Normal file
63
wrappers/android/README.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Android Wrapper 输出说明
|
||||
|
||||
该目录会在执行 `pnpm build:android` 后被复制到 `dist/android/wrapper`,用于交付给 Android 原生宿主工程。
|
||||
|
||||
## 目录内容
|
||||
|
||||
- `../maven`:brownfield AAR 及其依赖对应的 Maven 仓库
|
||||
- `wrapper/`:对外暴露 `DuooomiBleManager` 和 `DuooomiBleRuntime` 的 Android Library 模块
|
||||
|
||||
## 宿主 App 接入步骤
|
||||
|
||||
1. 将 `dist/android/maven` 配置为宿主工程可访问的 Maven 仓库。
|
||||
2. 将 `dist/android/wrapper` 作为 composite build 引入,或者直接把 `wrapper` 模块复制到宿主工程。
|
||||
3. 在宿主 App 的一个长期存在的容器里挂载 Brownfield 对应的 React Native View。
|
||||
4. 运行时启动后,通过 `runtime.manager` 调用蓝牙能力。
|
||||
|
||||
## 最小使用示例
|
||||
|
||||
```kotlin
|
||||
val runtime = DuooomiBleRuntime()
|
||||
runtime.start(container = rootView) { context ->
|
||||
createReactNativeView(context)
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
val result = runtime.manager.connect(deviceId = "device-id")
|
||||
Log.d("DuooomiBle", result.toString())
|
||||
}
|
||||
```
|
||||
|
||||
如果是扫描,仍然沿用监听方式:
|
||||
|
||||
```kotlin
|
||||
runtime.manager.onDiscoveredDevicesChange = { devices ->
|
||||
Log.d("DuooomiBle", devices.toString())
|
||||
}
|
||||
|
||||
runtime.manager.scan()
|
||||
```
|
||||
|
||||
## 如果宿主需要先初始化 Brownfield Host
|
||||
|
||||
```kotlin
|
||||
val runtime = DuooomiBleRuntime()
|
||||
runtime.start(
|
||||
container = rootView,
|
||||
initializeHost = {
|
||||
initializeBrownfieldHost()
|
||||
}
|
||||
) { context ->
|
||||
createReactNativeView(context)
|
||||
}
|
||||
```
|
||||
|
||||
## 说明
|
||||
|
||||
- `DuooomiBleRuntime` 负责挂载隐藏的 RN 容器并启动 JS 运行时
|
||||
- `DuooomiBleManager` 的大部分命令方法为 `suspend`,例如 `connect`、`bind`、`transferFile`
|
||||
- `scan` / `stopScan` 仍然保持事件监听方式
|
||||
- `DuooomiBleManager` 负责发送 `expo-brownfield` 消息和订阅 `BrownfieldState`
|
||||
- `createReactNativeView(context)` 需要由宿主工程提供,因为不同宿主的 Brownfield 接入方式可能不同
|
||||
|
||||
如果没有先调用 `DuooomiBleRuntime.start(...)`,那么 JS 入口不会真正启动,`scan`、`connect` 等调用也不会生效。
|
||||
4
wrappers/android/build.gradle.kts
Normal file
4
wrappers/android/build.gradle.kts
Normal file
@@ -0,0 +1,4 @@
|
||||
plugins {
|
||||
id("com.android.library") version "8.5.2" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.0.21" apply false
|
||||
}
|
||||
19
wrappers/android/settings.gradle.kts
Normal file
19
wrappers/android/settings.gradle.kts
Normal file
@@ -0,0 +1,19 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven(url = uri("../maven"))
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "duooomi-ble-wrapper"
|
||||
include(":wrapper")
|
||||
28
wrappers/android/wrapper/build.gradle.kts
Normal file
28
wrappers/android/wrapper/build.gradle.kts
Normal file
@@ -0,0 +1,28 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.duooomi.ble.wrapper"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("com.duooomi:duooomi-ble-sdk:1.0.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
|
||||
}
|
||||
0
wrappers/android/wrapper/consumer-rules.pro
Normal file
0
wrappers/android/wrapper/consumer-rules.pro
Normal file
2
wrappers/android/wrapper/src/main/AndroidManifest.xml
Normal file
2
wrappers/android/wrapper/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.duooomi.ble.wrapper" />
|
||||
@@ -0,0 +1,341 @@
|
||||
package com.duooomi.ble.wrapper
|
||||
|
||||
import expo.modules.brownfield.BrownfieldMessage
|
||||
import expo.modules.brownfield.BrownfieldMessaging
|
||||
import expo.modules.brownfield.BrownfieldState
|
||||
import expo.modules.brownfield.Removable
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlinx.coroutines.CancellableContinuation
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
/** 表示 `DuooomiBleManager` 异步命令调用失败。 */
|
||||
class DuooomiBleManagerException(message: String) : Exception(message)
|
||||
|
||||
/**
|
||||
* 原生宿主调用的 BLE 门面。
|
||||
*
|
||||
* 该类型负责把宿主方法调用转换为 `expo-brownfield` 消息,
|
||||
* 并将 JS 侧共享状态变化转发给宿主回调。
|
||||
*/
|
||||
class DuooomiBleManager {
|
||||
companion object {
|
||||
private const val BT_STATE = "btState"
|
||||
private const val CONNECTED_DEVICE = "connectedDevice"
|
||||
private const val DEVICE_INFO = "deviceInfo"
|
||||
private const val VERSION = "version"
|
||||
private const val IS_ACTIVATED = "isActivated"
|
||||
private const val TRANSFER_PROGRESS = "transferProgress"
|
||||
private const val ERROR = "error"
|
||||
private const val DISCOVERED_DEVICES = "discoveredDevices"
|
||||
}
|
||||
|
||||
private val sharedStateSubscriptions = mutableListOf<Removable>()
|
||||
private var messageListenerId: String? = null
|
||||
private var started = false
|
||||
private val pendingRequests = ConcurrentHashMap<String, CancellableContinuation<Map<String, Any?>>>()
|
||||
|
||||
/** 接收 JS 侧原始消息事件。 */
|
||||
var onMessage: ((BrownfieldMessage) -> Unit)? = null
|
||||
/** 接收蓝牙状态变化,例如 `idle`、`scanning`、`connected`。 */
|
||||
var onBluetoothStateChange: ((String) -> Unit)? = null
|
||||
/** 接收当前已连接设备信息。 */
|
||||
var onConnectedDeviceChange: ((DuooomiConnectedDevice) -> Unit)? = null
|
||||
/** 接收设备详情信息。 */
|
||||
var onDeviceInfoChange: ((DuooomiDeviceInfo) -> Unit)? = null
|
||||
/** 接收固件版本信息。 */
|
||||
var onVersionChange: ((String) -> Unit)? = null
|
||||
/** 接收设备激活状态变化。 */
|
||||
var onActivationChange: ((Boolean) -> Unit)? = null
|
||||
/** 接收文件传输进度,范围为 `0..100`。 */
|
||||
var onTransferProgressChange: ((Int) -> Unit)? = null
|
||||
/** 接收 SDK 上报的错误字符串。 */
|
||||
var onError: ((String) -> Unit)? = null
|
||||
/** 接收扫描发现的设备列表。 */
|
||||
var onDiscoveredDevicesChange: ((List<DuooomiDiscoveredDevice>) -> Unit)? = null
|
||||
|
||||
/**
|
||||
* 开始订阅 Brownfield 消息与共享状态。
|
||||
*
|
||||
* 调用前应先通过 [DuooomiBleRuntime] 启动 RN/Brownfield 运行时。
|
||||
*/
|
||||
fun start() {
|
||||
if (started) return
|
||||
started = true
|
||||
|
||||
sharedStateSubscriptions += BrownfieldState.subscribe(BT_STATE) { value ->
|
||||
(value as? String)?.let { onBluetoothStateChange?.invoke(it) }
|
||||
}
|
||||
|
||||
sharedStateSubscriptions += BrownfieldState.subscribe(CONNECTED_DEVICE) { value ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(value as? Map<String, Any?>)?.let {
|
||||
onConnectedDeviceChange?.invoke(
|
||||
DuooomiConnectedDevice(
|
||||
id = DuooomiBleValueDecoder.requiredString(it, "id"),
|
||||
name = DuooomiBleValueDecoder.optionalString(it, "name"),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sharedStateSubscriptions += BrownfieldState.subscribe(DEVICE_INFO) { value ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(value as? Map<String, Any?>)?.let {
|
||||
onDeviceInfoChange?.invoke(
|
||||
DuooomiDeviceInfo(
|
||||
allspace = DuooomiBleValueDecoder.requiredString(it, "allspace"),
|
||||
freespace = DuooomiBleValueDecoder.requiredString(it, "freespace"),
|
||||
name = DuooomiBleValueDecoder.requiredString(it, "name"),
|
||||
size = DuooomiBleValueDecoder.requiredString(it, "size"),
|
||||
brand = DuooomiBleValueDecoder.requiredString(it, "brand"),
|
||||
powerlevel = DuooomiBleValueDecoder.requiredInt(it, "powerlevel"),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sharedStateSubscriptions += BrownfieldState.subscribe(VERSION) { value ->
|
||||
(value as? String)?.let { onVersionChange?.invoke(it) }
|
||||
}
|
||||
|
||||
sharedStateSubscriptions += BrownfieldState.subscribe(IS_ACTIVATED) { value ->
|
||||
(value as? Boolean)?.let { onActivationChange?.invoke(it) }
|
||||
}
|
||||
|
||||
sharedStateSubscriptions += BrownfieldState.subscribe(TRANSFER_PROGRESS) { value ->
|
||||
(value as? Int)?.let { onTransferProgressChange?.invoke(it) }
|
||||
}
|
||||
|
||||
sharedStateSubscriptions += BrownfieldState.subscribe(ERROR) { value ->
|
||||
(value as? String)?.let { onError?.invoke(it) }
|
||||
}
|
||||
|
||||
sharedStateSubscriptions += BrownfieldState.subscribe(DISCOVERED_DEVICES) { value ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(value as? List<Map<String, Any?>>)?.let { devices ->
|
||||
onDiscoveredDevicesChange?.invoke(
|
||||
devices.mapNotNull { device ->
|
||||
runCatching {
|
||||
DuooomiDiscoveredDevice(
|
||||
id = DuooomiBleValueDecoder.requiredString(device, "id"),
|
||||
name = DuooomiBleValueDecoder.optionalString(device, "name"),
|
||||
rssi = DuooomiBleValueDecoder.optionalInt(device, "rssi"),
|
||||
)
|
||||
}.getOrNull()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
messageListenerId = BrownfieldMessaging.addListener { event ->
|
||||
if (handleResponse(event)) {
|
||||
return@addListener
|
||||
}
|
||||
|
||||
onMessage?.invoke(event)
|
||||
}
|
||||
}
|
||||
|
||||
/** 停止订阅 Brownfield 消息与共享状态。 */
|
||||
fun stop() {
|
||||
started = false
|
||||
sharedStateSubscriptions.forEach { it.remove() }
|
||||
sharedStateSubscriptions.clear()
|
||||
pendingRequests.forEach { (_, continuation) ->
|
||||
continuation.resumeWithException(DuooomiBleManagerException("Manager stopped before response was received"))
|
||||
}
|
||||
pendingRequests.clear()
|
||||
|
||||
messageListenerId?.let(BrownfieldMessaging::removeListener)
|
||||
messageListenerId = null
|
||||
}
|
||||
|
||||
/** 开始扫描蓝牙设备。 */
|
||||
fun scan() {
|
||||
send(mapOf("action" to "scan"))
|
||||
}
|
||||
|
||||
/** 停止扫描蓝牙设备。 */
|
||||
fun stopScan() {
|
||||
send(mapOf("action" to "stopScan"))
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接指定设备。
|
||||
*
|
||||
* @param deviceId 设备唯一标识。
|
||||
* @return 连接成功后的设备摘要信息。
|
||||
*/
|
||||
suspend fun connect(deviceId: String): DuooomiConnectedDevice {
|
||||
val result = sendAsync(
|
||||
action = "connect",
|
||||
params = mapOf("deviceId" to deviceId),
|
||||
)
|
||||
return DuooomiConnectedDevice(
|
||||
id = DuooomiBleValueDecoder.requiredString(result, "id"),
|
||||
name = DuooomiBleValueDecoder.optionalString(result, "name"),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开当前设备连接。
|
||||
*
|
||||
* @return 断开连接结果。
|
||||
*/
|
||||
suspend fun disconnect(): DuooomiCommandResult {
|
||||
val result = sendAsync(action = "disconnect")
|
||||
return DuooomiCommandResult(
|
||||
success = DuooomiBleValueDecoder.requiredBoolean(result, "success"),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前已连接设备的详细信息。
|
||||
*
|
||||
* @return 设备详情数据。
|
||||
*/
|
||||
suspend fun getDeviceInfo(): DuooomiDeviceInfo {
|
||||
val result = sendAsync(action = "getDeviceInfo")
|
||||
return DuooomiDeviceInfo(
|
||||
allspace = DuooomiBleValueDecoder.requiredString(result, "allspace"),
|
||||
freespace = DuooomiBleValueDecoder.requiredString(result, "freespace"),
|
||||
name = DuooomiBleValueDecoder.requiredString(result, "name"),
|
||||
size = DuooomiBleValueDecoder.requiredString(result, "size"),
|
||||
brand = DuooomiBleValueDecoder.requiredString(result, "brand"),
|
||||
powerlevel = DuooomiBleValueDecoder.requiredInt(result, "powerlevel"),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前已连接设备的版本信息。
|
||||
*
|
||||
* @return 版本信息数据。
|
||||
*/
|
||||
suspend fun getVersion(): DuooomiVersionInfo {
|
||||
val result = sendAsync(action = "getVersion")
|
||||
return DuooomiVersionInfo(
|
||||
version = DuooomiBleValueDecoder.requiredString(result, "version"),
|
||||
type = DuooomiBleValueDecoder.requiredString(result, "type"),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定当前设备到指定用户。
|
||||
*
|
||||
* @param userId 业务侧用户标识。
|
||||
* @return 绑定结果数据。
|
||||
*/
|
||||
suspend fun bind(userId: String): DuooomiBindingResult {
|
||||
val result = sendAsync(
|
||||
action = "bind",
|
||||
params = mapOf("userId" to userId),
|
||||
)
|
||||
return DuooomiBindingResult(
|
||||
type = DuooomiBleValueDecoder.requiredInt(result, "type"),
|
||||
sn = DuooomiBleValueDecoder.requiredString(result, "sn"),
|
||||
success = DuooomiBleValueDecoder.requiredInt(result, "success"),
|
||||
contents = DuooomiBleValueDecoder.stringList(result, "contents"),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑当前设备与指定用户的关系。
|
||||
*
|
||||
* @param userId 业务侧用户标识。
|
||||
* @return 解绑结果数据。
|
||||
*/
|
||||
suspend fun unbind(userId: String): DuooomiUnbindResult {
|
||||
val result = sendAsync(
|
||||
action = "unbind",
|
||||
params = mapOf("userId" to userId),
|
||||
)
|
||||
return DuooomiUnbindResult(
|
||||
success = DuooomiBleValueDecoder.requiredInt(result, "success"),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除设备中的指定文件。
|
||||
*
|
||||
* @param key 设备侧文件 key。
|
||||
* @return 删除结果数据。
|
||||
*/
|
||||
suspend fun deleteFile(key: String): DuooomiDeleteFileResult {
|
||||
val result = sendAsync(
|
||||
action = "deleteFile",
|
||||
params = mapOf("key" to key),
|
||||
)
|
||||
return DuooomiDeleteFileResult(
|
||||
type = DuooomiBleValueDecoder.requiredInt(result, "type"),
|
||||
success = DuooomiBleValueDecoder.requiredInt(result, "success"),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 向设备传输文件。
|
||||
*
|
||||
* @param fileUri 待传输文件的本地 URI。
|
||||
* @param commandType 传输命令类型;不传时由 JS 侧使用默认值。
|
||||
* @return 传输完成结果。
|
||||
*/
|
||||
suspend fun transferFile(fileUri: String, commandType: Int? = null): DuooomiCommandResult {
|
||||
val result = sendAsync(
|
||||
action = "transferFile",
|
||||
params = buildMap {
|
||||
put("fileUri", fileUri)
|
||||
commandType?.let { put("commandType", it) }
|
||||
},
|
||||
)
|
||||
return DuooomiCommandResult(
|
||||
success = DuooomiBleValueDecoder.requiredBoolean(result, "success"),
|
||||
)
|
||||
}
|
||||
|
||||
private fun send(message: BrownfieldMessage) {
|
||||
BrownfieldMessaging.sendMessage(message)
|
||||
}
|
||||
|
||||
private suspend fun sendAsync(
|
||||
action: String,
|
||||
params: Map<String, Any?> = emptyMap(),
|
||||
): Map<String, Any?> = suspendCancellableCoroutine { continuation ->
|
||||
val requestId = UUID.randomUUID().toString()
|
||||
pendingRequests[requestId] = continuation
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
pendingRequests.remove(requestId)
|
||||
}
|
||||
|
||||
send(
|
||||
buildMap {
|
||||
putAll(params)
|
||||
put("action", action)
|
||||
put("requestId", requestId)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleResponse(message: BrownfieldMessage): Boolean {
|
||||
if (message["event"] != "response") {
|
||||
return false
|
||||
}
|
||||
|
||||
val requestId = message["requestId"] as? String ?: return false
|
||||
val continuation = pendingRequests.remove(requestId) ?: return true
|
||||
val success = message["success"] as? Boolean ?: false
|
||||
|
||||
if (success) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val data = message["data"] as? Map<String, Any?> ?: emptyMap()
|
||||
continuation.resume(data)
|
||||
} else {
|
||||
val error = message["error"] as? String ?: "Unknown error"
|
||||
continuation.resumeWithException(DuooomiBleManagerException(error))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.duooomi.ble.wrapper
|
||||
|
||||
/** 扫描阶段发现的设备摘要信息。 */
|
||||
data class DuooomiDiscoveredDevice(
|
||||
val id: String,
|
||||
val name: String?,
|
||||
val rssi: Int?,
|
||||
)
|
||||
|
||||
/** 设备连接结果摘要。 */
|
||||
data class DuooomiConnectedDevice(
|
||||
val id: String,
|
||||
val name: String?,
|
||||
)
|
||||
|
||||
/** 设备详细信息。 */
|
||||
data class DuooomiDeviceInfo(
|
||||
val allspace: String,
|
||||
val freespace: String,
|
||||
val name: String,
|
||||
val size: String,
|
||||
val brand: String,
|
||||
val powerlevel: Int,
|
||||
)
|
||||
|
||||
/** 设备版本信息。 */
|
||||
data class DuooomiVersionInfo(
|
||||
val version: String,
|
||||
val type: String,
|
||||
)
|
||||
|
||||
/** 绑定结果信息。 */
|
||||
data class DuooomiBindingResult(
|
||||
val type: Int,
|
||||
val sn: String,
|
||||
val success: Int,
|
||||
val contents: List<String>,
|
||||
)
|
||||
|
||||
/** 解绑结果信息。 */
|
||||
data class DuooomiUnbindResult(
|
||||
val success: Int,
|
||||
)
|
||||
|
||||
/** 删除文件结果信息。 */
|
||||
data class DuooomiDeleteFileResult(
|
||||
val type: Int,
|
||||
val success: Int,
|
||||
)
|
||||
|
||||
/** 通用布尔完成结果。 */
|
||||
data class DuooomiCommandResult(
|
||||
val success: Boolean,
|
||||
)
|
||||
|
||||
internal object DuooomiBleValueDecoder {
|
||||
fun requiredString(map: Map<String, Any?>, key: String): String {
|
||||
return optionalString(map, key)
|
||||
?: throw DuooomiBleManagerException("Missing string value for key: $key")
|
||||
}
|
||||
|
||||
fun optionalString(map: Map<String, Any?>, key: String): String? {
|
||||
val value = map[key] ?: return null
|
||||
return when (value) {
|
||||
is String -> value
|
||||
is Number -> value.toString()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun requiredInt(map: Map<String, Any?>, key: String): Int {
|
||||
return optionalInt(map, key)
|
||||
?: throw DuooomiBleManagerException("Missing integer value for key: $key")
|
||||
}
|
||||
|
||||
fun optionalInt(map: Map<String, Any?>, key: String): Int? {
|
||||
val value = map[key] ?: return null
|
||||
return when (value) {
|
||||
is Int -> value
|
||||
is Number -> value.toInt()
|
||||
is String -> value.toIntOrNull()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun requiredBoolean(map: Map<String, Any?>, key: String): Boolean {
|
||||
val value = map[key]
|
||||
return when (value) {
|
||||
is Boolean -> value
|
||||
is Number -> value.toInt() != 0
|
||||
else -> throw DuooomiBleManagerException("Missing boolean value for key: $key")
|
||||
}
|
||||
}
|
||||
|
||||
fun stringList(map: Map<String, Any?>, key: String): List<String> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return map[key] as? List<String> ?: emptyList()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.duooomi.ble.wrapper
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
||||
/**
|
||||
* 负责在宿主视图层中挂载隐藏的 RN 容器并启动 JS 运行时。
|
||||
*
|
||||
* @property manager 运行时启动后对外复用的 BLE 门面实例。
|
||||
*/
|
||||
class DuooomiBleRuntime(
|
||||
val manager: DuooomiBleManager = DuooomiBleManager(),
|
||||
) {
|
||||
private var mountedView: View? = null
|
||||
|
||||
/**
|
||||
* 在指定容器中启动 Brownfield 运行时。
|
||||
*
|
||||
* @param container 用于挂载隐藏 React Native 视图的宿主容器。
|
||||
* @param initializeHost 可选的宿主初始化逻辑。
|
||||
* @param createReactNativeView 由宿主提供的 RN View 构造逻辑。
|
||||
*/
|
||||
fun start(
|
||||
container: ViewGroup,
|
||||
initializeHost: (() -> Unit)? = null,
|
||||
createReactNativeView: ((Context) -> View)?
|
||||
) {
|
||||
if (mountedView != null) {
|
||||
return
|
||||
}
|
||||
|
||||
initializeHost?.invoke()
|
||||
|
||||
val reactNativeView = createReactNativeView(container.context)
|
||||
reactNativeView.alpha = 0f
|
||||
reactNativeView.isClickable = false
|
||||
reactNativeView.isFocusable = false
|
||||
reactNativeView.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
|
||||
|
||||
val layoutParams = ViewGroup.LayoutParams(1, 1)
|
||||
container.addView(reactNativeView, layoutParams)
|
||||
|
||||
mountedView = reactNativeView
|
||||
manager.start()
|
||||
}
|
||||
|
||||
/** 停止运行时并移除已挂载的隐藏 RN 视图。 */
|
||||
fun stop() {
|
||||
manager.stop()
|
||||
mountedView?.let { view ->
|
||||
(view.parent as? ViewGroup)?.removeView(view)
|
||||
}
|
||||
mountedView = null
|
||||
}
|
||||
}
|
||||
30
wrappers/ios/Package.swift
Normal file
30
wrappers/ios/Package.swift
Normal file
@@ -0,0 +1,30 @@
|
||||
// swift-tools-version: 5.9
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "DuooomiBleWrapper",
|
||||
platforms: [
|
||||
.iOS(.v15),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
name: "DuooomiBleWrapper",
|
||||
targets: ["DuooomiBleWrapper"]
|
||||
),
|
||||
],
|
||||
targets: [
|
||||
.binaryTarget(
|
||||
name: "DuooomiBleSDK",
|
||||
path: "Frameworks/DuooomiBleSDK.xcframework"
|
||||
),
|
||||
.binaryTarget(
|
||||
name: "hermesvm",
|
||||
path: "Frameworks/hermesvm.xcframework"
|
||||
),
|
||||
.target(
|
||||
name: "DuooomiBleWrapper",
|
||||
dependencies: ["DuooomiBleSDK", "hermesvm"],
|
||||
path: "Sources/DuooomiBleWrapper"
|
||||
),
|
||||
]
|
||||
)
|
||||
62
wrappers/ios/README.md
Normal file
62
wrappers/ios/README.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# DuooomiBleWrapper
|
||||
|
||||
该目录由 `pnpm build:ios` 生成,是可以直接交付给 iOS 原生宿主工程使用的 Swift Package。
|
||||
|
||||
## 目录内容
|
||||
|
||||
- `Frameworks/DuooomiBleSDK.xcframework`
|
||||
- `Frameworks/hermesvm.xcframework`
|
||||
- `Sources/DuooomiBleWrapper/DuooomiBleManager.swift`
|
||||
- `Sources/DuooomiBleWrapper/DuooomiBleRuntime.swift`
|
||||
|
||||
## 宿主 App 接入步骤
|
||||
|
||||
1. 将 `dist/ios/DuooomiBleWrapper` 作为本地 Swift Package 引入宿主工程。
|
||||
2. 在宿主 App 的一个长期存在的容器视图里启动 Brownfield 运行时。
|
||||
3. 运行时启动完成后,通过 `DuooomiBleManager.shared` 调用蓝牙能力。
|
||||
|
||||
## 最小使用示例
|
||||
|
||||
```swift
|
||||
try DuooomiBleRuntime.shared.start(in: view)
|
||||
|
||||
Task {
|
||||
let result = try await DuooomiBleManager.shared.connect(deviceId: "device-id")
|
||||
print(result)
|
||||
}
|
||||
```
|
||||
|
||||
如果是扫描,仍然沿用监听方式:
|
||||
|
||||
```swift
|
||||
DuooomiBleManager.shared.onDiscoveredDevicesChange = { devices in
|
||||
print(devices)
|
||||
}
|
||||
|
||||
DuooomiBleManager.shared.scan()
|
||||
```
|
||||
|
||||
## 如果宿主需要显式初始化 Brownfield
|
||||
|
||||
如果你的宿主工程不能被 `DuooomiBleRuntime` 自动识别出 `ReactNativeHostManager` 或 `ReactNativeView`,可以显式传入初始化逻辑:
|
||||
|
||||
```swift
|
||||
try DuooomiBleRuntime.shared.start(
|
||||
in: view,
|
||||
initializeHost: {
|
||||
ReactNativeHostManager.shared.initialize()
|
||||
},
|
||||
createReactNativeView: {
|
||||
ReactNativeView()
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## 说明
|
||||
|
||||
- `DuooomiBleRuntime` 负责启动 Expo Brownfield 对应的 RN/JS 运行时
|
||||
- `DuooomiBleManager` 的大部分命令方法为 `async throws`,例如 `connect`、`bind`、`transferFile`
|
||||
- `scan` / `stopScan` 仍然保持事件监听方式
|
||||
- `DuooomiBleManager` 也会订阅 `ble/sdk/BleSDK.ts` 中定义的 shared state 并转发给宿主
|
||||
|
||||
如果只调用 `DuooomiBleManager` 而没有先启动 `DuooomiBleRuntime`,JS 侧的 `bleSDK.initialize()` 不会执行,消息也不会被处理。
|
||||
289
wrappers/ios/Sources/DuooomiBleWrapper/DuooomiBleManager.swift
Normal file
289
wrappers/ios/Sources/DuooomiBleWrapper/DuooomiBleManager.swift
Normal file
@@ -0,0 +1,289 @@
|
||||
import Combine
|
||||
import Foundation
|
||||
import DuooomiBleSDK
|
||||
|
||||
/// `DuooomiBleManager` 异步命令调用失败时抛出的错误。
|
||||
public struct DuooomiBleManagerError: Error, LocalizedError {
|
||||
/// 错误描述。
|
||||
public let message: String
|
||||
|
||||
public var errorDescription: String? {
|
||||
message
|
||||
}
|
||||
|
||||
/// 创建一个新的 manager 错误。
|
||||
/// - Parameter message: 错误描述。
|
||||
public init(message: String) {
|
||||
self.message = message
|
||||
}
|
||||
}
|
||||
|
||||
/// 原生宿主调用的 BLE 门面。
|
||||
///
|
||||
/// 该类型负责把宿主侧的方法调用转换为 `expo-brownfield` 消息,
|
||||
/// 并将 JS 侧共享状态变化转发给原生回调。
|
||||
public final class DuooomiBleManager {
|
||||
/// 供宿主全局复用的单例实例。
|
||||
public static let shared = DuooomiBleManager()
|
||||
|
||||
private enum StateKey {
|
||||
static let btState = "btState"
|
||||
static let connectedDevice = "connectedDevice"
|
||||
static let deviceInfo = "deviceInfo"
|
||||
static let version = "version"
|
||||
static let isActivated = "isActivated"
|
||||
static let transferProgress = "transferProgress"
|
||||
static let error = "error"
|
||||
static let discoveredDevices = "discoveredDevices"
|
||||
}
|
||||
|
||||
private var sharedStateCancellables: [AnyCancellable] = []
|
||||
private var messageListenerId: String?
|
||||
private var started = false
|
||||
private let pendingRequestsLock = NSLock()
|
||||
private var pendingRequests: [String: (Result<[String: Any?], Error>) -> Void] = [:]
|
||||
|
||||
/// 接收 JS 侧原始消息事件。
|
||||
public var onMessage: (([String: Any?]) -> Void)?
|
||||
/// 接收蓝牙状态变化,例如 `idle`、`scanning`、`connected`。
|
||||
public var onBluetoothStateChange: ((String) -> Void)?
|
||||
/// 接收当前已连接设备信息。
|
||||
public var onConnectedDeviceChange: ((DuooomiConnectedDevice) -> Void)?
|
||||
/// 接收设备详情信息。
|
||||
public var onDeviceInfoChange: ((DuooomiDeviceInfo) -> Void)?
|
||||
/// 接收固件版本信息。
|
||||
public var onVersionChange: ((String) -> Void)?
|
||||
/// 接收设备激活状态变化。
|
||||
public var onActivationChange: ((Bool) -> Void)?
|
||||
/// 接收文件传输进度,范围为 `0...100`。
|
||||
public var onTransferProgressChange: ((Int) -> Void)?
|
||||
/// 接收 SDK 上报的错误字符串。
|
||||
public var onError: ((String) -> Void)?
|
||||
/// 接收扫描发现的设备列表。
|
||||
public var onDiscoveredDevicesChange: (([DuooomiDiscoveredDevice]) -> Void)?
|
||||
|
||||
private init() {}
|
||||
|
||||
/// 开始订阅 Brownfield 消息与共享状态。
|
||||
///
|
||||
/// 调用前应先通过 `DuooomiBleRuntime` 启动 RN/Brownfield 运行时。
|
||||
public func start() {
|
||||
guard !started else { return }
|
||||
started = true
|
||||
|
||||
sharedStateCancellables = [
|
||||
BrownfieldStateInternal.shared.subscribe(StateKey.btState) { [weak self] value in
|
||||
if let state = value as? String {
|
||||
self?.onBluetoothStateChange?(state)
|
||||
}
|
||||
},
|
||||
BrownfieldStateInternal.shared.subscribe(StateKey.connectedDevice) { [weak self] value in
|
||||
if let device = value as? [String: Any?], let decoded = try? DuooomiConnectedDevice(dictionary: device) {
|
||||
self?.onConnectedDeviceChange?(decoded)
|
||||
}
|
||||
},
|
||||
BrownfieldStateInternal.shared.subscribe(StateKey.deviceInfo) { [weak self] value in
|
||||
if let info = value as? [String: Any?], let decoded = try? DuooomiDeviceInfo(dictionary: info) {
|
||||
self?.onDeviceInfoChange?(decoded)
|
||||
}
|
||||
},
|
||||
BrownfieldStateInternal.shared.subscribe(StateKey.version) { [weak self] value in
|
||||
if let version = value as? String {
|
||||
self?.onVersionChange?(version)
|
||||
}
|
||||
},
|
||||
BrownfieldStateInternal.shared.subscribe(StateKey.isActivated) { [weak self] value in
|
||||
if let isActivated = value as? Bool {
|
||||
self?.onActivationChange?(isActivated)
|
||||
}
|
||||
},
|
||||
BrownfieldStateInternal.shared.subscribe(StateKey.transferProgress) { [weak self] value in
|
||||
if let progress = value as? Int {
|
||||
self?.onTransferProgressChange?(progress)
|
||||
}
|
||||
},
|
||||
BrownfieldStateInternal.shared.subscribe(StateKey.error) { [weak self] value in
|
||||
if let error = value as? String {
|
||||
self?.onError?(error)
|
||||
}
|
||||
},
|
||||
BrownfieldStateInternal.shared.subscribe(StateKey.discoveredDevices) { [weak self] value in
|
||||
if let devices = value as? [[String: Any]] {
|
||||
let decoded = devices.compactMap { try? DuooomiDiscoveredDevice(dictionary: $0) }
|
||||
self?.onDiscoveredDevicesChange?(decoded)
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
messageListenerId = BrownfieldMessagingInternal.shared.addListener { [weak self] message in
|
||||
guard let self else { return }
|
||||
|
||||
if self.handleResponse(message) {
|
||||
return
|
||||
}
|
||||
|
||||
self.onMessage?(message)
|
||||
}
|
||||
}
|
||||
|
||||
/// 停止订阅 Brownfield 消息与共享状态。
|
||||
public func stop() {
|
||||
started = false
|
||||
sharedStateCancellables.removeAll()
|
||||
cancelPendingRequests()
|
||||
|
||||
if let messageListenerId {
|
||||
BrownfieldMessagingInternal.shared.removeListener(id: messageListenerId)
|
||||
self.messageListenerId = nil
|
||||
}
|
||||
}
|
||||
|
||||
/// 开始扫描蓝牙设备。
|
||||
public func scan() {
|
||||
send(["action": "scan"])
|
||||
}
|
||||
|
||||
/// 停止扫描蓝牙设备。
|
||||
public func stopScan() {
|
||||
send(["action": "stopScan"])
|
||||
}
|
||||
|
||||
/// 连接指定设备。
|
||||
/// - Parameter deviceId: 设备唯一标识。
|
||||
/// - Returns: 连接成功后的设备摘要信息。
|
||||
public func connect(deviceId: String) async throws -> DuooomiConnectedDevice {
|
||||
let result = try await sendAsync(action: "connect", params: [
|
||||
"deviceId": deviceId,
|
||||
])
|
||||
return try DuooomiConnectedDevice(dictionary: result)
|
||||
}
|
||||
|
||||
/// 断开当前设备连接。
|
||||
/// - Returns: 断开连接结果。
|
||||
public func disconnect() async throws -> DuooomiCommandResult {
|
||||
let result = try await sendAsync(action: "disconnect")
|
||||
return try DuooomiCommandResult(dictionary: result)
|
||||
}
|
||||
|
||||
/// 获取当前已连接设备的详细信息。
|
||||
/// - Returns: 设备详情数据。
|
||||
public func getDeviceInfo() async throws -> DuooomiDeviceInfo {
|
||||
let result = try await sendAsync(action: "getDeviceInfo")
|
||||
return try DuooomiDeviceInfo(dictionary: result)
|
||||
}
|
||||
|
||||
/// 获取当前已连接设备的版本信息。
|
||||
/// - Returns: 版本信息数据。
|
||||
public func getVersion() async throws -> DuooomiVersionInfo {
|
||||
let result = try await sendAsync(action: "getVersion")
|
||||
return try DuooomiVersionInfo(dictionary: result)
|
||||
}
|
||||
|
||||
/// 绑定当前设备到指定用户。
|
||||
/// - Parameter userId: 业务侧用户标识。
|
||||
/// - Returns: 绑定结果数据。
|
||||
public func bind(userId: String) async throws -> DuooomiBindingResult {
|
||||
let result = try await sendAsync(action: "bind", params: [
|
||||
"userId": userId,
|
||||
])
|
||||
return try DuooomiBindingResult(dictionary: result)
|
||||
}
|
||||
|
||||
/// 解绑当前设备与指定用户的关系。
|
||||
/// - Parameter userId: 业务侧用户标识。
|
||||
/// - Returns: 解绑结果数据。
|
||||
public func unbind(userId: String) async throws -> DuooomiUnbindResult {
|
||||
let result = try await sendAsync(action: "unbind", params: [
|
||||
"userId": userId,
|
||||
])
|
||||
return try DuooomiUnbindResult(dictionary: result)
|
||||
}
|
||||
|
||||
/// 删除设备中的指定文件。
|
||||
/// - Parameter key: 设备侧文件 key。
|
||||
/// - Returns: 删除结果数据。
|
||||
public func deleteFile(key: String) async throws -> DuooomiDeleteFileResult {
|
||||
let result = try await sendAsync(action: "deleteFile", params: [
|
||||
"key": key,
|
||||
])
|
||||
return try DuooomiDeleteFileResult(dictionary: result)
|
||||
}
|
||||
|
||||
/// 向设备传输文件。
|
||||
/// - Parameters:
|
||||
/// - fileUri: 待传输文件的本地 URI。
|
||||
/// - commandType: 传输命令类型;不传时由 JS 侧使用默认值。
|
||||
/// - Returns: 传输完成结果。
|
||||
public func transferFile(fileUri: String, commandType: Int? = nil) async throws -> DuooomiCommandResult {
|
||||
var params: [String: Any?] = [
|
||||
"fileUri": fileUri,
|
||||
]
|
||||
|
||||
if let commandType {
|
||||
params["commandType"] = commandType
|
||||
}
|
||||
|
||||
let result = try await sendAsync(action: "transferFile", params: params)
|
||||
return try DuooomiCommandResult(dictionary: result)
|
||||
}
|
||||
|
||||
private func send(_ message: [String: Any?]) {
|
||||
BrownfieldMessagingInternal.shared.sendMessage(message)
|
||||
}
|
||||
|
||||
private func sendAsync(action: String, params: [String: Any?] = [:]) async throws -> [String: Any?] {
|
||||
let requestId = UUID().uuidString
|
||||
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
storePendingRequest(requestId) { result in
|
||||
continuation.resume(with: result)
|
||||
}
|
||||
|
||||
var message = params
|
||||
message["action"] = action
|
||||
message["requestId"] = requestId
|
||||
send(message)
|
||||
}
|
||||
}
|
||||
|
||||
private func storePendingRequest(_ requestId: String, completion: @escaping (Result<[String: Any?], Error>) -> Void) {
|
||||
pendingRequestsLock.lock()
|
||||
pendingRequests[requestId] = completion
|
||||
pendingRequestsLock.unlock()
|
||||
}
|
||||
|
||||
private func completePendingRequest(_ requestId: String, result: Result<[String: Any?], Error>) {
|
||||
pendingRequestsLock.lock()
|
||||
let completion = pendingRequests.removeValue(forKey: requestId)
|
||||
pendingRequestsLock.unlock()
|
||||
completion?(result)
|
||||
}
|
||||
|
||||
private func cancelPendingRequests() {
|
||||
pendingRequestsLock.lock()
|
||||
let requests = pendingRequests
|
||||
pendingRequests.removeAll()
|
||||
pendingRequestsLock.unlock()
|
||||
|
||||
for completion in requests.values {
|
||||
completion(.failure(DuooomiBleManagerError(message: "Manager stopped before response was received")))
|
||||
}
|
||||
}
|
||||
|
||||
private func handleResponse(_ message: [String: Any?]) -> Bool {
|
||||
guard (message["event"] as? String) == "response",
|
||||
let requestId = message["requestId"] as? String else {
|
||||
return false
|
||||
}
|
||||
|
||||
if let success = message["success"] as? Bool, success {
|
||||
let data = message["data"] as? [String: Any?] ?? [:]
|
||||
completePendingRequest(requestId, result: .success(data))
|
||||
return true
|
||||
}
|
||||
|
||||
let errorMessage = message["error"] as? String ?? "Unknown error"
|
||||
completePendingRequest(requestId, result: .failure(DuooomiBleManagerError(message: errorMessage)))
|
||||
return true
|
||||
}
|
||||
}
|
||||
180
wrappers/ios/Sources/DuooomiBleWrapper/DuooomiBleModels.swift
Normal file
180
wrappers/ios/Sources/DuooomiBleWrapper/DuooomiBleModels.swift
Normal file
@@ -0,0 +1,180 @@
|
||||
import Foundation
|
||||
|
||||
/// 扫描阶段发现的设备摘要信息。
|
||||
public struct DuooomiDiscoveredDevice {
|
||||
/// 设备唯一标识。
|
||||
public let id: String
|
||||
/// 设备名称,可能为空。
|
||||
public let name: String?
|
||||
/// 信号强度,可能为空。
|
||||
public let rssi: Int?
|
||||
|
||||
init(dictionary: [String: Any]) throws {
|
||||
self.id = try DuooomiBleValueDecoder.requiredString(dictionary, key: "id")
|
||||
self.name = DuooomiBleValueDecoder.optionalString(dictionary, key: "name")
|
||||
self.rssi = DuooomiBleValueDecoder.optionalInt(dictionary, key: "rssi")
|
||||
}
|
||||
}
|
||||
|
||||
/// 设备连接结果摘要。
|
||||
public struct DuooomiConnectedDevice {
|
||||
/// 设备唯一标识。
|
||||
public let id: String
|
||||
/// 设备名称,可能为空。
|
||||
public let name: String?
|
||||
|
||||
init(dictionary: [String: Any?]) throws {
|
||||
self.id = try DuooomiBleValueDecoder.requiredString(dictionary, key: "id")
|
||||
self.name = DuooomiBleValueDecoder.optionalString(dictionary, key: "name")
|
||||
}
|
||||
}
|
||||
|
||||
/// 设备详细信息。
|
||||
public struct DuooomiDeviceInfo {
|
||||
/// 总存储空间,字符串表示以避免跨桥数值精度问题。
|
||||
public let allspace: String
|
||||
/// 可用存储空间,字符串表示以避免跨桥数值精度问题。
|
||||
public let freespace: String
|
||||
/// 设备名称。
|
||||
public let name: String
|
||||
/// 设备分辨率,例如 `360x360`。
|
||||
public let size: String
|
||||
/// 品牌名。
|
||||
public let brand: String
|
||||
/// 电量百分比,范围 `0...100`。
|
||||
public let powerlevel: Int
|
||||
|
||||
init(dictionary: [String: Any?]) throws {
|
||||
self.allspace = try DuooomiBleValueDecoder.requiredString(dictionary, key: "allspace")
|
||||
self.freespace = try DuooomiBleValueDecoder.requiredString(dictionary, key: "freespace")
|
||||
self.name = try DuooomiBleValueDecoder.requiredString(dictionary, key: "name")
|
||||
self.size = try DuooomiBleValueDecoder.requiredString(dictionary, key: "size")
|
||||
self.brand = try DuooomiBleValueDecoder.requiredString(dictionary, key: "brand")
|
||||
self.powerlevel = try DuooomiBleValueDecoder.requiredInt(dictionary, key: "powerlevel")
|
||||
}
|
||||
}
|
||||
|
||||
/// 设备版本信息。
|
||||
public struct DuooomiVersionInfo {
|
||||
/// 固件版本号。
|
||||
public let version: String
|
||||
/// 协议返回的版本类型字段。
|
||||
public let type: String
|
||||
|
||||
init(dictionary: [String: Any?]) throws {
|
||||
self.version = try DuooomiBleValueDecoder.requiredString(dictionary, key: "version")
|
||||
self.type = try DuooomiBleValueDecoder.requiredString(dictionary, key: "type")
|
||||
}
|
||||
}
|
||||
|
||||
/// 绑定结果信息。
|
||||
public struct DuooomiBindingResult {
|
||||
/// 协议类型。
|
||||
public let type: Int
|
||||
/// 设备 SN。
|
||||
public let sn: String
|
||||
/// 绑定结果,`1` 表示成功。
|
||||
public let success: Int
|
||||
/// 已存在内容列表。
|
||||
public let contents: [String]
|
||||
|
||||
init(dictionary: [String: Any?]) throws {
|
||||
self.type = try DuooomiBleValueDecoder.requiredInt(dictionary, key: "type")
|
||||
self.sn = try DuooomiBleValueDecoder.requiredString(dictionary, key: "sn")
|
||||
self.success = try DuooomiBleValueDecoder.requiredInt(dictionary, key: "success")
|
||||
self.contents = DuooomiBleValueDecoder.stringArray(dictionary, key: "contents")
|
||||
}
|
||||
}
|
||||
|
||||
/// 解绑结果信息。
|
||||
public struct DuooomiUnbindResult {
|
||||
/// 解绑结果,`1` 表示成功。
|
||||
public let success: Int
|
||||
|
||||
init(dictionary: [String: Any?]) throws {
|
||||
self.success = try DuooomiBleValueDecoder.requiredInt(dictionary, key: "success")
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除文件结果信息。
|
||||
public struct DuooomiDeleteFileResult {
|
||||
/// 协议类型。
|
||||
public let type: Int
|
||||
/// 删除结果,`0` 成功,`1` 失败,`2` 文件不存在。
|
||||
public let success: Int
|
||||
|
||||
init(dictionary: [String: Any?]) throws {
|
||||
self.type = try DuooomiBleValueDecoder.requiredInt(dictionary, key: "type")
|
||||
self.success = try DuooomiBleValueDecoder.requiredInt(dictionary, key: "success")
|
||||
}
|
||||
}
|
||||
|
||||
/// 通用布尔完成结果。
|
||||
public struct DuooomiCommandResult {
|
||||
/// 是否执行成功。
|
||||
public let success: Bool
|
||||
|
||||
init(dictionary: [String: Any?]) throws {
|
||||
self.success = try DuooomiBleValueDecoder.requiredBool(dictionary, key: "success")
|
||||
}
|
||||
}
|
||||
|
||||
enum DuooomiBleValueDecoder {
|
||||
static func requiredString(_ dictionary: [String: Any?], key: String) throws -> String {
|
||||
if let value = optionalString(dictionary, key: key) {
|
||||
return value
|
||||
}
|
||||
throw DuooomiBleManagerError(message: "Missing string value for key: \(key)")
|
||||
}
|
||||
|
||||
static func optionalString(_ dictionary: [String: Any?], key: String) -> String? {
|
||||
if let value = dictionary[key] as? String {
|
||||
return value
|
||||
}
|
||||
|
||||
if let value = dictionary[key] as? NSNumber {
|
||||
return value.stringValue
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
static func requiredInt(_ dictionary: [String: Any?], key: String) throws -> Int {
|
||||
if let value = optionalInt(dictionary, key: key) {
|
||||
return value
|
||||
}
|
||||
throw DuooomiBleManagerError(message: "Missing integer value for key: \(key)")
|
||||
}
|
||||
|
||||
static func optionalInt(_ dictionary: [String: Any?], key: String) -> Int? {
|
||||
if let value = dictionary[key] as? Int {
|
||||
return value
|
||||
}
|
||||
|
||||
if let value = dictionary[key] as? NSNumber {
|
||||
return value.intValue
|
||||
}
|
||||
|
||||
if let value = dictionary[key] as? String {
|
||||
return Int(value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
static func requiredBool(_ dictionary: [String: Any?], key: String) throws -> Bool {
|
||||
if let value = dictionary[key] as? Bool {
|
||||
return value
|
||||
}
|
||||
|
||||
if let value = dictionary[key] as? NSNumber {
|
||||
return value.boolValue
|
||||
}
|
||||
|
||||
throw DuooomiBleManagerError(message: "Missing boolean value for key: \(key)")
|
||||
}
|
||||
|
||||
static func stringArray(_ dictionary: [String: Any?], key: String) -> [String] {
|
||||
(dictionary[key] as? [String]) ?? []
|
||||
}
|
||||
}
|
||||
115
wrappers/ios/Sources/DuooomiBleWrapper/DuooomiBleRuntime.swift
Normal file
115
wrappers/ios/Sources/DuooomiBleWrapper/DuooomiBleRuntime.swift
Normal file
@@ -0,0 +1,115 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
/// Brownfield 运行时启动失败时抛出的错误。
|
||||
public enum DuooomiBleRuntimeError: Error, LocalizedError {
|
||||
case hostManagerUnavailable
|
||||
case reactNativeViewUnavailable
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .hostManagerUnavailable:
|
||||
return "Unable to locate ReactNativeHostManager. Make sure the brownfield framework is linked correctly."
|
||||
case .reactNativeViewUnavailable:
|
||||
return "Unable to create ReactNativeView automatically. Pass createReactNativeView when starting the runtime."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 负责在宿主视图层中挂载隐藏的 RN 容器并启动 JS 运行时。
|
||||
public final class DuooomiBleRuntime {
|
||||
/// 供宿主全局复用的单例实例。
|
||||
public static let shared = DuooomiBleRuntime()
|
||||
|
||||
private weak var mountedView: UIView?
|
||||
private weak var containerView: UIView?
|
||||
|
||||
private init() {}
|
||||
|
||||
/// 在指定容器中启动 Brownfield 运行时。
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - containerView: 用于挂载隐藏 React Native 视图的宿主容器。
|
||||
/// - initializeHost: 可选的宿主初始化逻辑;若不传则尝试自动初始化。
|
||||
/// - createReactNativeView: 可选的 RN View 构造逻辑;若不传则尝试自动创建。
|
||||
/// - Throws: `DuooomiBleRuntimeError`。
|
||||
public func start(
|
||||
in containerView: UIView,
|
||||
initializeHost: (() -> Void)? = nil,
|
||||
createReactNativeView: (() -> UIView)? = nil
|
||||
) throws {
|
||||
if mountedView != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if let initializeHost {
|
||||
initializeHost()
|
||||
} else {
|
||||
try autoInitializeHostManager()
|
||||
}
|
||||
|
||||
let reactNativeView = createReactNativeView?() ?? try autoCreateReactNativeView()
|
||||
reactNativeView.isHidden = true
|
||||
reactNativeView.isUserInteractionEnabled = false
|
||||
reactNativeView.frame = CGRect(x: 0, y: 0, width: 1, height: 1)
|
||||
reactNativeView.accessibilityElementsHidden = true
|
||||
|
||||
containerView.addSubview(reactNativeView)
|
||||
|
||||
self.containerView = containerView
|
||||
self.mountedView = reactNativeView
|
||||
|
||||
DuooomiBleManager.shared.start()
|
||||
}
|
||||
|
||||
/// 停止运行时并移除已挂载的隐藏 RN 视图。
|
||||
public func stop() {
|
||||
DuooomiBleManager.shared.stop()
|
||||
mountedView?.removeFromSuperview()
|
||||
mountedView = nil
|
||||
containerView = nil
|
||||
}
|
||||
|
||||
private func autoInitializeHostManager() throws {
|
||||
let possibleClassNames = [
|
||||
"ReactNativeHostManager",
|
||||
"DuooomiBleSDK.ReactNativeHostManager",
|
||||
]
|
||||
|
||||
for className in possibleClassNames {
|
||||
guard let managerClass = NSClassFromString(className) as? NSObject.Type else {
|
||||
continue
|
||||
}
|
||||
|
||||
let sharedSelector = NSSelectorFromString("shared")
|
||||
let initializeSelector = NSSelectorFromString("initialize")
|
||||
let managerObject = (managerClass as AnyObject).perform(sharedSelector)?.takeUnretainedValue() as? NSObject
|
||||
|
||||
if let managerObject, managerObject.responds(to: initializeSelector) {
|
||||
managerObject.perform(initializeSelector)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
throw DuooomiBleRuntimeError.hostManagerUnavailable
|
||||
}
|
||||
|
||||
private func autoCreateReactNativeView() throws -> UIView {
|
||||
let possibleClassNames = [
|
||||
"ReactNativeView",
|
||||
"DuooomiBleSDK.ReactNativeView",
|
||||
]
|
||||
|
||||
for className in possibleClassNames {
|
||||
guard let viewClass = NSClassFromString(className) as? NSObject.Type else {
|
||||
continue
|
||||
}
|
||||
|
||||
if let view = viewClass.init() as? UIView {
|
||||
return view
|
||||
}
|
||||
}
|
||||
|
||||
throw DuooomiBleRuntimeError.reactNativeViewUnavailable
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user