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 min_sensors = 3 # Nb de capteurs requis simultanément (1-3) — NUM6=+1 NUM4=-1
shot_count = 0 shot_count = 0
shot_pending = False # Flag : un tir reçu, à enregistrer dans shot_buf au prochain debug tick 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 ble_running = True
audio_max_global = 1000 # Tracks le max absolu jamais vu audio_max_global = 1000 # Tracks le max absolu jamais vu
@ -66,10 +68,14 @@ def debug_callback(sender, data):
shot_pending = False shot_pending = False
def shot_callback(sender, data): def shot_callback(sender, data):
global shot_count, shot_pending global shot_count, shot_pending, last_shot_peak
if data[0] == 1: if data[0] == 1:
shot_count += 1 shot_count += 1
shot_pending = True 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(): async def find_device():
"""Scan par nom (toutes les 0.5s pendant 30s max). """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_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)') 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]: for ax in axes[:3]:
ax.legend(loc='upper left', fontsize=7, facecolor=PANEL, edgecolor='#444466', ax.legend(loc='upper left', fontsize=7, facecolor=PANEL, edgecolor='#444466',
labelcolor=TEXT, framealpha=0.8, 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)')]) plt.Rectangle((0,0),1,1, fc=CT, alpha=0.25, label='Trigger actif (XIAO)')])
from matplotlib.patches import Rectangle from matplotlib.patches import Rectangle
@ -279,17 +290,25 @@ def update(frame):
m = np.array(audio_buf, dtype=float) m = np.array(audio_buf, dtype=float)
s = np.array(shot_buf) s = np.array(shot_buf)
# Pic max sur la dernière seconde (pour affichage dans le titre) # Mise à jour des données des lignes brutes
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
line_a.set_ydata(a) line_a.set_ydata(a)
line_g.set_ydata(g) line_g.set_ydata(g)
line_m.set_ydata(m) line_m.set_ydata(m)
line_s.set_ydata(s) 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 # Mise à jour des seuils
thr_a.set_ydata([thresholds["accel"], thresholds["accel"]]) thr_a.set_ydata([thresholds["accel"], thresholds["accel"]])
thr_g.set_ydata([thresholds["gyro"], thresholds["gyro"]]) thr_g.set_ydata([thresholds["gyro"], thresholds["gyro"]])
@ -310,22 +329,22 @@ def update(frame):
for i, span in enumerate(shot_spans): for i, span in enumerate(shot_spans):
span.set_alpha(0.85 if i < len(sl) and sl[i] > 0 else 0) span.set_alpha(0.85 if i < len(sl) and sl[i] > 0 else 0)
# Titres avec valeurs courantes # Titres avec valeurs courantes + pic dernier tir
a_ratio = a_peak / thresholds['accel'] * 100 pa = last_shot_peak["accel"] if shot_count > 0 else None
g_ratio = g_peak / thresholds['gyro'] * 100 pg = last_shot_peak["gyro"] if shot_count > 0 else None
m_ratio = m_peak / thresholds['audio'] * 100 pm = last_shot_peak["audio"] if shot_count > 0 else None
titles[0].set_text( titles[0].set_text(
f"Accelerometre " f"Accelerometre val: {a[-1]:.2f} G seuil: {thresholds['accel']:.1f} G"
f"val: {a[-1]:.2f} G pic(1s): {a_peak:.2f} G seuil: {thresholds['accel']:.1f} G " + (f" ── pic dernier tir: {pa:.2f} G ({pa/thresholds['accel']*100:.0f}%)" if pa else "")
f"({a_ratio:.0f}% du seuil) [NUM7=+0.1 NUM1=-0.1]") + f" [NUM7=+0.1 NUM1=-0.1]")
titles[1].set_text( titles[1].set_text(
f"Gyroscope " f"Gyroscope val: {g[-1]:.0f} dps seuil: {thresholds['gyro']:.0f} dps"
f"val: {g[-1]:.0f} dps pic(1s): {g_peak:.0f} dps seuil: {thresholds['gyro']:.0f} dps " + (f" ── pic dernier tir: {pg:.0f} dps ({pg/thresholds['gyro']*100:.0f}%)" if pg else "")
f"({g_ratio:.0f}% du seuil) [NUM8=+10 NUM2=-10]") + f" [NUM8=+10 NUM2=-10]")
titles[2].set_text( titles[2].set_text(
f"Microphone PDM " f"Microphone PDM val: {m[-1]:.0f} seuil: {thresholds['audio']}"
f"val: {m[-1]:.0f} pic(1s): {m_peak:.0f} seuil: {thresholds['audio']} " + (f" ── pic dernier tir: {pm:.0f} ({pm/thresholds['audio']*100:.0f}%)" if pm else "")
f"({m_ratio:.0f}% du seuil) [NUM9=+500 NUM3=-500]") + f" [NUM9=+500 NUM3=-500]")
titles[3].set_text(f"Tirs detectes : {shot_count} (fenetre glissante)") titles[3].set_text(f"Tirs detectes : {shot_count} (fenetre glissante)")
status_txt.set_text( status_txt.set_text(
@ -336,6 +355,7 @@ def update(frame):
return (line_a, line_g, line_m, line_s, return (line_a, line_g, line_m, line_s,
thr_a, thr_g, thr_m, thr_a, thr_g, thr_m,
peak_a, peak_g, peak_m,
*titles, status_txt, debug_txt) *titles, status_txt, debug_txt)
def on_key(event): def on_key(event):