From f8256956583fba2c9a4340bff3932e609e82c0f1 Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Thu, 19 Feb 2026 10:15:51 +0100 Subject: [PATCH] Python calibration tool: simplify graphs, fix CancelledError - Remove orange "last shot peak" line (unreliable at 20Hz) - Remove rolling max curve (not useful in practice) - Add specific asyncio.CancelledError catch in ble_loop with retry message when device is already connected elsewhere - Clean up titles: show only current val + threshold Co-Authored-By: Claude Opus 4.6 --- .../Xiao_BLE_tools/xiao_calibration_tool.py | 62 ++++++------------- 1 file changed, 18 insertions(+), 44 deletions(-) diff --git a/Python/Xiao_BLE_tools/xiao_calibration_tool.py b/Python/Xiao_BLE_tools/xiao_calibration_tool.py index 8f9af5f..b0618dc 100644 --- a/Python/Xiao_BLE_tools/xiao_calibration_tool.py +++ b/Python/Xiao_BLE_tools/xiao_calibration_tool.py @@ -13,7 +13,7 @@ from collections import deque from bleak import BleakClient, BleakScanner DEVICE_NAME = "XIAO Airsoft Pro" -DEVICE_ADDRESS = "" # Laisser vide pour scan automatique par nom, ou mettre l'adresse MAC pour connexion directe +DEVICE_ADDRESS = "46:35:F1:51:51:5A" # Adresse MAC directe — plus fiable que le scan par nom DEBUG_CHAR_UUID = "6E400005-B5A3-F393-E0A9-E50E24DCCA9E" SHOT_CHAR_UUID = "6E400004-B5A3-F393-E0A9-E50E24DCCA9E" CONFIG_CHAR_UUID = "6E400006-B5A3-F393-E0A9-E50E24DCCA9E" @@ -36,7 +36,7 @@ shot_count = 0 shot_pending = False # Flag : un tir reçu, à enregistrer dans shot_buf au prochain debug tick # Pic max au moment du dernier tir (mis à jour au moment où shot_pending devient True) last_shot_peak = {"accel": 0.0, "gyro": 0.0, "audio": 0} -ble_status = "🔍 Connexion..." +ble_status = "[..] Connexion..." ble_running = True audio_max_global = 1000 # Tracks le max absolu jamais vu ble_client = None # Référence au client BLE actif @@ -131,10 +131,10 @@ async def ble_loop(): global ble_status, ble_running, ble_client, config_pending while ble_running: try: - ble_status = f"🔍 Recherche '{DEVICE_NAME}'..." + ble_status = f"[?] Recherche '{DEVICE_NAME}'..." device = await find_device() if not device: - ble_status = "⚠️ XIAO non trouvé — réessai..." + ble_status = "[!] XIAO non trouve — reessai..." await asyncio.sleep(5) continue # device peut être une string (adresse directe) ou un BLEDevice @@ -144,13 +144,13 @@ async def ble_loop(): else: addr = device.address display_name = device.name or DEVICE_NAME - ble_status = f"📡 Connexion {display_name}..." + ble_status = f"[..] Connexion {display_name}..." async with BleakClient(addr, timeout=15.0) as client: ble_client = client await client.start_notify(DEBUG_CHAR_UUID, debug_callback) await client.start_notify(SHOT_CHAR_UUID, shot_callback) - ble_status = f"✅ {display_name} ({addr})" + ble_status = f"[OK] {display_name} ({addr})" # À la connexion : lire la config sauvegardée dans le XIAO (flash) try: raw = await client.read_gatt_char(CONFIG_CHAR_UUID) @@ -182,10 +182,14 @@ async def ble_loop(): config_pending = False await asyncio.sleep(0.1) ble_client = None - ble_status = "❌ Déconnecté — reconnexion..." + ble_status = "[X] Deconnecte — reconnexion..." + except asyncio.CancelledError: + ble_client = None + ble_status = "[!] Connexion annulee (device deja connecte?) — reessai..." + await asyncio.sleep(3) except Exception as e: ble_client = None - ble_status = f"❌ {str(e)[:50]}" + ble_status = f"[X] {str(e)[:50]}" await asyncio.sleep(5) def run_ble(): @@ -241,16 +245,12 @@ thr_a = axes[0].axhline(thresholds["accel"], color=CT, ls='--', lw=1.5, label='S thr_g = axes[1].axhline(thresholds["gyro"], color=CT, ls='--', lw=1.5, label='Seuil (trigger XIAO)') thr_m = axes[2].axhline(thresholds["audio"], color=CT, ls='--', lw=1.5, label='Seuil (trigger XIAO)') -# Ligne horizontale orange = pic au moment du dernier tir (invisible au départ) -peak_a = axes[0].axhline(0, color=CS, ls='-', lw=1.5, alpha=0.0, label='Pic dernier tir') -peak_g = axes[1].axhline(0, color=CS, ls='-', lw=1.5, alpha=0.0, label='Pic dernier tir') -peak_m = axes[2].axhline(0, color=CS, ls='-', lw=1.5, alpha=0.0, label='Pic dernier tir') - # Légendes for ax in axes[:3]: + lines = ax.get_lines() ax.legend(loc='upper left', fontsize=7, facecolor=PANEL, edgecolor='#444466', labelcolor=TEXT, framealpha=0.8, - handles=[ax.get_lines()[0], ax.get_lines()[1], ax.get_lines()[2], + handles=[lines[0], plt.Rectangle((0,0),1,1, fc=CT, alpha=0.25, label='Trigger actif (XIAO)')]) from matplotlib.patches import Rectangle @@ -296,19 +296,6 @@ def update(frame): line_m.set_ydata(m) line_s.set_ydata(s) - # Ligne orange = pic au moment du dernier tir (visible seulement si un tir a eu lieu) - if shot_count > 0: - peak_a.set_ydata([last_shot_peak["accel"]] * 2) - peak_g.set_ydata([last_shot_peak["gyro"]] * 2) - peak_m.set_ydata([last_shot_peak["audio"]] * 2) - peak_a.set_alpha(0.9) - peak_g.set_alpha(0.9) - peak_m.set_alpha(0.9) - else: - peak_a.set_alpha(0.0) - peak_g.set_alpha(0.0) - peak_m.set_alpha(0.0) - # Mise à jour des seuils thr_a.set_ydata([thresholds["accel"], thresholds["accel"]]) thr_g.set_ydata([thresholds["gyro"], thresholds["gyro"]]) @@ -329,22 +316,10 @@ def update(frame): for i, span in enumerate(shot_spans): span.set_alpha(0.85 if i < len(sl) and sl[i] > 0 else 0) - # Titres avec valeurs courantes + pic dernier tir - pa = last_shot_peak["accel"] if shot_count > 0 else None - pg = last_shot_peak["gyro"] if shot_count > 0 else None - pm = last_shot_peak["audio"] if shot_count > 0 else None - titles[0].set_text( - f"Accelerometre val: {a[-1]:.2f} G seuil: {thresholds['accel']:.1f} G" - + (f" ── pic dernier tir: {pa:.2f} G ({pa/thresholds['accel']*100:.0f}%)" if pa else "") - + f" [NUM7=+0.1 NUM1=-0.1]") - titles[1].set_text( - f"Gyroscope val: {g[-1]:.0f} dps seuil: {thresholds['gyro']:.0f} dps" - + (f" ── pic dernier tir: {pg:.0f} dps ({pg/thresholds['gyro']*100:.0f}%)" if pg else "") - + f" [NUM8=+10 NUM2=-10]") - titles[2].set_text( - f"Microphone PDM val: {m[-1]:.0f} seuil: {thresholds['audio']}" - + (f" ── pic dernier tir: {pm:.0f} ({pm/thresholds['audio']*100:.0f}%)" if pm else "") - + f" [NUM9=+500 NUM3=-500]") + # Titres avec valeurs courantes + titles[0].set_text(f"Accelerometre val: {a[-1]:.2f} G seuil: {thresholds['accel']:.1f} G [NUM7=+0.1 NUM1=-0.1]") + titles[1].set_text(f"Gyroscope val: {g[-1]:.0f} dps seuil: {thresholds['gyro']:.0f} dps [NUM8=+10 NUM2=-10]") + titles[2].set_text(f"Microphone PDM val: {m[-1]:.0f} seuil: {thresholds['audio']} [NUM9=+500 NUM3=-500]") titles[3].set_text(f"Tirs detectes : {shot_count} (fenetre glissante)") status_txt.set_text( @@ -355,7 +330,6 @@ def update(frame): return (line_a, line_g, line_m, line_s, thr_a, thr_g, thr_m, - peak_a, peak_g, peak_m, *titles, status_txt, debug_txt) def on_key(event):