Add mock heart rate simulator flow
This commit is contained in:
325
miniprogram/engine/sensor/heartRateInputController.ts
Normal file
325
miniprogram/engine/sensor/heartRateInputController.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
import { HeartRateController, type HeartRateControllerCallbacks, type HeartRateDiscoveredDevice } from './heartRateController'
|
||||
import { DEFAULT_MOCK_HEART_RATE_BRIDGE_URL, MockHeartRateBridge } from './mockHeartRateBridge'
|
||||
|
||||
export type HeartRateSourceMode = 'real' | 'mock'
|
||||
|
||||
export interface HeartRateInputControllerCallbacks {
|
||||
onHeartRate: (bpm: number) => void
|
||||
onStatus: (message: string) => void
|
||||
onError: (message: string) => void
|
||||
onConnectionChange: (connected: boolean, deviceName: string | null) => void
|
||||
onDeviceListChange: (devices: HeartRateDiscoveredDevice[]) => void
|
||||
onDebugStateChange?: () => void
|
||||
}
|
||||
|
||||
export interface HeartRateInputControllerDebugState {
|
||||
sourceMode: HeartRateSourceMode
|
||||
sourceModeText: string
|
||||
mockBridgeConnected: boolean
|
||||
mockBridgeStatusText: string
|
||||
mockBridgeUrlText: string
|
||||
mockHeartRateText: string
|
||||
}
|
||||
|
||||
function formatSourceModeText(mode: HeartRateSourceMode): string {
|
||||
return mode === 'mock' ? '模拟心率' : '真实心率'
|
||||
}
|
||||
|
||||
function formatMockHeartRateText(bpm: number | null): string {
|
||||
return bpm === null ? '--' : `${bpm} bpm`
|
||||
}
|
||||
|
||||
function normalizeMockBridgeUrl(rawUrl: string): string {
|
||||
const trimmed = rawUrl.trim()
|
||||
if (!trimmed) {
|
||||
return DEFAULT_MOCK_HEART_RATE_BRIDGE_URL
|
||||
}
|
||||
|
||||
let normalized = trimmed
|
||||
if (!/^wss?:\/\//i.test(normalized)) {
|
||||
normalized = `ws://${normalized.replace(/^\/+/, '')}`
|
||||
}
|
||||
|
||||
if (!/\/mock-gps(?:\?.*)?$/i.test(normalized)) {
|
||||
normalized = normalized.replace(/\/+$/, '')
|
||||
normalized = `${normalized}/mock-gps`
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
export class HeartRateInputController {
|
||||
callbacks: HeartRateInputControllerCallbacks
|
||||
realController: HeartRateController
|
||||
mockBridge: MockHeartRateBridge
|
||||
sourceMode: HeartRateSourceMode
|
||||
mockBridgeStatusText: string
|
||||
mockBridgeUrl: string
|
||||
mockBpm: number | null
|
||||
|
||||
constructor(callbacks: HeartRateInputControllerCallbacks) {
|
||||
this.callbacks = callbacks
|
||||
this.sourceMode = 'real'
|
||||
this.mockBridgeUrl = DEFAULT_MOCK_HEART_RATE_BRIDGE_URL
|
||||
this.mockBridgeStatusText = `未连接 (${this.mockBridgeUrl})`
|
||||
this.mockBpm = null
|
||||
|
||||
const realCallbacks: HeartRateControllerCallbacks = {
|
||||
onHeartRate: (bpm) => {
|
||||
if (this.sourceMode !== 'real') {
|
||||
return
|
||||
}
|
||||
|
||||
this.callbacks.onHeartRate(bpm)
|
||||
this.emitDebugState()
|
||||
},
|
||||
onStatus: (message) => {
|
||||
if (this.sourceMode !== 'real') {
|
||||
return
|
||||
}
|
||||
|
||||
this.callbacks.onStatus(message)
|
||||
this.emitDebugState()
|
||||
},
|
||||
onError: (message) => {
|
||||
if (this.sourceMode !== 'real') {
|
||||
return
|
||||
}
|
||||
|
||||
this.callbacks.onError(message)
|
||||
this.emitDebugState()
|
||||
},
|
||||
onConnectionChange: (connected, deviceName) => {
|
||||
if (this.sourceMode !== 'real') {
|
||||
return
|
||||
}
|
||||
|
||||
this.callbacks.onConnectionChange(connected, deviceName)
|
||||
this.emitDebugState()
|
||||
},
|
||||
onDeviceListChange: (devices) => {
|
||||
this.callbacks.onDeviceListChange(devices)
|
||||
this.emitDebugState()
|
||||
},
|
||||
}
|
||||
|
||||
this.realController = new HeartRateController(realCallbacks)
|
||||
this.mockBridge = new MockHeartRateBridge({
|
||||
onOpen: () => {
|
||||
this.mockBridgeStatusText = `已连接 (${this.mockBridge.url})`
|
||||
if (this.sourceMode === 'mock') {
|
||||
this.callbacks.onConnectionChange(true, '模拟心率源')
|
||||
this.callbacks.onStatus('模拟心率源已连接,等待外部输入')
|
||||
}
|
||||
this.emitDebugState()
|
||||
},
|
||||
onClose: () => {
|
||||
this.mockBridgeStatusText = `未连接 (${this.mockBridge.url})`
|
||||
if (this.sourceMode === 'mock') {
|
||||
this.callbacks.onConnectionChange(false, null)
|
||||
this.callbacks.onStatus('模拟心率源已断开')
|
||||
}
|
||||
this.emitDebugState()
|
||||
},
|
||||
onError: (message) => {
|
||||
this.mockBridgeStatusText = `连接失败 (${this.mockBridge.url})`
|
||||
if (this.sourceMode === 'mock') {
|
||||
this.callbacks.onConnectionChange(false, null)
|
||||
this.callbacks.onError(`模拟心率源错误: ${message}`)
|
||||
}
|
||||
this.emitDebugState()
|
||||
},
|
||||
onBpm: (bpm) => {
|
||||
this.mockBpm = bpm
|
||||
if (this.sourceMode === 'mock') {
|
||||
this.callbacks.onHeartRate(bpm)
|
||||
}
|
||||
this.emitDebugState()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
get currentDeviceId(): string | null {
|
||||
if (this.sourceMode === 'mock') {
|
||||
return this.mockBridge.connected ? 'mock-heart-rate' : null
|
||||
}
|
||||
|
||||
return this.realController.currentDeviceId
|
||||
}
|
||||
|
||||
get currentDeviceName(): string | null {
|
||||
if (this.sourceMode === 'mock') {
|
||||
return this.mockBridge.connected ? '模拟心率源' : null
|
||||
}
|
||||
|
||||
return this.realController.currentDeviceName
|
||||
}
|
||||
|
||||
get connected(): boolean {
|
||||
return this.sourceMode === 'mock' ? this.mockBridge.connected : this.realController.connected
|
||||
}
|
||||
|
||||
get connecting(): boolean {
|
||||
return this.sourceMode === 'mock' ? this.mockBridge.connecting : this.realController.connecting
|
||||
}
|
||||
|
||||
get scanning(): boolean {
|
||||
return this.sourceMode === 'mock' ? false : this.realController.scanning
|
||||
}
|
||||
|
||||
get reconnecting(): boolean {
|
||||
return this.sourceMode === 'mock' ? false : this.realController.reconnecting
|
||||
}
|
||||
|
||||
get disconnecting(): boolean {
|
||||
return this.sourceMode === 'mock' ? false : this.realController.disconnecting
|
||||
}
|
||||
|
||||
get discoveredDevices(): HeartRateDiscoveredDevice[] {
|
||||
return this.realController.discoveredDevices
|
||||
}
|
||||
|
||||
get lastDeviceId(): string | null {
|
||||
return this.realController.lastDeviceId
|
||||
}
|
||||
|
||||
get lastDeviceName(): string | null {
|
||||
return this.realController.lastDeviceName
|
||||
}
|
||||
|
||||
getDebugState(): HeartRateInputControllerDebugState {
|
||||
return {
|
||||
sourceMode: this.sourceMode,
|
||||
sourceModeText: formatSourceModeText(this.sourceMode),
|
||||
mockBridgeConnected: this.mockBridge.connected,
|
||||
mockBridgeStatusText: this.mockBridgeStatusText,
|
||||
mockBridgeUrlText: this.mockBridgeUrl,
|
||||
mockHeartRateText: formatMockHeartRateText(this.mockBpm),
|
||||
}
|
||||
}
|
||||
|
||||
startScanAndConnect(): void {
|
||||
if (this.sourceMode === 'mock') {
|
||||
this.callbacks.onStatus(this.mockBridge.connected ? '模拟心率源已连接' : '当前为模拟心率模式,请连接模拟源')
|
||||
this.emitDebugState()
|
||||
return
|
||||
}
|
||||
|
||||
this.realController.startScanAndConnect()
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
if (this.sourceMode === 'mock') {
|
||||
if (!this.mockBridge.connected && !this.mockBridge.connecting) {
|
||||
this.callbacks.onStatus('模拟心率源未连接')
|
||||
this.emitDebugState()
|
||||
return
|
||||
}
|
||||
|
||||
this.mockBridge.disconnect()
|
||||
this.emitDebugState()
|
||||
return
|
||||
}
|
||||
|
||||
this.realController.disconnect()
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.realController.destroy()
|
||||
this.mockBridge.destroy()
|
||||
}
|
||||
|
||||
setSourceMode(mode: HeartRateSourceMode): void {
|
||||
if (this.sourceMode === mode) {
|
||||
this.callbacks.onStatus(`${formatSourceModeText(mode)}已启用`)
|
||||
this.emitDebugState()
|
||||
return
|
||||
}
|
||||
|
||||
const previousMode = this.sourceMode
|
||||
this.sourceMode = mode
|
||||
|
||||
if (previousMode === 'real') {
|
||||
this.realController.disconnect()
|
||||
} else {
|
||||
this.callbacks.onConnectionChange(false, null)
|
||||
}
|
||||
|
||||
const activeDeviceName = this.currentDeviceName
|
||||
this.callbacks.onConnectionChange(this.connected, activeDeviceName)
|
||||
this.callbacks.onStatus(mode === 'mock' ? '已切换到模拟心率模式' : '已切换到真实心率模式')
|
||||
this.emitDebugState()
|
||||
}
|
||||
|
||||
setMockBridgeUrl(url: string): void {
|
||||
this.mockBridgeUrl = normalizeMockBridgeUrl(url)
|
||||
|
||||
if (this.mockBridge.connected || this.mockBridge.connecting) {
|
||||
this.mockBridgeStatusText = `已设置新地址,重连生效 (${this.mockBridgeUrl})`
|
||||
if (this.sourceMode === 'mock') {
|
||||
this.callbacks.onStatus('模拟心率源地址已更新,重连后生效')
|
||||
}
|
||||
} else {
|
||||
this.mockBridgeStatusText = `未连接 (${this.mockBridgeUrl})`
|
||||
if (this.sourceMode === 'mock') {
|
||||
this.callbacks.onStatus('模拟心率源地址已更新')
|
||||
}
|
||||
}
|
||||
|
||||
this.emitDebugState()
|
||||
}
|
||||
|
||||
connectMockBridge(url = DEFAULT_MOCK_HEART_RATE_BRIDGE_URL): void {
|
||||
if (this.mockBridge.connected || this.mockBridge.connecting) {
|
||||
if (this.sourceMode === 'mock') {
|
||||
this.callbacks.onStatus('模拟心率源已连接')
|
||||
}
|
||||
this.emitDebugState()
|
||||
return
|
||||
}
|
||||
|
||||
const targetUrl = normalizeMockBridgeUrl(url === DEFAULT_MOCK_HEART_RATE_BRIDGE_URL ? this.mockBridgeUrl : url)
|
||||
this.mockBridgeUrl = targetUrl
|
||||
this.mockBridgeStatusText = `连接中 (${targetUrl})`
|
||||
if (this.sourceMode === 'mock') {
|
||||
this.callbacks.onStatus('模拟心率源连接中')
|
||||
}
|
||||
this.emitDebugState()
|
||||
this.mockBridge.connect(targetUrl)
|
||||
}
|
||||
|
||||
disconnectMockBridge(): void {
|
||||
if (!this.mockBridge.connected && !this.mockBridge.connecting) {
|
||||
if (this.sourceMode === 'mock') {
|
||||
this.callbacks.onStatus('模拟心率源未连接')
|
||||
}
|
||||
this.emitDebugState()
|
||||
return
|
||||
}
|
||||
|
||||
this.mockBridge.disconnect()
|
||||
this.mockBridgeStatusText = `未连接 (${this.mockBridge.url})`
|
||||
this.emitDebugState()
|
||||
}
|
||||
|
||||
connectToDiscoveredDevice(deviceId: string): void {
|
||||
if (this.sourceMode !== 'real') {
|
||||
this.callbacks.onStatus('当前为模拟心率模式,无法连接真实心率带')
|
||||
this.emitDebugState()
|
||||
return
|
||||
}
|
||||
|
||||
this.realController.connectToDiscoveredDevice(deviceId)
|
||||
}
|
||||
|
||||
clearPreferredDevice(): void {
|
||||
this.realController.clearPreferredDevice()
|
||||
this.emitDebugState()
|
||||
}
|
||||
|
||||
emitDebugState(): void {
|
||||
if (this.callbacks.onDebugStateChange) {
|
||||
this.callbacks.onDebugStateChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user