heartRateInputController.ts 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. import { HeartRateController, type HeartRateControllerCallbacks, type HeartRateDiscoveredDevice } from './heartRateController'
  2. import { DEFAULT_MOCK_HEART_RATE_BRIDGE_URL, MockHeartRateBridge } from './mockHeartRateBridge'
  3. export type HeartRateSourceMode = 'real' | 'mock'
  4. export interface HeartRateInputControllerCallbacks {
  5. onHeartRate: (bpm: number) => void
  6. onStatus: (message: string) => void
  7. onError: (message: string) => void
  8. onConnectionChange: (connected: boolean, deviceName: string | null) => void
  9. onDeviceListChange: (devices: HeartRateDiscoveredDevice[]) => void
  10. onDebugStateChange?: () => void
  11. }
  12. export interface HeartRateInputControllerDebugState {
  13. sourceMode: HeartRateSourceMode
  14. sourceModeText: string
  15. mockBridgeConnected: boolean
  16. mockBridgeStatusText: string
  17. mockBridgeUrlText: string
  18. mockHeartRateText: string
  19. }
  20. function formatSourceModeText(mode: HeartRateSourceMode): string {
  21. return mode === 'mock' ? '模拟心率' : '真实心率'
  22. }
  23. function formatMockHeartRateText(bpm: number | null): string {
  24. return bpm === null ? '--' : `${bpm} bpm`
  25. }
  26. function normalizeMockBridgeUrl(rawUrl: string): string {
  27. const trimmed = rawUrl.trim()
  28. if (!trimmed) {
  29. return DEFAULT_MOCK_HEART_RATE_BRIDGE_URL
  30. }
  31. let normalized = trimmed
  32. if (!/^wss?:\/\//i.test(normalized)) {
  33. normalized = `ws://${normalized.replace(/^\/+/, '')}`
  34. }
  35. if (!/\/mock-gps(?:\?.*)?$/i.test(normalized)) {
  36. normalized = normalized.replace(/\/+$/, '')
  37. normalized = `${normalized}/mock-gps`
  38. }
  39. return normalized
  40. }
  41. export class HeartRateInputController {
  42. callbacks: HeartRateInputControllerCallbacks
  43. realController: HeartRateController
  44. mockBridge: MockHeartRateBridge
  45. sourceMode: HeartRateSourceMode
  46. mockBridgeStatusText: string
  47. mockBridgeUrl: string
  48. mockBpm: number | null
  49. constructor(callbacks: HeartRateInputControllerCallbacks) {
  50. this.callbacks = callbacks
  51. this.sourceMode = 'real'
  52. this.mockBridgeUrl = DEFAULT_MOCK_HEART_RATE_BRIDGE_URL
  53. this.mockBridgeStatusText = `未连接 (${this.mockBridgeUrl})`
  54. this.mockBpm = null
  55. const realCallbacks: HeartRateControllerCallbacks = {
  56. onHeartRate: (bpm) => {
  57. if (this.sourceMode !== 'real') {
  58. return
  59. }
  60. this.callbacks.onHeartRate(bpm)
  61. this.emitDebugState()
  62. },
  63. onStatus: (message) => {
  64. if (this.sourceMode !== 'real') {
  65. return
  66. }
  67. this.callbacks.onStatus(message)
  68. this.emitDebugState()
  69. },
  70. onError: (message) => {
  71. if (this.sourceMode !== 'real') {
  72. return
  73. }
  74. this.callbacks.onError(message)
  75. this.emitDebugState()
  76. },
  77. onConnectionChange: (connected, deviceName) => {
  78. if (this.sourceMode !== 'real') {
  79. return
  80. }
  81. this.callbacks.onConnectionChange(connected, deviceName)
  82. this.emitDebugState()
  83. },
  84. onDeviceListChange: (devices) => {
  85. this.callbacks.onDeviceListChange(devices)
  86. this.emitDebugState()
  87. },
  88. }
  89. this.realController = new HeartRateController(realCallbacks)
  90. this.mockBridge = new MockHeartRateBridge({
  91. onOpen: () => {
  92. this.mockBridgeStatusText = `已连接 (${this.mockBridge.url})`
  93. if (this.sourceMode === 'mock') {
  94. this.callbacks.onConnectionChange(true, '模拟心率源')
  95. this.callbacks.onStatus('模拟心率源已连接,等待外部输入')
  96. }
  97. this.emitDebugState()
  98. },
  99. onClose: () => {
  100. this.mockBridgeStatusText = `未连接 (${this.mockBridge.url})`
  101. if (this.sourceMode === 'mock') {
  102. this.callbacks.onConnectionChange(false, null)
  103. this.callbacks.onStatus('模拟心率源已断开')
  104. }
  105. this.emitDebugState()
  106. },
  107. onError: (message) => {
  108. this.mockBridgeStatusText = `连接失败 (${this.mockBridge.url})`
  109. if (this.sourceMode === 'mock') {
  110. this.callbacks.onConnectionChange(false, null)
  111. this.callbacks.onError(`模拟心率源错误: ${message}`)
  112. }
  113. this.emitDebugState()
  114. },
  115. onBpm: (bpm) => {
  116. this.mockBpm = bpm
  117. if (this.sourceMode === 'mock') {
  118. this.callbacks.onHeartRate(bpm)
  119. }
  120. this.emitDebugState()
  121. },
  122. })
  123. }
  124. get currentDeviceId(): string | null {
  125. if (this.sourceMode === 'mock') {
  126. return this.mockBridge.connected ? 'mock-heart-rate' : null
  127. }
  128. return this.realController.currentDeviceId
  129. }
  130. get currentDeviceName(): string | null {
  131. if (this.sourceMode === 'mock') {
  132. return this.mockBridge.connected ? '模拟心率源' : null
  133. }
  134. return this.realController.currentDeviceName
  135. }
  136. get connected(): boolean {
  137. return this.sourceMode === 'mock' ? this.mockBridge.connected : this.realController.connected
  138. }
  139. get connecting(): boolean {
  140. return this.sourceMode === 'mock' ? this.mockBridge.connecting : this.realController.connecting
  141. }
  142. get scanning(): boolean {
  143. return this.sourceMode === 'mock' ? false : this.realController.scanning
  144. }
  145. get reconnecting(): boolean {
  146. return this.sourceMode === 'mock' ? false : this.realController.reconnecting
  147. }
  148. get disconnecting(): boolean {
  149. return this.sourceMode === 'mock' ? false : this.realController.disconnecting
  150. }
  151. get discoveredDevices(): HeartRateDiscoveredDevice[] {
  152. return this.realController.discoveredDevices
  153. }
  154. get lastDeviceId(): string | null {
  155. return this.realController.lastDeviceId
  156. }
  157. get lastDeviceName(): string | null {
  158. return this.realController.lastDeviceName
  159. }
  160. getDebugState(): HeartRateInputControllerDebugState {
  161. return {
  162. sourceMode: this.sourceMode,
  163. sourceModeText: formatSourceModeText(this.sourceMode),
  164. mockBridgeConnected: this.mockBridge.connected,
  165. mockBridgeStatusText: this.mockBridgeStatusText,
  166. mockBridgeUrlText: this.mockBridgeUrl,
  167. mockHeartRateText: formatMockHeartRateText(this.mockBpm),
  168. }
  169. }
  170. startScanAndConnect(): void {
  171. if (this.sourceMode === 'mock') {
  172. this.callbacks.onStatus(this.mockBridge.connected ? '模拟心率源已连接' : '当前为模拟心率模式,请连接模拟源')
  173. this.emitDebugState()
  174. return
  175. }
  176. this.realController.startScanAndConnect()
  177. }
  178. disconnect(): void {
  179. if (this.sourceMode === 'mock') {
  180. if (!this.mockBridge.connected && !this.mockBridge.connecting) {
  181. this.callbacks.onStatus('模拟心率源未连接')
  182. this.emitDebugState()
  183. return
  184. }
  185. this.mockBridge.disconnect()
  186. this.emitDebugState()
  187. return
  188. }
  189. this.realController.disconnect()
  190. }
  191. destroy(): void {
  192. this.realController.destroy()
  193. this.mockBridge.destroy()
  194. }
  195. setSourceMode(mode: HeartRateSourceMode): void {
  196. if (this.sourceMode === mode) {
  197. this.callbacks.onStatus(`${formatSourceModeText(mode)}已启用`)
  198. this.emitDebugState()
  199. return
  200. }
  201. const previousMode = this.sourceMode
  202. this.sourceMode = mode
  203. if (previousMode === 'real') {
  204. this.realController.disconnect()
  205. } else {
  206. this.callbacks.onConnectionChange(false, null)
  207. }
  208. const activeDeviceName = this.currentDeviceName
  209. this.callbacks.onConnectionChange(this.connected, activeDeviceName)
  210. this.callbacks.onStatus(mode === 'mock' ? '已切换到模拟心率模式' : '已切换到真实心率模式')
  211. this.emitDebugState()
  212. }
  213. setMockBridgeUrl(url: string): void {
  214. this.mockBridgeUrl = normalizeMockBridgeUrl(url)
  215. if (this.mockBridge.connected || this.mockBridge.connecting) {
  216. this.mockBridgeStatusText = `已设置新地址,重连生效 (${this.mockBridgeUrl})`
  217. if (this.sourceMode === 'mock') {
  218. this.callbacks.onStatus('模拟心率源地址已更新,重连后生效')
  219. }
  220. } else {
  221. this.mockBridgeStatusText = `未连接 (${this.mockBridgeUrl})`
  222. if (this.sourceMode === 'mock') {
  223. this.callbacks.onStatus('模拟心率源地址已更新')
  224. }
  225. }
  226. this.emitDebugState()
  227. }
  228. connectMockBridge(url = DEFAULT_MOCK_HEART_RATE_BRIDGE_URL): void {
  229. if (this.mockBridge.connected || this.mockBridge.connecting) {
  230. if (this.sourceMode === 'mock') {
  231. this.callbacks.onStatus('模拟心率源已连接')
  232. }
  233. this.emitDebugState()
  234. return
  235. }
  236. const targetUrl = normalizeMockBridgeUrl(url === DEFAULT_MOCK_HEART_RATE_BRIDGE_URL ? this.mockBridgeUrl : url)
  237. this.mockBridgeUrl = targetUrl
  238. this.mockBridgeStatusText = `连接中 (${targetUrl})`
  239. if (this.sourceMode === 'mock') {
  240. this.callbacks.onStatus('模拟心率源连接中')
  241. }
  242. this.emitDebugState()
  243. this.mockBridge.connect(targetUrl)
  244. }
  245. disconnectMockBridge(): void {
  246. if (!this.mockBridge.connected && !this.mockBridge.connecting) {
  247. if (this.sourceMode === 'mock') {
  248. this.callbacks.onStatus('模拟心率源未连接')
  249. }
  250. this.emitDebugState()
  251. return
  252. }
  253. this.mockBridge.disconnect()
  254. this.mockBridgeStatusText = `未连接 (${this.mockBridge.url})`
  255. this.emitDebugState()
  256. }
  257. connectToDiscoveredDevice(deviceId: string): void {
  258. if (this.sourceMode !== 'real') {
  259. this.callbacks.onStatus('当前为模拟心率模式,无法连接真实心率带')
  260. this.emitDebugState()
  261. return
  262. }
  263. this.realController.connectToDiscoveredDevice(deviceId)
  264. }
  265. clearPreferredDevice(): void {
  266. this.realController.clearPreferredDevice()
  267. this.emitDebugState()
  268. }
  269. emitDebugState(): void {
  270. if (this.callbacks.onDebugStateChange) {
  271. this.callbacks.onDebugStateChange()
  272. }
  273. }
  274. }