Add mock heart rate simulator flow

This commit is contained in:
2026-03-24 18:28:21 +08:00
parent 0ccf7daf50
commit 3f6563c992
9 changed files with 892 additions and 29 deletions

View File

@@ -1,6 +1,7 @@
import { getTileSizePx, screenToWorld, worldToScreen, type CameraState } from '../camera/camera'
import { CompassHeadingController } from '../sensor/compassHeadingController'
import { HeartRateController, type HeartRateDiscoveredDevice } from '../sensor/heartRateController'
import { type HeartRateDiscoveredDevice } from '../sensor/heartRateController'
import { HeartRateInputController } from '../sensor/heartRateInputController'
import { LocationController } from '../sensor/locationController'
import { WebGLMapRenderer } from '../renderer/webglMapRenderer'
import { type MapRendererStats } from '../renderer/mapRenderer'
@@ -129,6 +130,8 @@ export interface MapEngineViewState {
mockCoordText: string
mockSpeedText: string
gpsCoordText: string
heartRateSourceMode: 'real' | 'mock'
heartRateSourceText: string
heartRateConnected: boolean
heartRateStatusText: string
heartRateDeviceText: string
@@ -140,6 +143,10 @@ export interface MapEngineViewState {
preferred: boolean
connected: boolean
}>
mockHeartRateBridgeConnected: boolean
mockHeartRateBridgeStatusText: string
mockHeartRateBridgeUrlText: string
mockHeartRateText: string
gameSessionStatus: 'idle' | 'running' | 'finished' | 'failed'
gameModeText: string
panelTimerText: string
@@ -232,11 +239,17 @@ const VIEW_SYNC_KEYS: Array<keyof MapEngineViewState> = [
'mockCoordText',
'mockSpeedText',
'gpsCoordText',
'heartRateSourceMode',
'heartRateSourceText',
'heartRateConnected',
'heartRateStatusText',
'heartRateDeviceText',
'heartRateScanText',
'heartRateDiscoveredDevices',
'mockHeartRateBridgeConnected',
'mockHeartRateBridgeStatusText',
'mockHeartRateBridgeUrlText',
'mockHeartRateText',
'gameSessionStatus',
'gameModeText',
'panelTimerText',
@@ -514,7 +527,7 @@ export class MapEngine {
renderer: WebGLMapRenderer
compassController: CompassHeadingController
locationController: LocationController
heartRateController: HeartRateController
heartRateController: HeartRateInputController
feedbackDirector: FeedbackDirector
onData: (patch: Partial<MapEngineViewState>) => void
state: MapEngineViewState
@@ -622,7 +635,7 @@ export class MapEngine {
this.setState(this.getLocationControllerViewPatch(), true)
},
})
this.heartRateController = new HeartRateController({
this.heartRateController = new HeartRateInputController({
onHeartRate: (bpm) => {
this.telemetryRuntime.dispatch({
type: 'heart_rate_updated',
@@ -639,6 +652,7 @@ export class MapEngine {
heartRateStatusText: message,
heartRateDeviceText: deviceName,
heartRateScanText: this.getHeartRateScanText(),
...this.getHeartRateControllerViewPatch(),
}, true)
},
onError: (message) => {
@@ -651,6 +665,7 @@ export class MapEngine {
heartRateStatusText: message,
heartRateDeviceText: deviceName,
heartRateScanText: this.getHeartRateScanText(),
...this.getHeartRateControllerViewPatch(),
statusText: `${message} (${this.buildVersion})`,
}, true)
},
@@ -667,18 +682,23 @@ export class MapEngine {
heartRateConnected: connected,
heartRateDeviceText: resolvedDeviceName,
heartRateStatusText: connected
? '心率带已连接'
: (this.heartRateController.reconnecting ? '心率带自动重连中' : '心率带未连接'),
? (this.heartRateController.sourceMode === 'mock' ? '模拟心率源已连接' : '心率带已连接')
: (this.heartRateController.reconnecting ? '心率带自动重连中' : (this.heartRateController.sourceMode === 'mock' ? '模拟心率源未连接' : '心率带未连接')),
heartRateScanText: this.getHeartRateScanText(),
heartRateDiscoveredDevices: this.formatHeartRateDevices(this.heartRateController.discoveredDevices),
...this.getHeartRateControllerViewPatch(),
}, true)
},
onDeviceListChange: (devices) => {
this.setState({
heartRateDiscoveredDevices: this.formatHeartRateDevices(devices),
heartRateScanText: this.getHeartRateScanText(),
...this.getHeartRateControllerViewPatch(),
}, true)
},
onDebugStateChange: () => {
this.setState(this.getHeartRateControllerViewPatch(), true)
},
})
this.feedbackDirector = new FeedbackDirector({
showPunchFeedback: (text, tone, motionClass) => {
@@ -782,11 +802,17 @@ export class MapEngine {
mockCoordText: '--',
mockSpeedText: '--',
gpsCoordText: '--',
heartRateSourceMode: 'real',
heartRateSourceText: '真实心率',
heartRateConnected: false,
heartRateStatusText: '心率带未连接',
heartRateDeviceText: '--',
heartRateScanText: '未扫描',
heartRateDiscoveredDevices: [],
mockHeartRateBridgeConnected: false,
mockHeartRateBridgeStatusText: '未连接',
mockHeartRateBridgeUrlText: 'wss://gs.gotomars.xyz/mock-gps',
mockHeartRateText: '--',
panelTimerText: '00:00:00',
panelMileageText: '0m',
panelActionTagText: '目标',
@@ -951,6 +977,18 @@ export class MapEngine {
}
}
getHeartRateControllerViewPatch(): Partial<MapEngineViewState> {
const debugState = this.heartRateController.getDebugState()
return {
heartRateSourceMode: debugState.sourceMode,
heartRateSourceText: debugState.sourceModeText,
mockHeartRateBridgeConnected: debugState.mockBridgeConnected,
mockHeartRateBridgeStatusText: debugState.mockBridgeStatusText,
mockHeartRateBridgeUrlText: debugState.mockBridgeUrlText,
mockHeartRateText: debugState.mockHeartRateText,
}
}
getGameModeText(): string {
return this.gameMode === 'score-o' ? '积分赛' : '顺序赛'
}
@@ -1442,6 +1480,26 @@ export class MapEngine {
this.heartRateController.disconnect()
}
handleSetRealHeartRateMode(): void {
this.heartRateController.setSourceMode('real')
}
handleSetMockHeartRateMode(): void {
this.heartRateController.setSourceMode('mock')
}
handleConnectMockHeartRateBridge(): void {
this.heartRateController.connectMockBridge()
}
handleDisconnectMockHeartRateBridge(): void {
this.heartRateController.disconnectMockBridge()
}
handleSetMockHeartRateBridgeUrl(url: string): void {
this.heartRateController.setMockBridgeUrl(url)
}
handleConnectHeartRateDevice(deviceId: string): void {
this.heartRateController.connectToDiscoveredDevice(deviceId)
}
@@ -1474,8 +1532,11 @@ export class MapEngine {
bpm: null,
})
this.setState({
heartRateStatusText: this.heartRateController.connected ? '心率带已连接' : '心率带未连接',
heartRateStatusText: this.heartRateController.connected
? (this.heartRateController.sourceMode === 'mock' ? '模拟心率源已连接' : '心率带已连接')
: (this.heartRateController.sourceMode === 'mock' ? '模拟心率源未连接' : '心率带未连接'),
heartRateScanText: this.getHeartRateScanText(),
...this.getHeartRateControllerViewPatch(),
}, true)
this.syncSessionTimerText()
}
@@ -1491,6 +1552,18 @@ export class MapEngine {
}
getHeartRateScanText(): string {
if (this.heartRateController.sourceMode === 'mock') {
if (this.heartRateController.connected) {
return '模拟源已连接'
}
if (this.heartRateController.connecting) {
return '模拟源连接中'
}
return '模拟模式'
}
if (this.heartRateController.connected) {
return '已连接'
}