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 mockChannelIdText: 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-hr(?:\?.*)?$/i.test(normalized)) { normalized = normalized.replace(/\/+$/, '') normalized = `${normalized}/mock-hr` } return normalized } export class HeartRateInputController { callbacks: HeartRateInputControllerCallbacks realController: HeartRateController mockBridge: MockHeartRateBridge sourceMode: HeartRateSourceMode mockBridgeStatusText: string mockBridgeUrl: string mockChannelId: 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.mockChannelId = 'default' 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, mockChannelIdText: this.mockChannelId, 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() } setMockChannelId(channelId: string): void { const normalized = String(channelId || '').trim() || 'default' this.mockChannelId = normalized this.mockBridge.setChannelId(normalized) if (this.sourceMode === 'mock') { this.callbacks.onStatus(`模拟心率通道已切换到 ${normalized}`) } 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() } } }