1
Showing
7 changed files
with
411 additions
and
1 deletions
.gitignore
0 → 100644
app.ico
0 → 100644
No preview for this file type
eee
deleted
100644 → 0
| 1 | r'r'rv | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
exe/we.exe
0 → 100644
This file is too large to display.
readme.md
0 → 100644
| 1 | ## | ||
| 2 | 重要文件就是这个了 | ||
| 3 | |||
| 4 | ## | ||
| 5 | tray_icon.py 是来进行显示系统托盘图标的 | ||
| 6 | |||
| 7 | ## | ||
| 8 | 主函数 we.py | ||
| 9 | 使用了 注释和引用图标逻辑 | ||
| 10 | # -*- coding: utf-8 -*-- | ||
| 11 | import asyncio | ||
| 12 | import websockets | ||
| 13 | from bleak import BleakScanner, BleakClient | ||
| 14 | import json | ||
| 15 | import base64 | ||
| 16 | import threading | ||
| 17 | from tray_icon import start_tray_icon | ||
| 18 | start_tray_icon("app.ico") | ||
| 19 | ## 打包需要python3 然后 | ||
| 20 | 使用 pip cache dir 观察python3 的路径 | ||
| 21 | |||
| 22 | 安装环境 | ||
| 23 | ## | ||
| 24 | pip install pyinstaller | ||
| 25 | pip install bleak | ||
| 26 | pip install pystray pillow | ||
| 27 | |||
| 28 | ## | ||
| 29 | 打包指令 产生一个带窗口的程序 打包完后可以随便改exe 名字 | ||
| 30 | C:\Users\Admin\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.13_qbz5n2kfra8p0\LocalCache\local-packages\Python313\Scripts\pyinstaller.exe --onefile --icon=data\app.ico we.py --add-data "data;data" | ||
| 31 | |||
| 32 | ## | ||
| 33 | 端口占用 | ||
| 34 | netstat -ano | findstr :201111 | ||
| 35 | taskkill /PID /F 1234 | ||
| 36 | |||
| 37 | ## | ||
| 38 | app.ico 需要有特定格式 不能随便图片 | ||
| 39 | |||
| 40 | |||
| 41 | |||
| 42 | ## | ||
| 43 | 运行时的路径 和打包后的路径 是不一致的 如何解决 见具体函数 | ||
| 44 | |||
| 45 | icon_path = resource_path(resource_path("data/app.ico")) |
tray_icon.py
0 → 100644
| 1 | import pystray | ||
| 2 | from PIL import Image, ImageDraw | ||
| 3 | import threading | ||
| 4 | |||
| 5 | def create_image(width, height, color1, color2): | ||
| 6 | image = Image.new("RGB", (width, height), color1) | ||
| 7 | dc = ImageDraw.Draw(image) | ||
| 8 | dc.rectangle((width // 2, 0, width, height // 2), fill=color2) | ||
| 9 | return image | ||
| 10 | |||
| 11 | def run_icon(icon_path): | ||
| 12 | image = Image.open(icon_path) # 加载自定义图标 | ||
| 13 | icon = pystray.Icon("test_icon", image, "My System Tray Icon") | ||
| 14 | icon.run() | ||
| 15 | |||
| 16 | def start_tray_icon(icon_path): | ||
| 17 | threading.Thread(target=run_icon, args=(icon_path,), daemon=True).start() | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
we.py
0 → 100644
| 1 | # -*- coding: utf-8 -*-- | ||
| 2 | import asyncio | ||
| 3 | import websockets | ||
| 4 | from bleak import BleakScanner, BleakClient | ||
| 5 | import json | ||
| 6 | import base64 | ||
| 7 | import threading | ||
| 8 | from tray_icon import start_tray_icon | ||
| 9 | import os | ||
| 10 | import sys | ||
| 11 | |||
| 12 | def resource_path(relative_path): | ||
| 13 | try: | ||
| 14 | base_path = sys._MEIPASS # 打包后运行时的临时路径 | ||
| 15 | except AttributeError: | ||
| 16 | base_path = os.path.dirname(os.path.abspath(__file__)) # 开发环境下的路径 | ||
| 17 | # 使用 os.path.join 拼接路径,并通过 os.path.abspath 转换为绝对路径 | ||
| 18 | return os.path.abspath(os.path.join(base_path, relative_path)) | ||
| 19 | |||
| 20 | def resource_path(relative_path): | ||
| 21 | """获取资源文件的绝对路径,适用于打包后的程序""" | ||
| 22 | try: | ||
| 23 | base_path = sys._MEIPASS # 打包后运行时的临时路径 | ||
| 24 | except AttributeError: | ||
| 25 | base_path = os.path.abspath(".") # 开发环境下的路径 | ||
| 26 | return os.path.join(base_path, relative_path) | ||
| 27 | |||
| 28 | # 获取图标路径 | ||
| 29 | icon_path = resource_path(resource_path("data/app.ico")) | ||
| 30 | |||
| 31 | # 调用系统托盘图标函数 | ||
| 32 | start_tray_icon(icon_path) | ||
| 33 | |||
| 34 | # 添加线程锁以确保日志写入的原子性 | ||
| 35 | write_lock = threading.Lock() | ||
| 36 | |||
| 37 | def log_message_sync(direction, message): | ||
| 38 | """同步日志记录函数""" | ||
| 39 | log_entry = f"{direction}: {message}\n" | ||
| 40 | print(log_entry, end='') # 控制台仍然输出 | ||
| 41 | with write_lock: | ||
| 42 | with open('a.log', 'a', encoding='utf-8') as f: | ||
| 43 | f.write(log_entry) | ||
| 44 | |||
| 45 | async def log_message(direction, message): | ||
| 46 | """异步封装日志记录""" | ||
| 47 | loop = asyncio.get_event_loop() | ||
| 48 | await loop.run_in_executor(None, log_message_sync, direction, message) | ||
| 49 | |||
| 50 | class BLEClient: | ||
| 51 | def __init__(self): | ||
| 52 | self.target_device = None | ||
| 53 | self.client = None | ||
| 54 | self.services = [] | ||
| 55 | self.optional_services = [] | ||
| 56 | self.websocket = None | ||
| 57 | |||
| 58 | def on_disconnect(self, client): | ||
| 59 | print("BLE连接断开,关闭WebSocket") | ||
| 60 | if self.websocket and not self.websocket.closed: | ||
| 61 | asyncio.create_task(self.close_websocket()) | ||
| 62 | |||
| 63 | async def close_websocket(self): | ||
| 64 | await self.websocket.close() | ||
| 65 | self.websocket = None | ||
| 66 | |||
| 67 | def detection_callback(self, device, advertisement_data): | ||
| 68 | if any(service_uuid in advertisement_data.service_uuids for service_uuid in self.services): | ||
| 69 | self.target_device = (device, advertisement_data) | ||
| 70 | if not self.target_device: | ||
| 71 | print("未找到匹配设备") | ||
| 72 | return | ||
| 73 | else: | ||
| 74 | device, adv_data = self.target_device | ||
| 75 | print("\n找到目标设备:") | ||
| 76 | print(f"设备名称: {device.name}") | ||
| 77 | print(f"设备地址: {device.address}") | ||
| 78 | print(f"信号强度: {device.rssi} dBm") | ||
| 79 | print("\n广播信息:") | ||
| 80 | print(f"服务UUID列表: {adv_data.service_uuids}") | ||
| 81 | print(f"制造商数据: {adv_data.manufacturer_data}") | ||
| 82 | print(f"服务数据: {adv_data.service_data}") | ||
| 83 | print(f"本地名称: {adv_data.local_name}") | ||
| 84 | return self.target_device | ||
| 85 | |||
| 86 | async def handle_client(self, websocket, path): | ||
| 87 | self.websocket = websocket | ||
| 88 | if path != "/scratch/ble": | ||
| 89 | await websocket.close(code=1003, reason="Path not allowed") | ||
| 90 | return | ||
| 91 | |||
| 92 | try: | ||
| 93 | async for message in websocket: | ||
| 94 | try: | ||
| 95 | # 记录接收到的消息 | ||
| 96 | await log_message("接收", message) | ||
| 97 | |||
| 98 | request = json.loads(message) | ||
| 99 | if request["jsonrpc"] != "2.0": | ||
| 100 | response = json.dumps({ | ||
| 101 | "jsonrpc": "2.0", | ||
| 102 | "result": {"message": "Invalid Request"}, | ||
| 103 | "id": request.get("id", None) | ||
| 104 | }) | ||
| 105 | await log_message("下发", response) | ||
| 106 | await websocket.send(response) | ||
| 107 | continue | ||
| 108 | |||
| 109 | method = request.get("method") | ||
| 110 | params = request.get("params", {}) | ||
| 111 | request_id = request.get("id") | ||
| 112 | |||
| 113 | if method == "discover": | ||
| 114 | self.services = params.get("filters", [{}])[0].get("services", []) | ||
| 115 | self.optional_services = params.get("optionalServices", []) | ||
| 116 | |||
| 117 | scanner = BleakScanner() | ||
| 118 | scanner.register_detection_callback(self.detection_callback) | ||
| 119 | |||
| 120 | # 添加重试机制 | ||
| 121 | max_retries = 3 | ||
| 122 | found = False | ||
| 123 | for attempt in range(max_retries): | ||
| 124 | self.target_device = None # 重置目标设备 | ||
| 125 | await scanner.start() | ||
| 126 | await asyncio.sleep(5) | ||
| 127 | await scanner.stop() | ||
| 128 | |||
| 129 | if self.target_device: | ||
| 130 | found = True | ||
| 131 | break | ||
| 132 | |||
| 133 | if attempt < max_retries - 1: # 最后一次不等待 | ||
| 134 | print(f"未找到设备,第{attempt+1}次重试...") | ||
| 135 | await asyncio.sleep(3) | ||
| 136 | |||
| 137 | if not found: | ||
| 138 | # response = json.dumps({ | ||
| 139 | # "jsonrpc": "2.0", | ||
| 140 | # "result": {"message": "Device not found"}, | ||
| 141 | # "id": request_id | ||
| 142 | # }) | ||
| 143 | # await log_message("下发", response) | ||
| 144 | # await websocket.send(response) | ||
| 145 | continue | ||
| 146 | |||
| 147 | device, adv_data = self.target_device | ||
| 148 | discover_response = json.dumps({ | ||
| 149 | "jsonrpc": "2.0", | ||
| 150 | "method": "didDiscoverPeripheral", | ||
| 151 | "params": { | ||
| 152 | "name": device.name, | ||
| 153 | "peripheralId": device.address, | ||
| 154 | "rssi": device.rssi | ||
| 155 | } | ||
| 156 | }) | ||
| 157 | await log_message("下发", discover_response) | ||
| 158 | await websocket.send(discover_response) | ||
| 159 | |||
| 160 | result_response = json.dumps({ | ||
| 161 | "jsonrpc": "2.0", | ||
| 162 | "result": None, | ||
| 163 | "id": request_id | ||
| 164 | }) | ||
| 165 | await log_message("下发", result_response) | ||
| 166 | await websocket.send(result_response) | ||
| 167 | |||
| 168 | elif method == "connect": | ||
| 169 | peripheral_id = params.get("peripheralId") | ||
| 170 | if not peripheral_id: | ||
| 171 | response = json.dumps({ | ||
| 172 | "jsonrpc": "2.0", | ||
| 173 | "result": {"message": "Invalid params"}, | ||
| 174 | "id": request_id | ||
| 175 | }) | ||
| 176 | await log_message("下发", response) | ||
| 177 | await websocket.send(response) | ||
| 178 | continue | ||
| 179 | |||
| 180 | self.client = BleakClient(peripheral_id) | ||
| 181 | self.client.set_disconnected_callback(self.on_disconnect) | ||
| 182 | await self.client.connect() | ||
| 183 | if self.client.is_connected: | ||
| 184 | print(f"已连接至设备: {peripheral_id}") | ||
| 185 | response = json.dumps({ | ||
| 186 | "jsonrpc": "2.0", | ||
| 187 | "result": None, | ||
| 188 | "id": request_id | ||
| 189 | }) | ||
| 190 | await log_message("下发", response) | ||
| 191 | await websocket.send(response) | ||
| 192 | else: | ||
| 193 | response = json.dumps({ | ||
| 194 | "jsonrpc": "2.0", | ||
| 195 | "result": {"message": "Failed to connect"}, | ||
| 196 | "id": request_id | ||
| 197 | }) | ||
| 198 | await log_message("下发", response) | ||
| 199 | await websocket.send(response) | ||
| 200 | |||
| 201 | elif method == "write": | ||
| 202 | service_id = params.get("serviceId") | ||
| 203 | characteristic_id = params.get("characteristicId") | ||
| 204 | message = params.get("message") | ||
| 205 | encoding = params.get("encoding", "utf-8") | ||
| 206 | |||
| 207 | if not all([service_id, characteristic_id, message]): | ||
| 208 | response = json.dumps({ | ||
| 209 | "jsonrpc": "2.0", | ||
| 210 | "result": {"message": "Invalid params"}, | ||
| 211 | "id": request_id | ||
| 212 | }) | ||
| 213 | await log_message("下发", response) | ||
| 214 | await websocket.send(response) | ||
| 215 | continue | ||
| 216 | |||
| 217 | if encoding == "base64": | ||
| 218 | message_bytes = base64.b64decode(message) | ||
| 219 | else: | ||
| 220 | message_bytes = message.encode(encoding) | ||
| 221 | |||
| 222 | await self.client.write_gatt_char(characteristic_id, message_bytes) | ||
| 223 | response = json.dumps({ | ||
| 224 | "jsonrpc": "2.0", | ||
| 225 | "result": None, | ||
| 226 | "id": request_id | ||
| 227 | }) | ||
| 228 | await log_message("下发", response) | ||
| 229 | await websocket.send(response) | ||
| 230 | |||
| 231 | elif method == "read": | ||
| 232 | service_id = params.get("serviceId") | ||
| 233 | characteristic_id = params.get("characteristicId") | ||
| 234 | |||
| 235 | if not all([service_id, characteristic_id]): | ||
| 236 | response = json.dumps({ | ||
| 237 | "jsonrpc": "2.0", | ||
| 238 | "result": {"message": "Invalid params"}, | ||
| 239 | "id": request_id | ||
| 240 | }) | ||
| 241 | await log_message("下发", response) | ||
| 242 | await websocket.send(response) | ||
| 243 | continue | ||
| 244 | |||
| 245 | data = await self.client.read_gatt_char(characteristic_id) | ||
| 246 | response = json.dumps({ | ||
| 247 | "jsonrpc": "2.0", | ||
| 248 | "result": { | ||
| 249 | "serviceId": service_id, | ||
| 250 | "characteristicId": characteristic_id, | ||
| 251 | "message": base64.b64encode(data).decode("utf-8") | ||
| 252 | }, | ||
| 253 | "id": request_id | ||
| 254 | }) | ||
| 255 | await log_message("下发", response) | ||
| 256 | await websocket.send(response) | ||
| 257 | |||
| 258 | elif method == "startNotifications": | ||
| 259 | service_id = params.get("serviceId") | ||
| 260 | characteristic_id = params.get("characteristicId") | ||
| 261 | |||
| 262 | if not all([service_id, characteristic_id]): | ||
| 263 | response = json.dumps({ | ||
| 264 | "jsonrpc": "2.0", | ||
| 265 | "result": {"message": "Invalid params"}, | ||
| 266 | "id": request_id | ||
| 267 | }) | ||
| 268 | await log_message("下发", response) | ||
| 269 | await websocket.send(response) | ||
| 270 | continue | ||
| 271 | |||
| 272 | await self.client.start_notify( | ||
| 273 | characteristic_id, | ||
| 274 | self.notification_handler(websocket, service_id, characteristic_id) | ||
| 275 | ) | ||
| 276 | response = json.dumps({ | ||
| 277 | "jsonrpc": "2.0", | ||
| 278 | "result": None, | ||
| 279 | "id": request_id | ||
| 280 | }) | ||
| 281 | await log_message("下发", response) | ||
| 282 | await websocket.send(response) | ||
| 283 | |||
| 284 | else: | ||
| 285 | response = json.dumps({ | ||
| 286 | "jsonrpc": "2.0", | ||
| 287 | "result": {"message": "Method not found"}, | ||
| 288 | "id": request_id | ||
| 289 | }) | ||
| 290 | await log_message("下发", response) | ||
| 291 | await websocket.send(response) | ||
| 292 | |||
| 293 | except json.JSONDecodeError: | ||
| 294 | response = json.dumps({ | ||
| 295 | "jsonrpc": "2.0", | ||
| 296 | "result": {"message": "Parse error"}, | ||
| 297 | "id": None | ||
| 298 | }) | ||
| 299 | await log_message("下发", response) | ||
| 300 | await websocket.send(response) | ||
| 301 | except Exception as e: | ||
| 302 | response = json.dumps({ | ||
| 303 | "jsonrpc": "2.0", | ||
| 304 | "result": {"message": str(e)}, | ||
| 305 | "id": request_id | ||
| 306 | }) | ||
| 307 | await log_message("下发", response) | ||
| 308 | await websocket.send(response) | ||
| 309 | |||
| 310 | except websockets.exceptions.ConnectionClosedOK: | ||
| 311 | print("WebSocket客户端正常断开") | ||
| 312 | except websockets.exceptions.ConnectionClosedError as e: | ||
| 313 | print(f"WebSocket客户端异常断开: {e.code} - {e.reason}") | ||
| 314 | finally: | ||
| 315 | if self.client and self.client.is_connected: | ||
| 316 | await self.client.disconnect() | ||
| 317 | print("BLE设备已主动断开") | ||
| 318 | self.client = None | ||
| 319 | self.target_device = None | ||
| 320 | |||
| 321 | def notification_handler(self, websocket, service_id, characteristic_id): | ||
| 322 | async def callback(sender, data): | ||
| 323 | response = json.dumps({ | ||
| 324 | "jsonrpc": "2.0", | ||
| 325 | "method": "characteristicDidChange", | ||
| 326 | "params": { | ||
| 327 | "serviceId": service_id, | ||
| 328 | "characteristicId": characteristic_id, | ||
| 329 | "message": base64.b64encode(data).decode("utf-8") | ||
| 330 | } | ||
| 331 | }) | ||
| 332 | await log_message("下发", response) | ||
| 333 | await websocket.send(response) | ||
| 334 | return callback | ||
| 335 | |||
| 336 | async def main(): | ||
| 337 | async with websockets.serve( | ||
| 338 | lambda websocket, path: BLEClient().handle_client(websocket, path), | ||
| 339 | "localhost", 20111 | ||
| 340 | ): | ||
| 341 | print("WebSocket服务已启动: ws://localhost:20111/scratch/ble") | ||
| 342 | print("日志文件路径: ./b.log") | ||
| 343 | await asyncio.Future() # 永久运行 | ||
| 344 | |||
| 345 | if __name__ == "__main__": | ||
| 346 | asyncio.run(main()) | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
-
Please register or sign in to post a comment