heartRateInputController.ts 10.0 KB

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