export interface TelemetryConfig { heartRateAge: number restingHeartRateBpm: number userWeightKg: number } export type HeartRateTone = 'blue' | 'purple' | 'green' | 'yellow' | 'orange' | 'red' type HeartRateToneMeta = { label: string heartRateRangeText: string speedRangeText: string } const HEART_RATE_TONE_META: Record = { blue: { label: '激活放松', heartRateRangeText: '<=39%', speedRangeText: '<3.2 km/h', }, purple: { label: '动态热身', heartRateRangeText: '40~54%', speedRangeText: '3.2~4.0 km/h', }, green: { label: '脂肪燃烧', heartRateRangeText: '55~69%', speedRangeText: '4.1~5.5 km/h', }, yellow: { label: '糖分消耗', heartRateRangeText: '70~79%', speedRangeText: '5.6~7.1 km/h', }, orange: { label: '心肺训练', heartRateRangeText: '80~89%', speedRangeText: '7.2~8.8 km/h', }, red: { label: '峰值锻炼', heartRateRangeText: '>=90%', speedRangeText: '>=8.9 km/h', }, } export function clampTelemetryAge(age: number): number { if (!Number.isFinite(age)) { return 30 } return Math.max(10, Math.min(85, Math.round(age))) } export function estimateRestingHeartRateBpm(age: number): number { const safeAge = clampTelemetryAge(age) const estimated = 68 + (safeAge - 30) * 0.12 return Math.max(56, Math.min(76, Math.round(estimated))) } export function normalizeRestingHeartRateBpm(restingHeartRateBpm: number, age: number): number { if (!Number.isFinite(restingHeartRateBpm) || restingHeartRateBpm <= 0) { return estimateRestingHeartRateBpm(age) } return Math.max(40, Math.min(95, Math.round(restingHeartRateBpm))) } export function normalizeUserWeightKg(userWeightKg: number): number { if (!Number.isFinite(userWeightKg) || userWeightKg <= 0) { return 65 } return Math.max(35, Math.min(180, Math.round(userWeightKg))) } export const DEFAULT_TELEMETRY_CONFIG: TelemetryConfig = { heartRateAge: 30, restingHeartRateBpm: estimateRestingHeartRateBpm(30), userWeightKg: 65, } export function mergeTelemetryConfig(overrides?: Partial | null): TelemetryConfig { const heartRateAge = overrides && overrides.heartRateAge !== undefined ? clampTelemetryAge(overrides.heartRateAge) : DEFAULT_TELEMETRY_CONFIG.heartRateAge const restingHeartRateBpm = overrides && overrides.restingHeartRateBpm !== undefined ? normalizeRestingHeartRateBpm(overrides.restingHeartRateBpm, heartRateAge) : estimateRestingHeartRateBpm(heartRateAge) const userWeightKg = overrides && overrides.userWeightKg !== undefined ? normalizeUserWeightKg(overrides.userWeightKg) : DEFAULT_TELEMETRY_CONFIG.userWeightKg return { heartRateAge, restingHeartRateBpm, userWeightKg, } } export function getHeartRateToneSampleBpm(tone: HeartRateTone, config: TelemetryConfig): number { const maxHeartRate = Math.max(120, 220 - config.heartRateAge) const restingHeartRate = Math.min(maxHeartRate - 15, config.restingHeartRateBpm) const reserve = Math.max(20, maxHeartRate - restingHeartRate) if (tone === 'blue') { return Math.round(restingHeartRate + reserve * 0.3) } if (tone === 'purple') { return Math.round(restingHeartRate + reserve * 0.47) } if (tone === 'green') { return Math.round(restingHeartRate + reserve * 0.62) } if (tone === 'yellow') { return Math.round(restingHeartRate + reserve * 0.745) } if (tone === 'orange') { return Math.round(restingHeartRate + reserve * 0.845) } return Math.round(restingHeartRate + reserve * 0.93) } export function getHeartRateToneLabel(tone: HeartRateTone): string { return HEART_RATE_TONE_META[tone].label } export function getHeartRateToneRangeText(tone: HeartRateTone): string { return HEART_RATE_TONE_META[tone].heartRateRangeText } export function getSpeedToneRangeText(tone: HeartRateTone): string { return HEART_RATE_TONE_META[tone].speedRangeText }