Python: calibration tool — simplification visuelle + pic max 1s dans titre

- Supprime la 2ème courbe (trompeuse, difficile à interpréter correctement)
- Affiche dans le titre : valeur courante + pic max sur 1s + % du seuil
- Les fonds rouges (trigger actif XIAO) restent la référence de décision
- Légende mise à jour : brut / seuil / trigger actif

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

View File

@ -44,10 +44,8 @@ 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 en samples correspondant à la Window du XIAO (60ms / 50ms debug rate = ~1.2 → 3 samples)
# Le XIAO ne fait PAS de moyenne : il compare la valeur brute au seuil et maintient le trigger 60ms.
# On affiche donc le MAX glissant sur 3 samples = ce que le XIAO "retient" comme pic actif.
AVG_WINDOW = 3
# Fenêtre glissante pour afficher le pic max récent dans le titre (informatif seulement)
PEAK_WINDOW = 20 # ~1s à 20Hz
# ─── BLE ────────────────────────────────────────────────
def debug_callback(sender, data):
@ -200,9 +198,6 @@ TEXT = '#e0e0e0'
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'
@ -230,23 +225,22 @@ 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.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'Pic maintenu ({AVG_WINDOW} pts / 60ms) ← logique XIAO')
line_g2, = axes[1].plot(X, list(gyro_buf), color=CG2, lw=2.0, label=f'Pic maintenu ({AVG_WINDOW} pts / 60ms) ← logique XIAO')
line_m2, = axes[2].plot(X, list(audio_buf), color=CM2, lw=2.0, label=f'Pic maintenu ({AVG_WINDOW} pts / 60ms) ← logique XIAO')
line_a, = axes[0].plot(X, list(accel_buf), color=CA, lw=1.5, label='Valeur brute')
line_g, = axes[1].plot(X, list(gyro_buf), color=CG, lw=1.5, label='Valeur brute')
line_m, = axes[2].plot(X, list(audio_buf), color=CM, lw=1.5, label='Valeur brute')
line_s, = axes[3].plot(X, list(shot_buf), color=CS, lw=0, marker='|',
markersize=20, markeredgewidth=2.5) # barres verticales
markersize=20, markeredgewidth=2.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')
thr_a = axes[0].axhline(thresholds["accel"], 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)')
# Légendes
# Légendes — incluent le fond rouge = trigger actif
for ax in axes[:3]:
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],
plt.Rectangle((0,0),1,1, fc=CT, alpha=0.25, label='Trigger actif (XIAO)')])
from matplotlib.patches import Rectangle
from matplotlib.collections import PatchCollection
@ -278,13 +272,6 @@ 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_max(arr, window):
"""Max glissant — simule le trigger qui reste actif pendant 'window' samples (logique XIAO)"""
from numpy.lib.stride_tricks import sliding_window_view
pad = window - 1
padded = np.pad(arr, (pad, 0), mode='edge')
return np.max(sliding_window_view(padded, window), axis=1)
def update(frame):
# Snapshot des buffers (rapide)
a = np.array(accel_buf)
@ -292,18 +279,15 @@ def update(frame):
m = np.array(audio_buf, dtype=float)
s = np.array(shot_buf)
# Pic maintenu sur la fenêtre (même logique que le trigger XIAO)
a_avg = rolling_max(a, AVG_WINDOW)
g_avg = rolling_max(g, AVG_WINDOW)
m_avg = rolling_max(m, AVG_WINDOW)
# 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
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
@ -327,21 +311,21 @@ def update(frame):
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
titles[0].set_text(
f"Accelerometre "
f"val: {a[-1]:.2f} G "
f"seuil: {thresholds['accel']:.1f} G "
f"[NUM7=+0.1 NUM1=-0.1]")
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]")
titles[1].set_text(
f"Gyroscope "
f"val: {g[-1]:.0f} dps "
f"seuil: {thresholds['gyro']:.0f} dps "
f"[NUM8=+10 NUM2=-10]")
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]")
titles[2].set_text(
f"Microphone PDM "
f"val: {m[-1]:.0f} "
f"seuil: {thresholds['audio']} "
f"[NUM9=+500 NUM3=-500]")
f"val: {m[-1]:.0f} pic(1s): {m_peak:.0f} seuil: {thresholds['audio']} "
f"({m_ratio:.0f}% du seuil) [NUM9=+500 NUM3=-500]")
titles[3].set_text(f"Tirs detectes : {shot_count} (fenetre glissante)")
status_txt.set_text(
@ -350,7 +334,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_a2, line_g2, line_m2, line_s,
return (line_a, line_g, line_m, line_s,
thr_a, thr_g, thr_m,
*titles, status_txt, debug_txt)