Python: calibration tool — ligne orange pic dernier tir + titre simplifié

Ajoute une ligne horizontale orange sur chaque graphe indiquant le niveau
atteint au moment du dernier tir détecté, avec % du seuil dans le titre.
Permet de comprendre pourquoi un tir est déclenché même si le pic BLE
(20Hz) ne semble pas dépasser le seuil (résolution temporelle limitée).
Supprime le pic 1s (remplacé par le pic dernier tir, plus pertinent).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
j.foucher 2026-02-19 09:52:03 +01:00
parent f23ddf9a82
commit f33fd5b216

View File

@ -34,6 +34,8 @@ thresholds = {"accel": 2.5, "gyro": 200.0, "audio": 3000} # PDM : 0-32767
min_sensors = 3 # Nb de capteurs requis simultanément (1-3) — NUM6=+1 NUM4=-1
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_running = True
audio_max_global = 1000 # Tracks le max absolu jamais vu
@ -66,10 +68,14 @@ def debug_callback(sender, data):
shot_pending = False
def shot_callback(sender, data):
global shot_count, shot_pending
global shot_count, shot_pending, last_shot_peak
if data[0] == 1:
shot_count += 1
shot_pending = True
# Capturer le pic courant au moment exact du tir (meilleure approximation possible en BLE)
last_shot_peak["accel"] = accel_buf[-1] if accel_buf else 0.0
last_shot_peak["gyro"] = gyro_buf[-1] if gyro_buf else 0.0
last_shot_peak["audio"] = audio_buf[-1] if audio_buf else 0
async def find_device():
"""Scan par nom (toutes les 0.5s pendant 30s max).
@ -235,11 +241,16 @@ 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)')
# Légendes — incluent le fond rouge = trigger actif
# 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]:
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],
handles=[ax.get_lines()[0], ax.get_lines()[1], ax.get_lines()[2],
plt.Rectangle((0,0),1,1, fc=CT, alpha=0.25, label='Trigger actif (XIAO)')])
from matplotlib.patches import Rectangle
@ -279,17 +290,25 @@ def update(frame):
m = np.array(audio_buf, dtype=float)
s = np.array(shot_buf)
# Pic max sur la dernière seconde (pour affichage dans le titre)
a_peak = float(a[-PEAK_WINDOW:].max())
g_peak = float(g[-PEAK_WINDOW:].max())
m_peak = float(m[-PEAK_WINDOW:].max())
# Mise à jour des données des lignes
# Mise à jour des données des lignes brutes
line_a.set_ydata(a)
line_g.set_ydata(g)
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"]])
@ -310,22 +329,22 @@ 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
a_ratio = a_peak / thresholds['accel'] * 100
g_ratio = g_peak / thresholds['gyro'] * 100
m_ratio = m_peak / thresholds['audio'] * 100
# 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 "
f"val: {a[-1]:.2f} G pic(1s): {a_peak:.2f} G seuil: {thresholds['accel']:.1f} G "
f"({a_ratio:.0f}% du seuil) [NUM7=+0.1 NUM1=-0.1]")
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 "
f"val: {g[-1]:.0f} dps pic(1s): {g_peak:.0f} dps seuil: {thresholds['gyro']:.0f} dps "
f"({g_ratio:.0f}% du seuil) [NUM8=+10 NUM2=-10]")
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 "
f"val: {m[-1]:.0f} pic(1s): {m_peak:.0f} seuil: {thresholds['audio']} "
f"({m_ratio:.0f}% du seuil) [NUM9=+500 NUM3=-500]")
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]")
titles[3].set_text(f"Tirs detectes : {shot_count} (fenetre glissante)")
status_txt.set_text(
@ -336,6 +355,7 @@ 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):