Compare commits

...

8 Commits

Author SHA1 Message Date
f750378e9d no message 2026-05-03 11:00:08 +02:00
45bc779556 Last change for Demo Actor 2026-02-19 12:32:03 +01:00
f825695658 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 <noreply@anthropic.com>
2026-02-19 10:15:51 +01:00
f33fd5b216 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>
2026-02-19 09:52:03 +01:00
f23ddf9a82 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>
2026-02-19 09:47:09 +01:00
3bb8d6c58a Python: calibration tool — correction logique fenêtre glissante
Remplace la moyenne glissante par un max glissant sur 3 samples (60ms),
ce qui reflète fidèlement la logique réelle du XIAO : la valeur brute
est comparée au seuil et le trigger reste actif 60ms (pas de moyenne).
Légende mise à jour en conséquence.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:40:21 +01:00
6048ed9066 Python: calibration tool — ajout courbe moyenne glissante + légende
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 <noreply@anthropic.com>
2026-02-19 09:39:18 +01:00
ad72c2aad5 no message 2026-02-19 09:33:13 +01:00
6 changed files with 91 additions and 40 deletions

View File

@@ -0,0 +1,30 @@
{
"permissions": {
"allow": [
"Bash(powershell -Command \"Get-ChildItem ''C:\\\\Program Files'',''C:\\\\Program Files \\(x86\\)'',''C:\\\\Arduino'',''D:\\\\'' -Filter ''arduino.exe'' -Recurse -Depth 3 -ErrorAction SilentlyContinue | Select -First 3 -ExpandProperty FullName\")",
"Bash(\"/c/Program Files/Epic Games/UE_5.5/Engine/Build/BatchFiles/Build.bat\" PS_BLE_Tracker Win64 Development -project=\"C:/ASTERION/GIT/PS_BLE_Tracker/Unreal/PS_BLE_Tracker.uproject\" -waitmutex)",
"Bash(powershell -Command \"\\(Get-ItemProperty ''HKLM:\\\\SOFTWARE\\\\WOW6432Node\\\\Arduino'' -ErrorAction SilentlyContinue\\).Install_Dir; \\(Get-ItemProperty ''HKLM:\\\\SOFTWARE\\\\Arduino'' -ErrorAction SilentlyContinue\\).Install_Dir; \\(Get-ItemProperty ''HKCU:\\\\SOFTWARE\\\\Arduino'' -ErrorAction SilentlyContinue\\).Install_Dir\")",
"Bash(powershell -Command \"Get-Command arduino -ErrorAction SilentlyContinue | Select -ExpandProperty Source; Get-ItemProperty ''HKLM:\\\\SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\*'',''HKLM:\\\\SOFTWARE\\\\WOW6432Node\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\*'' -ErrorAction SilentlyContinue | Where-Object DisplayName -like ''*Arduino*'' | Select DisplayName,InstallLocation | Format-List\")",
"Bash(powershell -Command \"Get-ChildItem ''$env:APPDATA\\\\Microsoft\\\\Windows\\\\Start Menu'' -Filter ''*arduino*'' -Recurse -ErrorAction SilentlyContinue | Select FullName; Get-ChildItem ''C:\\\\ProgramData\\\\Microsoft\\\\Windows\\\\Start Menu'' -Filter ''*arduino*'' -Recurse -ErrorAction SilentlyContinue | Select FullName; Get-ChildItem ''$env:USERPROFILE\\\\Desktop'' -Filter ''*arduino*'' -ErrorAction SilentlyContinue | Select FullName\")",
"Read(//c/Users/j_foucher/AppData/Local/Arduino15/**)",
"Read(//c/Users/j_foucher/AppData/Roaming/Arduino15/**)",
"Bash(powershell -Command \"Get-ChildItem ''C:\\\\'' -Directory -ErrorAction SilentlyContinue | ForEach-Object { Get-ChildItem $_FullName -Filter ''arduino.exe'' -ErrorAction SilentlyContinue -Depth 2 } | Select -First 3 -ExpandProperty FullName\")",
"Bash(powershell -Command \"Get-ChildItem ''C:\\\\Program Files'',''C:\\\\Program Files \\(x86\\)'',''C:\\\\Users\\\\j_foucher'' -Directory -ErrorAction SilentlyContinue | ForEach-Object { Get-ChildItem $_FullName -Filter ''arduino.exe'' -Recurse -Depth 3 -ErrorAction SilentlyContinue } | Select -First 5 -ExpandProperty FullName\")",
"Bash(powershell -Command \"Get-Process | Where-Object { $_.ProcessName -match ''arduino|java'' -and $_.Path } | Select ProcessName,Path | Format-List\")",
"Read(//c/Users/j_foucher/AppData/Local/**)",
"Bash(find /c/Users/j_foucher -maxdepth 6 -name boards.txt -path *nrf*)",
"Bash(ARDUINO_DIR=\"/c/Program Files/WindowsApps/ArduinoLLC.ArduinoIDE_1.8.57.0_x86__mdqgnx93n4wtt\")",
"Bash(\"$ARDUINO_DIR/arduino-builder\" -hardware \"$ARDUINO_DIR/hardware\" -hardware \"/c/Users/j_foucher/Documents/ArduinoData/packages\" -tools \"$ARDUINO_DIR/tools-builder\" -tools \"/c/Users/j_foucher/Documents/ArduinoData/packages\" -libraries \"/c/Users/j_foucher/Documents/Arduino/libraries\" -fqbn \"Seeeduino:mbed:xiaonRF52840Sense\" -build-path \"/c/Users/j_foucher/AppData/Local/Temp/arduino_build\" -build-cache \"/c/Users/j_foucher/AppData/Local/Temp/arduino_cache\" -verbose \"C:/ASTERION/GIT/PS_BLE_Tracker/Arduino/PS_BLE_ShotDetection/PS_BLE_ShotDetection.ino\")",
"Bash(mkdir -p /c/Users/j_foucher/AppData/Local/Temp/arduino_build)",
"Bash(cmd.exe //c \"\"\"C:\\\\Program Files\\\\WindowsApps\\\\ArduinoLLC.ArduinoIDE_1.8.57.0_x86__mdqgnx93n4wtt\\\\arduino_debug.exe\"\" --verify --board Seeeduino:mbed:xiaonRF52840Sense --pref build.path=C:\\\\Users\\\\j_foucher\\\\AppData\\\\Local\\\\Temp\\\\arduino_build \"\"C:\\\\ASTERION\\\\GIT\\\\PS_BLE_Tracker\\\\Arduino\\\\PS_BLE_ShotDetection\\\\PS_BLE_ShotDetection.ino\"\" 2>&1\")",
"Bash(powershell -Command \"& ''C:\\\\Program Files\\\\WindowsApps\\\\ArduinoLLC.ArduinoIDE_1.8.57.0_x86__mdqgnx93n4wtt\\\\arduino_debug.exe'' --verify --board Seeeduino:mbed:xiaonRF52840Sense --pref ''build.path=C:\\\\Users\\\\j_foucher\\\\AppData\\\\Local\\\\Temp\\\\arduino_build'' ''C:\\\\ASTERION\\\\GIT\\\\PS_BLE_Tracker\\\\Arduino\\\\PS_BLE_ShotDetection\\\\PS_BLE_ShotDetection.ino'' 2>&1\")",
"Bash(where winget:*)",
"Bash(winget install:*)",
"Bash(export PATH=\"$PATH:/c/Program Files/Arduino CLI\")",
"Bash(arduino-cli version:*)",
"Bash(arduino-cli core:*)",
"Bash(arduino-cli lib:*)",
"Bash(arduino-cli compile:*)"
]
}
}

