From 6048ed9066c1527f0b39046669154720f1a965ff Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Thu, 19 Feb 2026 09:39:18 +0100 Subject: [PATCH] =?UTF-8?q?Python:=20calibration=20tool=20=E2=80=94=20ajou?= =?UTF-8?q?t=20courbe=20moyenne=20glissante=20+=20l=C3=A9gende?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Affiche la moyenne glissante (même fenêtre 60ms que le XIAO) en plus de la courbe brute sur chaque graphe, avec légende. Permet de comprendre visuellement pourquoi un tir est déclenché même si le pic brut semble ne pas atteindre le seuil. Co-Authored-By: Claude Opus 4.6 --- .../Xiao_BLE_tools/xiao_calibration_tool.py | 51 ++++++++++++++----- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/Python/Xiao_BLE_tools/xiao_calibration_tool.py b/Python/Xiao_BLE_tools/xiao_calibration_tool.py index 015a787..177bc48 100644 --- a/Python/Xiao_BLE_tools/xiao_calibration_tool.py +++ b/Python/Xiao_BLE_tools/xiao_calibration_tool.py @@ -44,6 +44,9 @@ debug_mode = 3 # 0=OFF, 1=RAW, 2=TRIGGERS, 3=FULL (actif par défaut) import numpy as np X = np.arange(WINDOW_SIZE) # axe X fixe, ne change jamais +# Fenêtre de moyenne glissante en samples (60ms window / 50ms debug rate = ~1.2 → 3 samples min) +AVG_WINDOW = 3 # correspond à la accelWindow/gyroWindow/audioWindow envoyée au XIAO (60ms @ 20Hz) + # ─── BLE ──────────────────────────────────────────────── def debug_callback(sender, data): global audio_max_global, shot_pending @@ -192,9 +195,12 @@ from matplotlib.animation import FuncAnimation BG = '#1a1a2e' PANEL = '#16213e' TEXT = '#e0e0e0' -CA = '#4fc3f7' # accel -CG = '#81c784' # gyro -CM = '#ce93d8' # audio +CA = '#4fc3f7' # accel brut +CG = '#81c784' # gyro brut +CM = '#ce93d8' # audio brut +CA2 = '#0288d1' # accel moyenne (plus foncé) +CG2 = '#388e3c' # gyro moyenne +CM2 = '#7b1fa2' # audio moyenne CT = '#ef5350' # trigger / seuil CS = '#ff9800' # shot (orange vif) GRID = '#2a2a4a' @@ -222,15 +228,23 @@ axes[3].set_ylim(-0.1, 1.5) axes[3].set_yticks([]) # pas de graduations Y sur la timeline # Créer les artistes UNE SEULE FOIS — on ne les recrée jamais -line_a, = axes[0].plot(X, list(accel_buf), color=CA, lw=1.5) -line_g, = axes[1].plot(X, list(gyro_buf), color=CG, lw=1.5) -line_m, = axes[2].plot(X, list(audio_buf), color=CM, lw=1.5) -line_s, = axes[3].plot(X, list(shot_buf), color=CS, lw=0, marker='|', +line_a, = axes[0].plot(X, list(accel_buf), color=CA, lw=1.0, alpha=0.5, label='Brut') +line_g, = axes[1].plot(X, list(gyro_buf), color=CG, lw=1.0, alpha=0.5, label='Brut') +line_m, = axes[2].plot(X, list(audio_buf), color=CM, lw=1.0, alpha=0.5, label='Brut') +line_a2, = axes[0].plot(X, list(accel_buf), color=CA2, lw=2.0, label=f'Moyenne ({AVG_WINDOW} pts / 60ms)') +line_g2, = axes[1].plot(X, list(gyro_buf), color=CG2, lw=2.0, label=f'Moyenne ({AVG_WINDOW} pts / 60ms)') +line_m2, = axes[2].plot(X, list(audio_buf), color=CM2, lw=2.0, label=f'Moyenne ({AVG_WINDOW} pts / 60ms)') +line_s, = axes[3].plot(X, list(shot_buf), color=CS, lw=0, marker='|', markersize=20, markeredgewidth=2.5) # barres verticales -thr_a = axes[0].axhline(thresholds["accel"], color=CT, ls='--', lw=1.5) -thr_g = axes[1].axhline(thresholds["gyro"], color=CT, ls='--', lw=1.5) -thr_m = axes[2].axhline(thresholds["audio"], color=CT, ls='--', lw=1.5) +thr_a = axes[0].axhline(thresholds["accel"], color=CT, ls='--', lw=1.5, label='Seuil') +thr_g = axes[1].axhline(thresholds["gyro"], color=CT, ls='--', lw=1.5, label='Seuil') +thr_m = axes[2].axhline(thresholds["audio"], color=CT, ls='--', lw=1.5, label='Seuil') + +# Légendes +for ax in axes[:3]: + ax.legend(loc='upper left', fontsize=7, facecolor=PANEL, edgecolor='#444466', + labelcolor=TEXT, framealpha=0.8) from matplotlib.patches import Rectangle from matplotlib.collections import PatchCollection @@ -262,17 +276,30 @@ def update_spans(spans, trig_list): for i, span in enumerate(spans): span.set_alpha(0.25 if i < len(tl) and tl[i] else 0) +def rolling_avg(arr, window): + """Moyenne glissante simple — même logique que le XIAO""" + kernel = np.ones(window) / window + return np.convolve(arr, kernel, mode='same') + def update(frame): # Snapshot des buffers (rapide) a = np.array(accel_buf) g = np.array(gyro_buf) - m = np.array(audio_buf) + m = np.array(audio_buf, dtype=float) s = np.array(shot_buf) + # Moyennes glissantes (même fenêtre que le XIAO) + a_avg = rolling_avg(a, AVG_WINDOW) + g_avg = rolling_avg(g, AVG_WINDOW) + m_avg = rolling_avg(m, AVG_WINDOW) + # Mise à jour des données des lignes line_a.set_ydata(a) line_g.set_ydata(g) line_m.set_ydata(m) + line_a2.set_ydata(a_avg) + line_g2.set_ydata(g_avg) + line_m2.set_ydata(m_avg) line_s.set_ydata(s) # Mise à jour des seuils @@ -319,7 +346,7 @@ def update(frame): f"Debug XIAO : {DEBUG_MODE_NAMES[debug_mode]} [NUM0 = cycle OFF->RAW->TRIGGERS->FULL] | " f"MinSensors : {min_sensors}/3 [NUM6=+1 NUM4=-1]") - return (line_a, line_g, line_m, line_s, + return (line_a, line_g, line_m, line_a2, line_g2, line_m2, line_s, thr_a, thr_g, thr_m, *titles, status_txt, debug_txt)