Improve heart rate device reconnect flow

This commit is contained in:
2026-03-24 17:17:29 +08:00
parent 71ad6c6535
commit 0ccf7daf50
8 changed files with 831 additions and 72 deletions

View File

@@ -1,6 +1,6 @@
import { getTileSizePx, screenToWorld, worldToScreen, type CameraState } from '../camera/camera'
import { CompassHeadingController } from '../sensor/compassHeadingController'
import { HeartRateController } from '../sensor/heartRateController'
import { HeartRateController, type HeartRateDiscoveredDevice } from '../sensor/heartRateController'
import { LocationController } from '../sensor/locationController'
import { WebGLMapRenderer } from '../renderer/webglMapRenderer'
import { type MapRendererStats } from '../renderer/mapRenderer'
@@ -132,6 +132,14 @@ export interface MapEngineViewState {
heartRateConnected: boolean
heartRateStatusText: string
heartRateDeviceText: string
heartRateScanText: string
heartRateDiscoveredDevices: Array<{
deviceId: string
name: string
rssiText: string
preferred: boolean
connected: boolean
}>
gameSessionStatus: 'idle' | 'running' | 'finished' | 'failed'
gameModeText: string
panelTimerText: string
@@ -227,6 +235,8 @@ const VIEW_SYNC_KEYS: Array<keyof MapEngineViewState> = [
'heartRateConnected',
'heartRateStatusText',
'heartRateDeviceText',
'heartRateScanText',
'heartRateDiscoveredDevices',
'gameSessionStatus',
'gameModeText',
'panelTimerText',
@@ -612,37 +622,64 @@ export class MapEngine {
this.setState(this.getLocationControllerViewPatch(), true)
},
})
this.heartRateController = new HeartRateController({
onHeartRate: (bpm) => {
this.telemetryRuntime.dispatch({
type: 'heart_rate_updated',
at: Date.now(),
this.heartRateController = new HeartRateController({
onHeartRate: (bpm) => {
this.telemetryRuntime.dispatch({
type: 'heart_rate_updated',
at: Date.now(),
bpm,
})
this.syncSessionTimerText()
},
onStatus: (message) => {
this.setState({
heartRateStatusText: message,
heartRateDeviceText: this.heartRateController.currentDeviceName || '--',
}, true)
},
onError: (message) => {
this.setState({
heartRateConnected: false,
heartRateStatusText: message,
heartRateDeviceText: '--',
statusText: `${message} (${this.buildVersion})`,
}, true)
},
onConnectionChange: (connected, deviceName) => {
this.setState({
heartRateConnected: connected,
heartRateDeviceText: deviceName || '--',
heartRateStatusText: connected ? '心率带已连接' : '心率带未连接',
}, true)
},
})
},
onStatus: (message) => {
const deviceName = this.heartRateController.currentDeviceName
|| (this.heartRateController.reconnecting ? this.heartRateController.lastDeviceName : null)
|| '--'
this.setState({
heartRateStatusText: message,
heartRateDeviceText: deviceName,
heartRateScanText: this.getHeartRateScanText(),
}, true)
},
onError: (message) => {
this.clearHeartRateSignal()
const deviceName = this.heartRateController.reconnecting
? (this.heartRateController.lastDeviceName || '--')
: '--'
this.setState({
heartRateConnected: false,
heartRateStatusText: message,
heartRateDeviceText: deviceName,
heartRateScanText: this.getHeartRateScanText(),
statusText: `${message} (${this.buildVersion})`,
}, true)
},
onConnectionChange: (connected, deviceName) => {
if (!connected) {
this.clearHeartRateSignal()
}
const resolvedDeviceName = connected
? (deviceName || '--')
: (this.heartRateController.reconnecting
? (this.heartRateController.lastDeviceName || '--')
: '--')
this.setState({
heartRateConnected: connected,
heartRateDeviceText: resolvedDeviceName,
heartRateStatusText: connected
? '心率带已连接'
: (this.heartRateController.reconnecting ? '心率带自动重连中' : '心率带未连接'),
heartRateScanText: this.getHeartRateScanText(),
heartRateDiscoveredDevices: this.formatHeartRateDevices(this.heartRateController.discoveredDevices),
}, true)
},
onDeviceListChange: (devices) => {
this.setState({
heartRateDiscoveredDevices: this.formatHeartRateDevices(devices),
heartRateScanText: this.getHeartRateScanText(),
}, true)
},
})
this.feedbackDirector = new FeedbackDirector({
showPunchFeedback: (text, tone, motionClass) => {
this.showPunchFeedback(text, tone, motionClass)
@@ -745,9 +782,11 @@ export class MapEngine {
mockCoordText: '--',
mockSpeedText: '--',
gpsCoordText: '--',
heartRateConnected: false,
heartRateStatusText: '心率带未连接',
heartRateDeviceText: '--',
heartRateConnected: false,
heartRateStatusText: '心率带未连接',
heartRateDeviceText: '--',
heartRateScanText: '未扫描',
heartRateDiscoveredDevices: [],
panelTimerText: '00:00:00',
panelMileageText: '0m',
panelActionTagText: '目标',
@@ -848,6 +887,14 @@ export class MapEngine {
this.mounted = false
}
handleAppShow(): void {
this.feedbackDirector.setAppAudioMode('foreground')
}
handleAppHide(): void {
this.feedbackDirector.setAppAudioMode('foreground')
}
clearGameRuntime(): void {
this.gameRuntime.clear()
@@ -858,6 +905,15 @@ export class MapEngine {
this.setCourseHeading(null)
}
clearHeartRateSignal(): void {
this.telemetryRuntime.dispatch({
type: 'heart_rate_updated',
at: Date.now(),
bpm: null,
})
this.syncSessionTimerText()
}
clearFinishedTestOverlay(): void {
this.currentGpsPoint = null
this.currentGpsTrack = []
@@ -1386,6 +1442,18 @@ export class MapEngine {
this.heartRateController.disconnect()
}
handleConnectHeartRateDevice(deviceId: string): void {
this.heartRateController.connectToDiscoveredDevice(deviceId)
}
handleClearPreferredHeartRateDevice(): void {
this.heartRateController.clearPreferredDevice()
this.setState({
heartRateDeviceText: this.heartRateController.currentDeviceName || '--',
heartRateScanText: this.getHeartRateScanText(),
}, true)
}
handleDebugHeartRateTone(tone: HeartRateTone): void {
const sampleBpm = getHeartRateToneSampleBpm(tone, this.telemetryRuntime.config)
this.telemetryRuntime.dispatch({
@@ -1407,9 +1475,42 @@ export class MapEngine {
})
this.setState({
heartRateStatusText: this.heartRateController.connected ? '心率带已连接' : '心率带未连接',
heartRateScanText: this.getHeartRateScanText(),
}, true)
this.syncSessionTimerText()
}
formatHeartRateDevices(devices: HeartRateDiscoveredDevice[]): Array<{ deviceId: string; name: string; rssiText: string; preferred: boolean; connected: boolean }> {
return devices.map((device) => ({
deviceId: device.deviceId,
name: device.name,
rssiText: device.rssi === null ? '--' : `${device.rssi} dBm`,
preferred: device.isPreferred,
connected: !!this.heartRateController.currentDeviceId && this.heartRateController.currentDeviceId === device.deviceId && this.heartRateController.connected,
}))
}
getHeartRateScanText(): string {
if (this.heartRateController.connected) {
return '已连接'
}
if (this.heartRateController.connecting) {
return '连接中'
}
if (this.heartRateController.disconnecting) {
return '断开中'
}
if (this.heartRateController.scanning) {
return this.heartRateController.lastDeviceId ? '扫描中(优先首选)' : '扫描中(等待选择)'
}
return this.heartRateController.discoveredDevices.length
? `已发现 ${this.heartRateController.discoveredDevices.length} 个设备`
: '未扫描'
}
setStage(rect: MapEngineStageRect): void {
this.previewScale = 1
this.previewOriginX = rect.width / 2