2
.gitignore vendored
View File

@@ -3,3 +3,5 @@ Unreal/Intermediate/
Unreal/Plugins/WinBluetoothLE/Intermediate/ Unreal/Plugins/WinBluetoothLE/Intermediate/
Unreal/Binaries/ Unreal/Binaries/
Unreal/Plugins/WinBluetoothLE/Binaries/ Unreal/Plugins/WinBluetoothLE/Binaries/
Unreal/Plugins/PS_Win_BLE/Binaries/
Unreal/Plugins/PS_Win_BLE/Intermediate/

View File

@@ -13,7 +13,7 @@ from collections import deque
from bleak import BleakClient, BleakScanner from bleak import BleakClient, BleakScanner
DEVICE_NAME = "XIAO Airsoft Pro" 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" DEBUG_CHAR_UUID = "6E400005-B5A3-F393-E0A9-E50E24DCCA9E"
SHOT_CHAR_UUID = "6E400004-B5A3-F393-E0A9-E50E24DCCA9E" SHOT_CHAR_UUID = "6E400004-B5A3-F393-E0A9-E50E24DCCA9E"
CONFIG_CHAR_UUID = "6E400006-B5A3-F393-E0A9-E50E24DCCA9E" CONFIG_CHAR_UUID = "6E400006-B5A3-F393-E0A9-E50E24DCCA9E"
@@ -34,7 +34,9 @@ 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
ble_status = "🔍 Connexion..." # 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 ble_running = True
audio_max_global = 1000 # Tracks le max absolu jamais vu audio_max_global = 1000 # Tracks le max absolu jamais vu
ble_client = None # Référence au client BLE actif ble_client = None # Référence au client BLE actif
@@ -44,6 +46,9 @@ debug_mode = 3 # 0=OFF, 1=RAW, 2=TRIGGERS, 3=FULL (actif par défaut)
import numpy as np import numpy as np
X = np.arange(WINDOW_SIZE) # axe X fixe, ne change jamais X = np.arange(WINDOW_SIZE) # axe X fixe, ne change jamais
# Fenêtre glissante pour afficher le pic max récent dans le titre (informatif seulement)
PEAK_WINDOW = 20 # ~1s à 20Hz
# ─── BLE ──────────────────────────────────────────────── # ─── BLE ────────────────────────────────────────────────
def debug_callback(sender, data): def debug_callback(sender, data):
global audio_max_global, shot_pending global audio_max_global, shot_pending
@@ -63,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).
@@ -122,10 +131,10 @@ async def ble_loop():
global ble_status, ble_running, ble_client, config_pending global ble_status, ble_running, ble_client, config_pending
while ble_running: while ble_running:
try: try:
ble_status = f"🔍 Recherche '{DEVICE_NAME}'..." ble_status = f"[?] Recherche '{DEVICE_NAME}'..."
device = await find_device() device = await find_device()
if not device: if not device:
ble_status = "⚠️ XIAO non trouvé — réessai..." ble_status = "[!] XIAO non trouve — reessai..."
await asyncio.sleep(5) await asyncio.sleep(5)
continue continue
# device peut être une string (adresse directe) ou un BLEDevice # device peut être une string (adresse directe) ou un BLEDevice
@@ -135,13 +144,13 @@ async def ble_loop():
else: else:
addr = device.address addr = device.address
display_name = device.name or DEVICE_NAME 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: async with BleakClient(addr, timeout=15.0) as client:
ble_client = client ble_client = client
await client.start_notify(DEBUG_CHAR_UUID, debug_callback) await client.start_notify(DEBUG_CHAR_UUID, debug_callback)
await client.start_notify(SHOT_CHAR_UUID, shot_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) # À la connexion : lire la config sauvegardée dans le XIAO (flash)
try: try:
raw = await client.read_gatt_char(CONFIG_CHAR_UUID) raw = await client.read_gatt_char(CONFIG_CHAR_UUID)
@@ -173,10 +182,14 @@ async def ble_loop():
config_pending = False config_pending = False
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
ble_client = None 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: except Exception as e:
ble_client = None ble_client = None
ble_status = f" {str(e)[:50]}" ble_status = f"[X] {str(e)[:50]}"
await asyncio.sleep(5) await asyncio.sleep(5)
def run_ble(): def run_ble():
@@ -192,9 +205,9 @@ from matplotlib.animation import FuncAnimation
BG = '#1a1a2e' BG = '#1a1a2e'
PANEL = '#16213e' PANEL = '#16213e'
TEXT = '#e0e0e0' TEXT = '#e0e0e0'
CA = '#4fc3f7' # accel CA = '#4fc3f7' # accel brut
CG = '#81c784' # gyro CG = '#81c784' # gyro brut
CM = '#ce93d8' # audio CM = '#ce93d8' # audio brut
CT = '#ef5350' # trigger / seuil CT = '#ef5350' # trigger / seuil
CS = '#ff9800' # shot (orange vif) CS = '#ff9800' # shot (orange vif)
GRID = '#2a2a4a' GRID = '#2a2a4a'
@@ -222,15 +235,23 @@ axes[3].set_ylim(-0.1, 1.5)
axes[3].set_yticks([]) # pas de graduations Y sur la timeline axes[3].set_yticks([]) # pas de graduations Y sur la timeline
# Créer les artistes UNE SEULE FOIS — on ne les recrée jamais # 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_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) 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) 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='|', 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) 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) 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) thr_m = axes[2].axhline(thresholds["audio"], color=CT, ls='--', lw=1.5, label='Seuil (trigger XIAO)')
# 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=[lines[0],
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
from matplotlib.collections import PatchCollection from matplotlib.collections import PatchCollection
@@ -266,10 +287,10 @@ def update(frame):
# Snapshot des buffers (rapide) # Snapshot des buffers (rapide)
a = np.array(accel_buf) a = np.array(accel_buf)
g = np.array(gyro_buf) g = np.array(gyro_buf)
m = np.array(audio_buf) m = np.array(audio_buf, dtype=float)
s = np.array(shot_buf) s = np.array(shot_buf)
# Mise à jour des données des lignes # Mise à jour des données des lignes brutes
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)
@@ -296,21 +317,9 @@ def update(frame):
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
titles[0].set_text( titles[0].set_text(f"Accelerometre val: {a[-1]:.2f} G seuil: {thresholds['accel']:.1f} G [NUM7=+0.1 NUM1=-0.1]")
f"Accelerometre " titles[1].set_text(f"Gyroscope val: {g[-1]:.0f} dps seuil: {thresholds['gyro']:.0f} dps [NUM8=+10 NUM2=-10]")
f"val: {a[-1]:.2f} G " titles[2].set_text(f"Microphone PDM val: {m[-1]:.0f} seuil: {thresholds['audio']} [NUM9=+500 NUM3=-500]")
f"seuil: {thresholds['accel']:.1f} G "
f"[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]")
titles[2].set_text(
f"Microphone PDM "
f"val: {m[-1]:.0f} "
f"seuil: {thresholds['audio']} "
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(

View File

@@ -0,0 +1,8 @@
[FilterPlugin]
; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and
; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively.
;
; Examples:
; /README.txt
; /Extras/...
; /Binaries/ThirdParty/*.dll

2
remove worktree.txt Normal file
View File

@@ -0,0 +1,2 @@
Supprimer E:\ASTERION\GIT\PS_BLE_Tracker\.claude\worktrees\hardcore-taussig
git worktree prune dans le repo