Skip to content

Commit e6dd794

Browse files
committed
feat(heart_rate_display_ui): 新增 WebSocket 服务器功能
- 添加 WebSocket 服务器相关 UI 和配置 - 实现 WebSocket 服务器的启动、停止和状态显示 - 在心率更新时通过 WebSocket 广播数据 - 加载和保存 WebSocket 服务器的配置
1 parent cc4fc95 commit e6dd794

4 files changed

Lines changed: 189 additions & 11 deletions

File tree

‎config_webhook.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@
1919
"body": "{\n \"id\": \"heart-rate-widget\",\n \"show_name\": \"❤\",\n \"using\": false,\n \"app_name\": \"未在使用\"\n}",
2020
"headers": "{\n \"Content-Type\": \"application/json\",\n \"Sleepy-Secret\": \"改成你的秘钥\"\n}"
2121
}
22-
]
22+
]

‎heart_rate_display_ui.py‎

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from floating_window import FloatingWindow
1616
from vrc_osc import VrcOscClient
1717
from api_server import ApiServer
18+
from websocket_server import WebSocketServer # [新增] 导入WebSocket服务器
1819
from webhook_manager import WebhookManager
1920
from webhook_ui import WebhookWindow
2021

@@ -30,6 +31,7 @@ def __init__(self):
3031
self.should_stop = False
3132

3233
self.api_server = None
34+
self.websocket_server = None # [新增] WebSocket服务器实例
3335

3436
self.floating_window = FloatingWindow(self)
3537

@@ -56,6 +58,10 @@ def setup_ui(self):
5658
self.root.minsize(800, 500)
5759
self.root.resizable(True, True)
5860

61+
# [新增] WebSocket UI变量
62+
self.websocket_server_enabled = tk.BooleanVar(value=False)
63+
self.websocket_port_var = tk.StringVar(value="8001")
64+
5965
self.api_server_enabled = tk.BooleanVar(value=False)
6066
self.api_port_var = tk.StringVar(value="8000")
6167
self.vrc_ip_var = tk.StringVar(value="127.0.0.1")
@@ -120,6 +126,17 @@ def setup_ui(self):
120126
self.vrc_status_label = ttk.Label(vrc_frame, text="状态: 未连接", font=("Arial", 10), foreground="gray")
121127
self.vrc_status_label.grid(row=3, column=0, columnspan=2, sticky="w", pady=(5,0))
122128

129+
# [新增] WebSocket 服务器 UI
130+
websocket_frame = ttk.LabelFrame(middle_column_frame, text="WebSocket服务器 (实时推送)", padding="10")
131+
websocket_frame.pack(fill="x", pady=PAD_Y)
132+
websocket_frame.columnconfigure(1, weight=1)
133+
self.websocket_server_enabled.trace_add("write", self.toggle_websocket_server)
134+
ttk.Checkbutton(websocket_frame, text="启用WebSocket服务器", variable=self.websocket_server_enabled).grid(row=0, column=0, sticky=tk.W, columnspan=2)
135+
ttk.Label(websocket_frame, text="端口:").grid(row=1, column=0, sticky=tk.W, pady=(5,0))
136+
ttk.Entry(websocket_frame, textvariable=self.websocket_port_var, width=10).grid(row=1, column=1, sticky="ew", padx=5, pady=(5,0))
137+
self.websocket_status_label = ttk.Label(websocket_frame, text="状态: 已禁用", font=("Arial", 10), foreground="gray")
138+
self.websocket_status_label.grid(row=2, column=0, columnspan=2, sticky="w", pady=(5,0))
139+
123140
api_frame = ttk.LabelFrame(middle_column_frame, text="心率API服务器 (被动获取)", padding="10")
124141
api_frame.pack(fill="x", pady=PAD_Y)
125142
api_frame.columnconfigure(1, weight=1)
@@ -249,9 +266,12 @@ def update_heart_rate_display(self):
249266
self.heart_rate_label.config(text=f"心率: {heart_rate}")
250267

251268
if heart_rate > 0:
252-
# [修改] 使用新的事件触发方法
253269
self.webhook_manager.trigger_event("heart_rate_updated", heart_rate)
254270

271+
# [修改] 增加WebSocket广播
272+
if self.websocket_server:
273+
self.websocket_server.broadcast()
274+
255275
if heart_rate > 0:
256276
self.heart_rate_label.config(fg="green")
257277
if self.vrc_connected:
@@ -267,6 +287,29 @@ def update_heart_rate_display(self):
267287
def clear_logs(self):
268288
self.log_text.delete(1.0, tk.END)
269289

