telemetryConfig.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. export interface TelemetryConfig {
  2. heartRateAge: number
  3. restingHeartRateBpm: number
  4. userWeightKg: number
  5. }
  6. export type HeartRateTone = 'blue' | 'purple' | 'green' | 'yellow' | 'orange' | 'red'
  7. type HeartRateToneMeta = {
  8. label: string
  9. heartRateRangeText: string
  10. speedRangeText: string
  11. }
  12. const HEART_RATE_TONE_META: Record<HeartRateTone, HeartRateToneMeta> = {
  13. blue: {
  14. label: '激活放松',
  15. heartRateRangeText: '<=39%',
  16. speedRangeText: '<3.2 km/h',
  17. },
  18. purple: {
  19. label: '动态热身',
  20. heartRateRangeText: '40~54%',
  21. speedRangeText: '3.2~4.0 km/h',
  22. },
  23. green: {
  24. label: '脂肪燃烧',
  25. heartRateRangeText: '55~69%',
  26. speedRangeText: '4.1~5.5 km/h',
  27. },
  28. yellow: {
  29. label: '糖分消耗',
  30. heartRateRangeText: '70~79%',
  31. speedRangeText: '5.6~7.1 km/h',
  32. },
  33. orange: {
  34. label: '心肺训练',
  35. heartRateRangeText: '80~89%',
  36. speedRangeText: '7.2~8.8 km/h',
  37. },
  38. red: {
  39. label: '峰值锻炼',
  40. heartRateRangeText: '>=90%',
  41. speedRangeText: '>=8.9 km/h',
  42. },
  43. }
  44. export function clampTelemetryAge(age: number): number {
  45. if (!Number.isFinite(age)) {
  46. return 30
  47. }
  48. return Math.max(10, Math.min(85, Math.round(age)))
  49. }
  50. export function estimateRestingHeartRateBpm(age: number): number {
  51. const safeAge = clampTelemetryAge(age)
  52. const estimated = 68 + (safeAge - 30) * 0.12
  53. return Math.max(56, Math.min(76, Math.round(estimated)))
  54. }
  55. export function normalizeRestingHeartRateBpm(restingHeartRateBpm: number, age: number): number {
  56. if (!Number.isFinite(restingHeartRateBpm) || restingHeartRateBpm <= 0) {
  57. return estimateRestingHeartRateBpm(age)
  58. }
  59. return Math.max(40, Math.min(95, Math.round(restingHeartRateBpm)))
  60. }
  61. export function normalizeUserWeightKg(userWeightKg: number): number {
  62. if (!Number.isFinite(userWeightKg) || userWeightKg <= 0) {
  63. return 65
  64. }
  65. return Math.max(35, Math.min(180, Math.round(userWeightKg)))
  66. }
  67. export const DEFAULT_TELEMETRY_CONFIG: TelemetryConfig = {
  68. heartRateAge: 30,
  69. restingHeartRateBpm: estimateRestingHeartRateBpm(30),
  70. userWeightKg: 65,
  71. }
  72. export function mergeTelemetryConfig(overrides?: Partial<TelemetryConfig> | null): TelemetryConfig {
  73. const heartRateAge = overrides && overrides.heartRateAge !== undefined
  74. ? clampTelemetryAge(overrides.heartRateAge)
  75. : DEFAULT_TELEMETRY_CONFIG.heartRateAge
  76. const restingHeartRateBpm = overrides && overrides.restingHeartRateBpm !== undefined
  77. ? normalizeRestingHeartRateBpm(overrides.restingHeartRateBpm, heartRateAge)
  78. : estimateRestingHeartRateBpm(heartRateAge)
  79. const userWeightKg = overrides && overrides.userWeightKg !== undefined
  80. ? normalizeUserWeightKg(overrides.userWeightKg)
  81. : DEFAULT_TELEMETRY_CONFIG.userWeightKg
  82. return {
  83. heartRateAge,
  84. restingHeartRateBpm,
  85. userWeightKg,
  86. }
  87. }
  88. export function getHeartRateToneSampleBpm(tone: HeartRateTone, config: TelemetryConfig): number {
  89. const maxHeartRate = Math.max(120, 220 - config.heartRateAge)
  90. const restingHeartRate = Math.min(maxHeartRate - 15, config.restingHeartRateBpm)
  91. const reserve = Math.max(20, maxHeartRate - restingHeartRate)
  92. if (tone === 'blue') {
  93. return Math.round(restingHeartRate + reserve * 0.3)
  94. }
  95. if (tone === 'purple') {
  96. return Math.round(restingHeartRate + reserve * 0.47)
  97. }
  98. if (tone === 'green') {
  99. return Math.round(restingHeartRate + reserve * 0.62)
  100. }
  101. if (tone === 'yellow') {
  102. return Math.round(restingHeartRate + reserve * 0.745)
  103. }
  104. if (tone === 'orange') {
  105. return Math.round(restingHeartRate + reserve * 0.845)
  106. }
  107. return Math.round(restingHeartRate + reserve * 0.93)
  108. }
  109. export function getHeartRateToneLabel(tone: HeartRateTone): string {
  110. return HEART_RATE_TONE_META[tone].label
  111. }
  112. export function getHeartRateToneRangeText(tone: HeartRateTone): string {
  113. return HEART_RATE_TONE_META[tone].heartRateRangeText
  114. }
  115. export function getSpeedToneRangeText(tone: HeartRateTone): string {
  116. return HEART_RATE_TONE_META[tone].speedRangeText
  117. }