|
@@ -3,18 +3,30 @@ export interface HeartRateControllerCallbacks {
|
|
|
onStatus: (message: string) => void
|
|
onStatus: (message: string) => void
|
|
|
onError: (message: string) => void
|
|
onError: (message: string) => void
|
|
|
onConnectionChange: (connected: boolean, deviceName: string | null) => void
|
|
onConnectionChange: (connected: boolean, deviceName: string | null) => void
|
|
|
|
|
+ onDeviceListChange: (devices: HeartRateDiscoveredDevice[]) => void
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export interface HeartRateDiscoveredDevice {
|
|
|
|
|
+ deviceId: string
|
|
|
|
|
+ name: string
|
|
|
|
|
+ rssi: number | null
|
|
|
|
|
+ lastSeenAt: number
|
|
|
|
|
+ isPreferred: boolean
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
type BluetoothDeviceLike = {
|
|
type BluetoothDeviceLike = {
|
|
|
deviceId?: string
|
|
deviceId?: string
|
|
|
name?: string
|
|
name?: string
|
|
|
localName?: string
|
|
localName?: string
|
|
|
|
|
+ RSSI?: number
|
|
|
advertisServiceUUIDs?: string[]
|
|
advertisServiceUUIDs?: string[]
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const HEART_RATE_SERVICE_UUID = '180D'
|
|
const HEART_RATE_SERVICE_UUID = '180D'
|
|
|
const HEART_RATE_MEASUREMENT_UUID = '2A37'
|
|
const HEART_RATE_MEASUREMENT_UUID = '2A37'
|
|
|
const DISCOVERY_TIMEOUT_MS = 12000
|
|
const DISCOVERY_TIMEOUT_MS = 12000
|
|
|
|
|
+const DEVICE_STALE_MS = 15000
|
|
|
|
|
+const PREFERRED_HEART_RATE_DEVICE_STORAGE_KEY = 'cmr.preferredHeartRateDevice'
|
|
|
|
|
|
|
|
function normalizeUuid(uuid: string | undefined | null): string {
|
|
function normalizeUuid(uuid: string | undefined | null): string {
|
|
|
return String(uuid || '').replace(/[^0-9a-f]/gi, '').toUpperCase()
|
|
return String(uuid || '').replace(/[^0-9a-f]/gi, '').toUpperCase()
|
|
@@ -80,9 +92,18 @@ export class HeartRateController {
|
|
|
connected: boolean
|
|
connected: boolean
|
|
|
currentDeviceId: string | null
|
|
currentDeviceId: string | null
|
|
|
currentDeviceName: string | null
|
|
currentDeviceName: string | null
|
|
|
|
|
+ discoveredDevices: HeartRateDiscoveredDevice[]
|
|
|
|
|
+ lastDeviceId: string | null
|
|
|
|
|
+ lastDeviceName: string | null
|
|
|
|
|
+ manualDisconnect: boolean
|
|
|
|
|
+ reconnecting: boolean
|
|
|
|
|
+ disconnecting: boolean
|
|
|
|
|
+ disconnectingDeviceId: string | null
|
|
|
|
|
+ autoConnectDeviceId: string | null
|
|
|
measurementServiceId: string | null
|
|
measurementServiceId: string | null
|
|
|
measurementCharacteristicId: string | null
|
|
measurementCharacteristicId: string | null
|
|
|
discoveryTimer: number
|
|
discoveryTimer: number
|
|
|
|
|
+ reconnectTimer: number
|
|
|
deviceFoundHandler: ((result: any) => void) | null
|
|
deviceFoundHandler: ((result: any) => void) | null
|
|
|
characteristicHandler: ((result: any) => void) | null
|
|
characteristicHandler: ((result: any) => void) | null
|
|
|
connectionStateHandler: ((result: any) => void) | null
|
|
connectionStateHandler: ((result: any) => void) | null
|
|
@@ -94,12 +115,22 @@ export class HeartRateController {
|
|
|
this.connected = false
|
|
this.connected = false
|
|
|
this.currentDeviceId = null
|
|
this.currentDeviceId = null
|
|
|
this.currentDeviceName = null
|
|
this.currentDeviceName = null
|
|
|
|
|
+ this.discoveredDevices = []
|
|
|
|
|
+ this.lastDeviceId = null
|
|
|
|
|
+ this.lastDeviceName = null
|
|
|
|
|
+ this.manualDisconnect = false
|
|
|
|
|
+ this.reconnecting = false
|
|
|
|
|
+ this.disconnecting = false
|
|
|
|
|
+ this.disconnectingDeviceId = null
|
|
|
|
|
+ this.autoConnectDeviceId = null
|
|
|
this.measurementServiceId = null
|
|
this.measurementServiceId = null
|
|
|
this.measurementCharacteristicId = null
|
|
this.measurementCharacteristicId = null
|
|
|
this.discoveryTimer = 0
|
|
this.discoveryTimer = 0
|
|
|
|
|
+ this.reconnectTimer = 0
|
|
|
this.deviceFoundHandler = null
|
|
this.deviceFoundHandler = null
|
|
|
this.characteristicHandler = null
|
|
this.characteristicHandler = null
|
|
|
this.connectionStateHandler = null
|
|
this.connectionStateHandler = null
|
|
|
|
|
+ this.restorePreferredDevice()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
startScanAndConnect(): void {
|
|
startScanAndConnect(): void {
|
|
@@ -108,40 +139,55 @@ export class HeartRateController {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if (this.disconnecting) {
|
|
|
|
|
+ this.callbacks.onStatus('心率带断开中,请稍后再试')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if (this.scanning || this.connecting) {
|
|
if (this.scanning || this.connecting) {
|
|
|
this.callbacks.onStatus('心率带连接进行中')
|
|
this.callbacks.onStatus('心率带连接进行中')
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const wxAny = wx as any
|
|
|
|
|
- wxAny.openBluetoothAdapter({
|
|
|
|
|
- success: () => {
|
|
|
|
|
|
|
+ this.manualDisconnect = false
|
|
|
|
|
+ this.reconnecting = false
|
|
|
|
|
+ this.clearReconnectTimer()
|
|
|
|
|
+
|
|
|
|
|
+ this.withFreshBluetoothAdapter(() => {
|
|
|
|
|
+ if (this.lastDeviceId) {
|
|
|
|
|
+ this.callbacks.onStatus(`正在扫描并优先连接 ${this.lastDeviceName || '心率带'}`)
|
|
|
this.beginDiscovery()
|
|
this.beginDiscovery()
|
|
|
- },
|
|
|
|
|
- fail: (error: any) => {
|
|
|
|
|
- const message = error && error.errMsg ? error.errMsg : 'openBluetoothAdapter 失败'
|
|
|
|
|
- this.callbacks.onError(`蓝牙不可用: ${message}`)
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.beginDiscovery()
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
disconnect(): void {
|
|
disconnect(): void {
|
|
|
this.clearDiscoveryTimer()
|
|
this.clearDiscoveryTimer()
|
|
|
|
|
+ this.clearReconnectTimer()
|
|
|
this.stopDiscovery()
|
|
this.stopDiscovery()
|
|
|
|
|
|
|
|
const deviceId = this.currentDeviceId
|
|
const deviceId = this.currentDeviceId
|
|
|
this.connecting = false
|
|
this.connecting = false
|
|
|
|
|
+ this.reconnecting = false
|
|
|
|
|
+ this.manualDisconnect = true
|
|
|
|
|
+ this.disconnecting = true
|
|
|
|
|
|
|
|
if (!deviceId) {
|
|
if (!deviceId) {
|
|
|
|
|
+ this.disconnecting = false
|
|
|
this.clearConnectionState()
|
|
this.clearConnectionState()
|
|
|
this.callbacks.onStatus('心率带未连接')
|
|
this.callbacks.onStatus('心率带未连接')
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ this.disconnectingDeviceId = deviceId
|
|
|
const wxAny = wx as any
|
|
const wxAny = wx as any
|
|
|
wxAny.closeBLEConnection({
|
|
wxAny.closeBLEConnection({
|
|
|
deviceId,
|
|
deviceId,
|
|
|
complete: () => {
|
|
complete: () => {
|
|
|
|
|
+ this.disconnecting = false
|
|
|
this.clearConnectionState()
|
|
this.clearConnectionState()
|
|
|
this.callbacks.onStatus('心率带已断开')
|
|
this.callbacks.onStatus('心率带已断开')
|
|
|
},
|
|
},
|
|
@@ -150,6 +196,7 @@ export class HeartRateController {
|
|
|
|
|
|
|
|
destroy(): void {
|
|
destroy(): void {
|
|
|
this.clearDiscoveryTimer()
|
|
this.clearDiscoveryTimer()
|
|
|
|
|
+ this.clearReconnectTimer()
|
|
|
this.stopDiscovery()
|
|
this.stopDiscovery()
|
|
|
this.detachListeners()
|
|
this.detachListeners()
|
|
|
|
|
|
|
@@ -173,14 +220,20 @@ export class HeartRateController {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
beginDiscovery(): void {
|
|
beginDiscovery(): void {
|
|
|
|
|
+ if (this.scanning || this.connecting || this.connected) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
this.bindListeners()
|
|
this.bindListeners()
|
|
|
|
|
+ this.autoConnectDeviceId = this.lastDeviceId
|
|
|
const wxAny = wx as any
|
|
const wxAny = wx as any
|
|
|
wxAny.startBluetoothDevicesDiscovery({
|
|
wxAny.startBluetoothDevicesDiscovery({
|
|
|
allowDuplicatesKey: false,
|
|
allowDuplicatesKey: false,
|
|
|
services: [HEART_RATE_SERVICE_UUID],
|
|
services: [HEART_RATE_SERVICE_UUID],
|
|
|
success: () => {
|
|
success: () => {
|
|
|
this.scanning = true
|
|
this.scanning = true
|
|
|
- this.callbacks.onStatus('正在扫描心率带')
|
|
|
|
|
|
|
+ this.pruneDiscoveredDevices()
|
|
|
|
|
+ this.callbacks.onStatus(this.autoConnectDeviceId ? '正在扫描心率带并等待自动连接' : '正在扫描心率带,请选择设备')
|
|
|
this.clearDiscoveryTimer()
|
|
this.clearDiscoveryTimer()
|
|
|
this.discoveryTimer = setTimeout(() => {
|
|
this.discoveryTimer = setTimeout(() => {
|
|
|
this.discoveryTimer = 0
|
|
this.discoveryTimer = 0
|
|
@@ -189,7 +242,7 @@ export class HeartRateController {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
this.stopDiscovery()
|
|
this.stopDiscovery()
|
|
|
- this.callbacks.onError('未发现可连接的心率带')
|
|
|
|
|
|
|
+ this.callbacks.onError(this.discoveredDevices.length ? '已发现心率带,请从列表选择连接' : '未发现可连接的心率带')
|
|
|
}, DISCOVERY_TIMEOUT_MS) as unknown as number
|
|
}, DISCOVERY_TIMEOUT_MS) as unknown as number
|
|
|
},
|
|
},
|
|
|
fail: (error: any) => {
|
|
fail: (error: any) => {
|
|
@@ -207,6 +260,7 @@ export class HeartRateController {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
this.scanning = false
|
|
this.scanning = false
|
|
|
|
|
+ this.autoConnectDeviceId = null
|
|
|
const wxAny = wx as any
|
|
const wxAny = wx as any
|
|
|
wxAny.stopBluetoothDevicesDiscovery({
|
|
wxAny.stopBluetoothDevicesDiscovery({
|
|
|
complete: () => {},
|
|
complete: () => {},
|
|
@@ -224,8 +278,13 @@ export class HeartRateController {
|
|
|
? [result]
|
|
? [result]
|
|
|
: []
|
|
: []
|
|
|
|
|
|
|
|
- const targetDevice = devices.find((device: BluetoothDeviceLike) => isHeartRateDevice(device))
|
|
|
|
|
- if (!targetDevice || !targetDevice.deviceId || !this.scanning || this.connecting || this.connected) {
|
|
|
|
|
|
|
+ this.mergeDiscoveredDevices(devices)
|
|
|
|
|
+ if (!this.scanning || this.connecting || this.connected) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const targetDevice = this.selectTargetDevice(devices)
|
|
|
|
|
+ if (!targetDevice || !targetDevice.deviceId) {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -267,7 +326,7 @@ export class HeartRateController {
|
|
|
|
|
|
|
|
if (!this.connectionStateHandler) {
|
|
if (!this.connectionStateHandler) {
|
|
|
this.connectionStateHandler = (result: any) => {
|
|
this.connectionStateHandler = (result: any) => {
|
|
|
- if (!result || !this.currentDeviceId || result.deviceId !== this.currentDeviceId) {
|
|
|
|
|
|
|
+ if (!result || !result.deviceId) {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -275,8 +334,20 @@ export class HeartRateController {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if (this.disconnectingDeviceId && result.deviceId === this.disconnectingDeviceId) {
|
|
|
|
|
+ this.disconnectingDeviceId = null
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!this.currentDeviceId || result.deviceId !== this.currentDeviceId) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const disconnectedDeviceId = this.currentDeviceId
|
|
|
|
|
+ const disconnectedDeviceName = this.currentDeviceName
|
|
|
this.clearConnectionState()
|
|
this.clearConnectionState()
|
|
|
this.callbacks.onStatus('心率带连接已断开')
|
|
this.callbacks.onStatus('心率带连接已断开')
|
|
|
|
|
+ this.scheduleAutoReconnect(disconnectedDeviceId, disconnectedDeviceName)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (typeof wxAny.onBLEConnectionStateChange === 'function') {
|
|
if (typeof wxAny.onBLEConnectionStateChange === 'function') {
|
|
@@ -303,8 +374,13 @@ export class HeartRateController {
|
|
|
this.connectionStateHandler = null
|
|
this.connectionStateHandler = null
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- connectToDevice(deviceId: string, deviceName: string): void {
|
|
|
|
|
|
|
+ connectToDevice(deviceId: string, deviceName: string, fallbackToDiscovery: boolean = false): void {
|
|
|
this.connecting = true
|
|
this.connecting = true
|
|
|
|
|
+ this.reconnecting = false
|
|
|
|
|
+ this.disconnecting = false
|
|
|
|
|
+ this.manualDisconnect = false
|
|
|
|
|
+ this.disconnectingDeviceId = null
|
|
|
|
|
+ this.autoConnectDeviceId = deviceId
|
|
|
this.currentDeviceId = deviceId
|
|
this.currentDeviceId = deviceId
|
|
|
this.currentDeviceName = deviceName
|
|
this.currentDeviceName = deviceName
|
|
|
this.callbacks.onStatus(`正在连接 ${deviceName}`)
|
|
this.callbacks.onStatus(`正在连接 ${deviceName}`)
|
|
@@ -319,6 +395,11 @@ export class HeartRateController {
|
|
|
fail: (error: any) => {
|
|
fail: (error: any) => {
|
|
|
const message = error && error.errMsg ? error.errMsg : 'createBLEConnection 失败'
|
|
const message = error && error.errMsg ? error.errMsg : 'createBLEConnection 失败'
|
|
|
this.clearConnectionState()
|
|
this.clearConnectionState()
|
|
|
|
|
+ if (fallbackToDiscovery && !this.manualDisconnect) {
|
|
|
|
|
+ this.callbacks.onStatus(`直连失败,转入扫描: ${deviceName}`)
|
|
|
|
|
+ this.beginDiscovery()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
this.callbacks.onError(`连接心率带失败: ${message}`)
|
|
this.callbacks.onError(`连接心率带失败: ${message}`)
|
|
|
},
|
|
},
|
|
|
})
|
|
})
|
|
@@ -364,6 +445,15 @@ export class HeartRateController {
|
|
|
success: () => {
|
|
success: () => {
|
|
|
this.connecting = false
|
|
this.connecting = false
|
|
|
this.connected = true
|
|
this.connected = true
|
|
|
|
|
+ this.lastDeviceId = deviceId
|
|
|
|
|
+ this.lastDeviceName = deviceName
|
|
|
|
|
+ this.persistPreferredDevice()
|
|
|
|
|
+ this.manualDisconnect = false
|
|
|
|
|
+ this.reconnecting = false
|
|
|
|
|
+ this.disconnectingDeviceId = null
|
|
|
|
|
+ this.autoConnectDeviceId = deviceId
|
|
|
|
|
+ this.refreshPreferredFlags()
|
|
|
|
|
+ this.clearReconnectTimer()
|
|
|
this.callbacks.onConnectionChange(true, deviceName)
|
|
this.callbacks.onConnectionChange(true, deviceName)
|
|
|
this.callbacks.onStatus(`心率带已连接: ${deviceName}`)
|
|
this.callbacks.onStatus(`心率带已连接: ${deviceName}`)
|
|
|
},
|
|
},
|
|
@@ -393,6 +483,7 @@ export class HeartRateController {
|
|
|
complete: () => {
|
|
complete: () => {
|
|
|
this.clearConnectionState()
|
|
this.clearConnectionState()
|
|
|
this.callbacks.onError(message)
|
|
this.callbacks.onError(message)
|
|
|
|
|
+ this.scheduleAutoReconnect(this.lastDeviceId, this.lastDeviceName)
|
|
|
},
|
|
},
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
@@ -402,6 +493,9 @@ export class HeartRateController {
|
|
|
this.scanning = false
|
|
this.scanning = false
|
|
|
this.connecting = false
|
|
this.connecting = false
|
|
|
this.connected = false
|
|
this.connected = false
|
|
|
|
|
+ this.disconnecting = false
|
|
|
|
|
+ this.disconnectingDeviceId = null
|
|
|
|
|
+ this.autoConnectDeviceId = null
|
|
|
this.currentDeviceId = null
|
|
this.currentDeviceId = null
|
|
|
this.measurementServiceId = null
|
|
this.measurementServiceId = null
|
|
|
this.measurementCharacteristicId = null
|
|
this.measurementCharacteristicId = null
|
|
@@ -418,4 +512,264 @@ export class HeartRateController {
|
|
|
this.discoveryTimer = 0
|
|
this.discoveryTimer = 0
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ clearReconnectTimer(): void {
|
|
|
|
|
+ if (this.reconnectTimer) {
|
|
|
|
|
+ clearTimeout(this.reconnectTimer)
|
|
|
|
|
+ this.reconnectTimer = 0
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ selectTargetDevice(devices: BluetoothDeviceLike[]): BluetoothDeviceLike | null {
|
|
|
|
|
+ if (!Array.isArray(devices) || !devices.length) {
|
|
|
|
|
+ return null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (this.autoConnectDeviceId) {
|
|
|
|
|
+ const rememberedDevice = devices.find((device) => device && device.deviceId === this.autoConnectDeviceId)
|
|
|
|
|
+ if (rememberedDevice && isHeartRateDevice(rememberedDevice)) {
|
|
|
|
|
+ return rememberedDevice
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ scheduleAutoReconnect(deviceId: string | null, deviceName: string | null): void {
|
|
|
|
|
+ if (this.manualDisconnect || !deviceId || this.connected || this.connecting || this.scanning) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.lastDeviceId = deviceId
|
|
|
|
|
+ this.lastDeviceName = deviceName || this.lastDeviceName
|
|
|
|
|
+ this.clearReconnectTimer()
|
|
|
|
|
+ this.reconnecting = true
|
|
|
|
|
+ this.callbacks.onStatus(`心率带已断开,等待自动重连: ${this.lastDeviceName || '设备'}`)
|
|
|
|
|
+ this.reconnectTimer = setTimeout(() => {
|
|
|
|
|
+ this.reconnectTimer = 0
|
|
|
|
|
+ if (this.manualDisconnect || this.connected || this.connecting || this.scanning) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const rememberedDeviceId = this.lastDeviceId
|
|
|
|
|
+ const rememberedDeviceName = this.lastDeviceName || '心率带'
|
|
|
|
|
+ this.reconnecting = false
|
|
|
|
|
+ this.withFreshBluetoothAdapter(() => {
|
|
|
|
|
+ if (rememberedDeviceId) {
|
|
|
|
|
+ this.callbacks.onStatus(`正在自动重连 ${rememberedDeviceName}`)
|
|
|
|
|
+ this.resetConnectionAndConnect(rememberedDeviceId, rememberedDeviceName, true)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.beginDiscovery()
|
|
|
|
|
+ })
|
|
|
|
|
+ }, 600) as unknown as number
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ resetConnectionAndConnect(deviceId: string, deviceName: string, fallbackToDiscovery: boolean): void {
|
|
|
|
|
+ const wxAny = wx as any
|
|
|
|
|
+ this.disconnectingDeviceId = deviceId
|
|
|
|
|
+ wxAny.closeBLEConnection({
|
|
|
|
|
+ deviceId,
|
|
|
|
|
+ complete: () => {
|
|
|
|
|
+ if (this.disconnectingDeviceId === deviceId) {
|
|
|
|
|
+ this.disconnectingDeviceId = null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ if (this.connected || this.connecting || this.scanning || this.disconnecting) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.connectToDevice(deviceId, deviceName, fallbackToDiscovery)
|
|
|
|
|
+ }, 320)
|
|
|
|
|
+ },
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ connectToDiscoveredDevice(deviceId: string): void {
|
|
|
|
|
+ if (!deviceId) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (this.disconnecting) {
|
|
|
|
|
+ this.callbacks.onStatus('心率带断开中,请稍后再试')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const targetDevice = this.discoveredDevices.find((device) => device.deviceId === deviceId)
|
|
|
|
|
+ const deviceName = targetDevice ? targetDevice.name : (this.lastDeviceId === deviceId ? (this.lastDeviceName || '心率带') : '心率带')
|
|
|
|
|
+ this.lastDeviceId = deviceId
|
|
|
|
|
+ this.lastDeviceName = deviceName
|
|
|
|
|
+ this.refreshPreferredFlags()
|
|
|
|
|
+ this.stopDiscovery()
|
|
|
|
|
+
|
|
|
|
|
+ this.withFreshBluetoothAdapter(() => {
|
|
|
|
|
+ this.callbacks.onStatus(`正在连接 ${deviceName}`)
|
|
|
|
|
+ this.resetConnectionAndConnect(deviceId, deviceName, true)
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ clearPreferredDevice(): void {
|
|
|
|
|
+ this.lastDeviceId = null
|
|
|
|
|
+ this.lastDeviceName = null
|
|
|
|
|
+ this.autoConnectDeviceId = null
|
|
|
|
|
+ this.removePreferredDevice()
|
|
|
|
|
+ this.refreshPreferredFlags()
|
|
|
|
|
+ this.callbacks.onStatus('已清除首选心率带')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ mergeDiscoveredDevices(devices: BluetoothDeviceLike[]): void {
|
|
|
|
|
+ if (!Array.isArray(devices) || !devices.length) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const now = Date.now()
|
|
|
|
|
+ let changed = false
|
|
|
|
|
+ const nextDevices = [...this.discoveredDevices]
|
|
|
|
|
+ for (const rawDevice of devices) {
|
|
|
|
|
+ if (!rawDevice || !rawDevice.deviceId || !isHeartRateDevice(rawDevice)) {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const name = getDeviceDisplayName(rawDevice)
|
|
|
|
|
+ const rssi = typeof rawDevice.RSSI === 'number' && Number.isFinite(rawDevice.RSSI) ? rawDevice.RSSI : null
|
|
|
|
|
+ const existingIndex = nextDevices.findIndex((item) => item.deviceId === rawDevice.deviceId)
|
|
|
|
|
+ const nextDevice: HeartRateDiscoveredDevice = {
|
|
|
|
|
+ deviceId: rawDevice.deviceId,
|
|
|
|
|
+ name,
|
|
|
|
|
+ rssi,
|
|
|
|
|
+ lastSeenAt: now,
|
|
|
|
|
+ isPreferred: rawDevice.deviceId === this.lastDeviceId,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (existingIndex >= 0) {
|
|
|
|
|
+ nextDevices[existingIndex] = nextDevice
|
|
|
|
|
+ } else {
|
|
|
|
|
+ nextDevices.push(nextDevice)
|
|
|
|
|
+ }
|
|
|
|
|
+ changed = true
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!changed) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.discoveredDevices = this.sortDiscoveredDevices(nextDevices.filter((device) => now - device.lastSeenAt <= DEVICE_STALE_MS))
|
|
|
|
|
+ this.callbacks.onDeviceListChange([...this.discoveredDevices])
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ pruneDiscoveredDevices(): void {
|
|
|
|
|
+ const now = Date.now()
|
|
|
|
|
+ const nextDevices = this.sortDiscoveredDevices(
|
|
|
|
|
+ this.discoveredDevices.filter((device) => now - device.lastSeenAt <= DEVICE_STALE_MS),
|
|
|
|
|
+ )
|
|
|
|
|
+ const changed = nextDevices.length !== this.discoveredDevices.length
|
|
|
|
|
+ || nextDevices.some((device, index) => {
|
|
|
|
|
+ const previous = this.discoveredDevices[index]
|
|
|
|
|
+ return !previous || previous.deviceId !== device.deviceId || previous.isPreferred !== device.isPreferred || previous.rssi !== device.rssi
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ this.discoveredDevices = nextDevices
|
|
|
|
|
+ if (changed) {
|
|
|
|
|
+ this.callbacks.onDeviceListChange([...this.discoveredDevices])
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ refreshPreferredFlags(): void {
|
|
|
|
|
+ if (!this.discoveredDevices.length) {
|
|
|
|
|
+ this.callbacks.onDeviceListChange([])
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.discoveredDevices = this.sortDiscoveredDevices(
|
|
|
|
|
+ this.discoveredDevices.map((device) => ({
|
|
|
|
|
+ ...device,
|
|
|
|
|
+ isPreferred: !!this.lastDeviceId && device.deviceId === this.lastDeviceId,
|
|
|
|
|
+ })),
|
|
|
|
|
+ )
|
|
|
|
|
+ this.callbacks.onDeviceListChange([...this.discoveredDevices])
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ sortDiscoveredDevices(devices: HeartRateDiscoveredDevice[]): HeartRateDiscoveredDevice[] {
|
|
|
|
|
+ return [...devices].sort((a, b) => {
|
|
|
|
|
+ if (a.isPreferred !== b.isPreferred) {
|
|
|
|
|
+ return a.isPreferred ? -1 : 1
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const aRssi = a.rssi === null ? -999 : a.rssi
|
|
|
|
|
+ const bRssi = b.rssi === null ? -999 : b.rssi
|
|
|
|
|
+ if (aRssi !== bRssi) {
|
|
|
|
|
+ return bRssi - aRssi
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return b.lastSeenAt - a.lastSeenAt
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ withFreshBluetoothAdapter(onReady: () => void): void {
|
|
|
|
|
+ const wxAny = wx as any
|
|
|
|
|
+ const openAdapter = () => {
|
|
|
|
|
+ wxAny.openBluetoothAdapter({
|
|
|
|
|
+ success: () => {
|
|
|
|
|
+ onReady()
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (error: any) => {
|
|
|
|
|
+ const message = error && error.errMsg ? error.errMsg : 'openBluetoothAdapter 失败'
|
|
|
|
|
+ this.callbacks.onError(`蓝牙不可用: ${message}`)
|
|
|
|
|
+ },
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (typeof wxAny.closeBluetoothAdapter !== 'function') {
|
|
|
|
|
+ openAdapter()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ wxAny.closeBluetoothAdapter({
|
|
|
|
|
+ complete: () => {
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ openAdapter()
|
|
|
|
|
+ }, 180)
|
|
|
|
|
+ },
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ restorePreferredDevice(): void {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const stored = wx.getStorageSync(PREFERRED_HEART_RATE_DEVICE_STORAGE_KEY)
|
|
|
|
|
+ if (!stored || typeof stored !== 'object') {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const normalized = stored as { deviceId?: unknown; name?: unknown }
|
|
|
|
|
+ if (typeof normalized.deviceId !== 'string' || !normalized.deviceId) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.lastDeviceId = normalized.deviceId
|
|
|
|
|
+ this.lastDeviceName = typeof normalized.name === 'string' && normalized.name ? normalized.name : '心率带'
|
|
|
|
|
+ } catch {}
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ persistPreferredDevice(): void {
|
|
|
|
|
+ if (!this.lastDeviceId) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ wx.setStorageSync(PREFERRED_HEART_RATE_DEVICE_STORAGE_KEY, {
|
|
|
|
|
+ deviceId: this.lastDeviceId,
|
|
|
|
|
+ name: this.lastDeviceName || '心率带',
|
|
|
|
|
+ })
|
|
|
|
|
+ } catch {}
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ removePreferredDevice(): void {
|
|
|
|
|
+ try {
|
|
|
|
|
+ wx.removeStorageSync(PREFERRED_HEART_RATE_DEVICE_STORAGE_KEY)
|
|
|
|
|
+ } catch {}
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|