290+
# [新增] 启动/停止 WebSocket 服务器
291+
def toggle_websocket_server(self, *args):
292+
if self.websocket_server_enabled.get():
293+
try:
294+
port = int(self.websocket_port_var.get())
295+
self.websocket_server = WebSocketServer(self, port, self.log_message)
296+
self.websocket_server.start()
297+
self.websocket_status_label.config(text=f"状态: 运行于 ws://127.0.0.1:{port}", foreground="green")
298+
except ValueError:
299+
self.log_message("WebSocket服务器启动失败:端口号必须是有效的数字。")
300+
self.websocket_status_label.config(text="状态: 端口号无效", foreground="red")
301+
self.websocket_server_enabled.set(False)
302+
except Exception as e:
303+
self.log_message(f"WebSocket服务器启动失败: {e}")
304+
self.websocket_status_label.config(text=f"状态: 启动失败", foreground="red")
305+
self.websocket_server_enabled.set(False)
306+
307+
else:
308+
if self.websocket_server:
309+
self.websocket_server.stop()
310+
self.websocket_server = None
311+
self.websocket_status_label.config(text="状态: 已禁用", foreground="gray")
312+
270313
def toggle_api_server(self, *args):
271314
if self.api_server_enabled.get():
272315
try:
@@ -313,6 +356,11 @@ def save_settings(self):
313356
"api_server": {
314357
"enabled": self.api_server_enabled.get(),
315358
"port": self.api_port_var.get()
359+
},
360+
# [新增] 保存 WebSocket 设置
361+
"websocket_server": {
362+
"enabled": self.websocket_server_enabled.get(),
363+
"port": self.websocket_port_var.get()
316364
}
317365
}
318366
save_config(config)
@@ -367,10 +415,19 @@ def load_settings(self):
367415

368416
api_settings = config.get("api_server")
369417
if api_settings:
370-
self.api_port_var.set(api_settings.get("port", "8080"))
418+
self.api_port_var.set(api_settings.get("port", "8000"))
371419
if api_settings.get("enabled", False):
372420
self.root.after(100, lambda: self.api_server_enabled.set(True))
373421
self.log_message("已加载 API 服务器设置")
422+
423+
# [新增] 加载 WebSocket 设置
424+
websocket_settings = config.get("websocket_server")
425+
if websocket_settings:
426+
self.websocket_port_var.set(websocket_settings.get("port", "8001"))
427+
if websocket_settings.get("enabled", False):
428+
# 延迟执行,确保UI完全加载
429+
self.root.after(200, lambda: self.websocket_server_enabled.set(True))
430+
self.log_message("已加载 WebSocket 服务器设置")
374431

375432
def toggle_vrc_connection(self):
376433
if self.vrc_connected:
@@ -433,8 +490,11 @@ def on_closing(self):
433490
self.disconnect_device()
434491
if self.vrc_connected:
435492
self.vrc_osc_client.disconnect()
436-
if self.api_server and self.api_server.httpd:
493+
# [修改] 增加停止服务器的逻辑
494+
if self.api_server:
437495
self.api_server.stop()
496+
if self.websocket_server:
497+
self.websocket_server.stop()
438498
if self.floating_window.is_open():
439499
self.floating_window.close_window()
440500
self.root.destroy()
@@ -588,13 +648,14 @@ def _on_connect(self):
588648
self.connected = True
589649
self.status_label.config(text="状态: 已连接", fg="green")
590650
self.log_message("设备连接成功,开始监控心率")
591-
# [修改] 触发 "connected" 事件
592651
self.webhook_manager.trigger_event("connected", self.heart_rate)
652+
# [新增] 连接时广播状态
653+
if self.websocket_server:
654+
self.websocket_server.broadcast()
593655

594656

595657
def _on_disconnect(self):
596-
# [修改] 先触发事件,再更新状态
597-
if self.connected: # 只有在之前是连接状态时才触发
658+
if self.connected:
598659
self.webhook_manager.trigger_event("disconnected", self.heart_rate)
599660

600661
self.connected = False
@@ -605,13 +666,13 @@ def _on_disconnect(self):
605666
self.heart_rate_label.config(text="心率: --", fg="red")
606667
self.heart_rate_queue.put(0)
607668
self.log_message("设备已断开连接")
669+
# [新增] 断开时广播状态
670+
if self.websocket_server:
671+
self.websocket_server.broadcast()
608672

609673
def disconnect_device(self):
610674
self.should_stop = True
611675
if self.ble_loop and not self.ble_loop.is_closed() and self.ble_loop.is_running():
612-
# 停止事件循环可能会导致正在运行的异步任务突然中断,
613-
# 这里改为在循环内通过 should_stop 标志来优雅退出
614-
# self.ble_loop.call_soon_threadsafe(self.ble_loop.stop)
615676
pass
616677
self._on_disconnect()
617678
self.log_message("手动断开连接")

‎requirements.txt‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
bleak
22
python-osc
33
Flask
4-
pillow
4+
pillow
5+
websockets

