DataFlowing / collector /core /ble_engine.py
stouchinglhh
collector: ๅŒๆญฅ้‡‡้›†ๅทฅ็จ‹ v1.2.1๏ผˆๅ…จ้ƒจ LFS ๆจกๅผๆ–‡ไปถๆŒ‡้’ˆๅŒ–๏ผŒไธŽ Space ็Žฐๆœ‰ๆ–‡ไปถๅˆๅนถ๏ผ‰
d646dd7
Raw History Blame Contribute Delete
11.2 kB
"""
BLE ไผ ๆ„Ÿๅ™จๆ•ฐๆฎๅผ•ๆ“Ž โ€”โ€” ่“็‰™่ฟžๆŽฅใ€ๆ•ฐๆฎๆŽฅๆ”ถใ€่งฃๆžใ€้™ๅ™ชใ€‚
ไฝœไธบ QObject ่ฟ่กŒ๏ผŒๅœจ็‹ฌ็ซ‹็บฟ็จ‹ไธญๅค„็† BLE ๅผ‚ๆญฅ้€šไฟก๏ผŒ
้€š่ฟ‡ Qt ไฟกๅทๅฐ†ๅค„็†ๅŽ็š„ๆ•ฐๆฎๅธงๅ‘้€ๅˆฐ UI ็บฟ็จ‹ใ€‚
"""
import asyncio
import sys
import time
import threading
import logging
import numpy as np
from PyQt5.QtCore import QObject, pyqtSignal, QMutex, QMutexLocker
if sys.platform == "win32":
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
from bleak import BleakClient, BleakScanner
# โ”€โ”€ ้…็ฝฎๅธธ้‡ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
TX_CHAR_UUID = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"
MATRIX_ROWS = 16
MATRIX_COLS = 16
CALIBRATION_FRAMES = 100
TARGET_FPS = 100
logger = logging.getLogger("SensorBLE")
class SensorBLEEngine(QObject):
"""
BLE ไผ ๆ„Ÿๅ™จๆ•ฐๆฎ้‡‡้›†ๅผ•ๆ“Žใ€‚
ไฟกๅท
----
device_found(str, str, int) โ€” ่ฎพๅค‡ๅ, ๅœฐๅ€, RSSI
scan_complete(list) โ€” ๆ‰ซๆๅฎŒๆˆ๏ผŒ่ฎพๅค‡ๅˆ—่กจ
connected(str) โ€” ๅทฒ่ฟžๆŽฅ๏ผŒ่ฎพๅค‡ๅœฐๅ€
disconnected() โ€” ๅทฒๆ–ญๅผ€
data_ready(ndarray) โ€” ๅค„็†ๅŽ็š„ 16ร—16 ๆ•ฐๆฎๅธง
fps_updated(float) โ€” ็กฌไปถ FPS
error_occurred(str) โ€” ้”™่ฏฏๆถˆๆฏ
calibration_progress(int) โ€” ๆ กๅ‡†่ฟ›ๅบฆ (0-100)
"""
device_found = pyqtSignal(str, str, int)
scan_complete = pyqtSignal(list)
connected = pyqtSignal(str)
disconnected = pyqtSignal()
data_ready = pyqtSignal(np.ndarray)
fps_updated = pyqtSignal(float)
error_occurred = pyqtSignal(str)
calibration_progress = pyqtSignal(int)
def __init__(self):
super().__init__()
self.data_array = np.zeros((MATRIX_ROWS, MATRIX_COLS), dtype=np.float32)
self.data_lock = threading.Lock()
self.raw_buffer = bytearray()
self.buffer_lock = threading.Lock()
self._running = False
self._scanning = False
self._mutex = QMutex()
# ้™ๅ™ชๅ‚ๆ•ฐ
self.base_noise_gate = 500
self.dynamic_noise_ratio = 0.0
self.temporal_smooth = 0.15
self.spatial_filter_enabled = True
self.history_buffer = None
self.drift_baseline_val = 0
# ๆ กๅ‡†
self.is_calibrating = True
self.calibration_buffer = []
self.baseline_map = np.zeros((MATRIX_ROWS, MATRIX_COLS), dtype=np.float32)
# FPS
self.frame_count = 0
self.last_fps_time = time.time()
self.hardware_fps = 0.0
# ๆœ€ๆ–ฐไผ ๆ„Ÿๅ™จๆ•ฐๆฎ็š„้‡‡้›†ๆ—ถ้—ดๆˆณ๏ผˆๅพฎ็ง’๏ผ‰๏ผŒ็”ฑ่งฃๆž็บฟ็จ‹ๅ†™ๅ…ฅ๏ผŒไธป็บฟ็จ‹่ฏปๅ–
self.latest_data_ts_us: int = 0
self._ts_lock = threading.Lock()
# โ”€โ”€ ๅ…ฌๅผ€ๆŽฅๅฃ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def start_scan(self):
"""ๅผ€ๅง‹ๆ‰ซๆ BLE ่ฎพๅค‡ใ€‚"""
with QMutexLocker(self._mutex):
if self._scanning:
return
self._scanning = True
threading.Thread(target=self._scan_loop, daemon=True).start()
def connect_device(self, address: str):
"""่ฟžๆŽฅๅˆฐๆŒ‡ๅฎšๅœฐๅ€็š„ BLE ่ฎพๅค‡ใ€‚"""
with QMutexLocker(self._mutex):
self._running = True
self._target_mac = address
threading.Thread(target=self._ble_loop, daemon=True).start()
threading.Thread(target=self._parse_loop, daemon=True).start()
def disconnect(self):
"""ๆ–ญๅผ€ BLE ่ฟžๆŽฅใ€‚"""
with QMutexLocker(self._mutex):
self._running = False
def start_calibration(self):
"""ๅผ€ๅง‹ๆ กๅ‡†ใ€‚"""
with self.data_lock:
self.is_calibrating = True
self.calibration_buffer = []
def set_noise_gate(self, value: int):
self.base_noise_gate = value
def set_dynamic_ratio(self, value: float):
self.dynamic_noise_ratio = value
def set_spatial_filter(self, enabled: bool):
self.spatial_filter_enabled = enabled
def set_temporal_smooth(self, value: float):
self.temporal_smooth = value
# โ”€โ”€ BLE ๆ‰ซๆ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def _scan_loop(self):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
devices = loop.run_until_complete(BleakScanner.discover(timeout=5.0))
devices.sort(
key=lambda d: ("Matrix" in (d.name or ""), bool((d.name or "").strip()),
getattr(d.details, "rssi", -999)),
reverse=True,
)
self.scan_complete.emit(devices)
except Exception as e:
self.error_occurred.emit(f"BLE ๆ‰ซๆๅคฑ่ดฅ: {e}")
finally:
loop.close()
with QMutexLocker(self._mutex):
self._scanning = False
# โ”€โ”€ BLE ้€šไฟก โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def _ble_loop(self):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(self._ble_main_async())
async def _ble_main_async(self):
while True:
with QMutexLocker(self._mutex):
if not self._running:
break
try:
async with BleakClient(self._target_mac) as client:
self.connected.emit(self._target_mac)
await client.start_notify(TX_CHAR_UUID, self._ble_notify_callback)
while client.is_connected:
with QMutexLocker(self._mutex):
if not self._running:
break
await asyncio.sleep(1)
except Exception as e:
# ่ฟžๆŽฅ/้€š็Ÿฅๅคฑ่ดฅๅŽŸๅ› ไธŠๆŠ› UI๏ผˆๅฆๅˆ™็•Œ้ขๅœๅœจ"่ฟžๆŽฅไธญ"ๆ— ไปŽๆŽ’ๆŸฅ๏ผ‰
self.error_occurred.emit(f"่ฟžๆŽฅๅคฑ่ดฅ: {e}")
self.disconnected.emit()
with QMutexLocker(self._mutex):
if not self._running:
break
await asyncio.sleep(3)
def _ble_notify_callback(self, sender, data):
with self.buffer_lock:
self.raw_buffer.extend(data)
# โ”€โ”€ ๆ•ฐๆฎ่งฃๆž โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def _parse_loop(self):
frame_header = b"\xAA\x55"
frame_tail = b"\xFB\x03"
data_size = 512
frame_size = len(frame_header) + data_size + 1 + len(frame_tail)
while True:
with QMutexLocker(self._mutex):
if not self._running:
break
with self.buffer_lock:
buffer_len = len(self.raw_buffer)
if buffer_len < frame_size:
time.sleep(0.001)
continue
with self.buffer_lock:
h_idx = self.raw_buffer.find(frame_header)
if h_idx == -1:
del self.raw_buffer[:-1]
continue
if h_idx > 0:
del self.raw_buffer[:h_idx]
if len(self.raw_buffer) < frame_size:
continue
frame = self.raw_buffer[:frame_size]
if frame[-2:] == frame_tail:
d_bytes = frame[2 : 2 + data_size]
chksum = frame[2 + data_size]
calc_chk = 0
for b in d_bytes:
calc_chk ^= b
if calc_chk == chksum:
new_data = (
np.frombuffer(d_bytes, dtype=np.uint16)
.reshape((16, 16))
.astype(np.float32)
)
with self.data_lock:
self.data_array[:] = new_data
# ่ฎฐๅฝ•ๆ•ฐๆฎ้‡‡้›†ๆ—ถ้—ดๆˆณ๏ผˆๅพฎ็ง’๏ผ‰
with self._ts_lock:
self.latest_data_ts_us = int(time.time() * 1_000_000)
self.frame_count += 1
del self.raw_buffer[:frame_size]
else:
del self.raw_buffer[:2]
curr_time = time.time()
if curr_time - self.last_fps_time >= 1.0:
self.hardware_fps = self.frame_count / (
curr_time - self.last_fps_time
)
self.fps_updated.emit(self.hardware_fps)
self.frame_count = 0
self.last_fps_time = curr_time
# โ”€โ”€ ๆ•ฐๆฎๅค„็† โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def process_frame(self) -> tuple:
"""
ๅค„็†ไธ€ๅธงๆ•ฐๆฎ๏ผšๆ กๅ‡†ใ€้™ๅ™ชใ€ๆปคๆณขใ€‚
่ฟ”ๅ›ž (processed_data, max_signal)ใ€‚
"""
with self.data_lock:
raw_data = self.data_array.copy()
# ๆ กๅ‡†ไธญ
if self.is_calibrating:
self.calibration_buffer.append(raw_data)
progress = min(100, len(self.calibration_buffer) * 100 // CALIBRATION_FRAMES)
self.calibration_progress.emit(progress)
if len(self.calibration_buffer) >= CALIBRATION_FRAMES:
self.baseline_map = np.max(np.stack(self.calibration_buffer), axis=0) + 10
self.is_calibrating = False
return None, 0
# ๆ—ถๅŸŸๅนณๆป‘
if self.history_buffer is None:
self.history_buffer = raw_data.copy()
self.history_buffer = (
self.history_buffer * self.temporal_smooth
+ raw_data * (1.0 - self.temporal_smooth)
)
smoothed = self.history_buffer.copy()
# ๅŽปๅŸบ็บฟ
processed = np.maximum(0, smoothed - self.baseline_map)
# ๆผ‚็งป่กฅๅฟ
self.drift_baseline_val = np.percentile(processed, 40)
if self.drift_baseline_val > 100:
processed = np.maximum(0, processed - self.drift_baseline_val * 0.8)
# ๅŠจๆ€ๅ™ชๅฃฐ้—จ
current_max = processed.max()
dynamic_gate = min(current_max * self.dynamic_noise_ratio, 250)
final_gate = max(self.base_noise_gate, dynamic_gate)
processed[processed < final_gate] = 0
# ็ฉบ้—ดๆปคๆณข
if self.spatial_filter_enabled:
import cv2
mask = (processed > 0).astype(np.uint8)
kernel = np.array([[1, 1, 1], [1, 0, 1], [1, 1, 1]], dtype=np.uint8)
neighbors = cv2.filter2D(mask, -1, kernel, borderType=cv2.BORDER_CONSTANT)
noise_floor = np.maximum(self.baseline_map * 2.5, 180)
isolated_noise = (
(mask > 0) & (neighbors == 0) & (processed < noise_floor)
)
processed[isolated_noise] = 0
max_signal = processed.max()
return processed, max_signal