‎websocket_server.py‎

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# websocket_server.py
2+
3+
import asyncio
4+
import json
5+
import threading
6+
from typing import Set, Optional, Callable, TYPE_CHECKING
7+
import websockets
8+
from websockets.server import ServerProtocol
9+
10+
if TYPE_CHECKING:
11+
from heart_rate_display_ui import HeartRateMonitor
12+
13+
14+
class WebSocketServer:
15+
"""
16+
运行在独立线程中的WebSocket服务器,用于实时推送心率数据。
17+
"""
18+
19+
def __init__(self, monitor_instance: 'HeartRateMonitor', port: int, logger_func: Callable[[str], None]):
20+
self.monitor_instance = monitor_instance
21+
self.port = port
22+
self.logger = logger_func
23+
self.server_thread: Optional[threading.Thread] = None
24+
self.loop: Optional[asyncio.AbstractEventLoop] = None
25+
self.server = None
26+
self.connected_clients: Set[ServerProtocol] = set()
27+
28+
# [修正] 从函数签名中移除未使用的 'path' 参数,以解决 TypeError
29+
async def _handler(self, websocket: ServerProtocol):
30+
"""处理新的客户端连接和消息"""
31+
self.connected_clients.add(websocket)
32+
self.logger(f"[WebSocket] 客户端连接: {websocket.remote_address}") # type: ignore
33+
try:
34+
# 发送当前状态
35+
await self.send_data(websocket)
36+
# 持续监听,直到客户端断开
37+
async for message in websocket: # type: ignore
38+
pass
39+
except websockets.exceptions.ConnectionClosed:
40+
self.logger(f"[WebSocket] 客户端断开连接: {websocket.remote_address}") # type: ignore
41+
finally:
42+
# 确保即使在发生异常时也能移除客户端
43+
if websocket in self.connected_clients:
44+
self.connected_clients.remove(websocket)
45+
46+
async def _run_server(self):
47+
"""启动WebSocket服务器的异步任务"""
48+
try:
49+
async with websockets.serve(self._handler, "0.0.0.0", self.port) as server: # type: ignore
50+
self.server = server
51+
self.logger(f"WebSocket 服务器已在 ws://0.0.0.0:{self.port} 启动")
52+
await server.wait_closed()
53+
except OSError as e:
54+
self.logger(f"[WebSocket] 服务器启动失败: {e}. 端口可能已被占用。")
55+
except Exception as e:
56+
self.logger(f"[WebSocket] 服务器发生未知错误: {e}")
57+
58+
def _start_server_thread(self):
59+
"""在新的事件循环和线程中运行服务器"""
60+
self.loop = asyncio.new_event_loop()
61+
asyncio.set_event_loop(self.loop)
62+
self.loop.run_until_complete(self._run_server())
63+
64+
def start(self):
65+
"""在独立线程中启动WebSocket服务器"""
66+
if self.server_thread and self.server_thread.is_alive():
67+
self.logger("[WebSocket] 服务器已在运行中。")
68+
return
69+
self.server_thread = threading.Thread(target=self._start_server_thread, daemon=True)
70+
self.server_thread.start()
71+
72+
def stop(self):
73+
"""停止WebSocket服务器"""
74+
if self.server and self.loop:
75+
self.logger("[WebSocket] 正在停止服务器...")
76+
# 优雅地关闭所有客户端连接
77+
for client in self.connected_clients:
78+
self.loop.call_soon_threadsafe(asyncio.create_task, client.close()) # type: ignore
79+
80+
# 停止服务器
81+
self.loop.call_soon_threadsafe(self.server.close)
82+
83+
# 等待服务器完全关闭
84+
if self.server_thread:
85+
self.server_thread.join(timeout=2)
86+
87+
if self.loop and self.loop.is_running():
88+
self.loop.call_soon_threadsafe(self.loop.stop)
89+
90+
self.logger("[WebSocket] 服务器已停止。")
91+
self.server = None
92+
self.server_thread = None
93+
self.loop = None
94+
95+
async def send_data(self, websocket: ServerProtocol):
96+
"""向单个客户端发送当前的心率数据"""
97+
data = {
98+
"heart_rate": self.monitor_instance.heart_rate,
99+
"connected": self.monitor_instance.connected,
100+
"status": "connected" if self.monitor_instance.connected else "disconnected"
101+
}
102+
try:
103+
await websocket.send(json.dumps(data)) # type: ignore
104+
except websockets.exceptions.ConnectionClosed:
105+
pass # 连接已关闭,无需处理
106+
107+
def broadcast(self):
108+
"""向所有连接的客户端广播心率数据"""
109+
if not self.connected_clients or not self.loop:
110+
return
111+
112+
# 使用 call_soon_threadsafe 从主线程安排协程在服务器的事件循环中执行
113+
for client in list(self.connected_clients):
114+
self.loop.call_soon_threadsafe(
115+
asyncio.create_task, self.send_data(client)
116+
)

0 commit comments

Comments
 (0)