Add arduino and python code

This commit is contained in:
j.foucher 2026-02-18 09:20:51 +01:00
parent 00f44bf81b
commit d30569e6a1
3 changed files with 941 additions and 0 deletions

View File

@ -0,0 +1,537 @@
/*
* XIAO nRF52840 Sense - Airsoft Tracker Pro v3.2
* Microphone PDM natif (pas analogRead !)
* Détection multi-capteurs : Accel + Gyro + Micro PDM
*/
#include <Arduino.h>
#include <ArduinoBLE.h>
#include <LSM6DS3.h>
#include <Wire.h>
#include <PDM.h> // ← Bibliothèque PDM intégrée nRF52840
// ====== PINS LED ======
#define LED_RED 11
#define LED_GREEN 12
#define LED_BLUE 13
// ====== FLASH ======
#define FLASH_STORAGE_START 0x7F000
// ====== ENUMS (déclarés en premier) ======
enum LedState {
LED_BOOT, LED_NO_PAIRING, LED_ADVERTISING,
LED_CONNECTED, LED_SHOT_FLASH, LED_REJECTED, LED_ERROR
};
enum DebugMode {
DEBUG_OFF, DEBUG_RAW, DEBUG_TRIGGERS, DEBUG_FULL
};
// ====== STRUCTURES ======
struct PairingData {
uint32_t magic;
char authorizedMAC[18];
uint8_t reserved[10];
};
struct ShotConfig {
float accelThreshold; // G
float gyroThreshold; // deg/s
uint16_t audioThreshold; // 0-32767 (PDM 16-bit)
uint16_t accelWindow; // ms
uint16_t gyroWindow; // ms
uint16_t audioWindow; // ms
uint8_t minSensors; // 1-3
uint16_t shotCooldown; // ms
bool useAccel;
bool useGyro;
bool useAudio;
};
// ====== VARIABLES GLOBALES ======
PairingData pairing;
ShotConfig shotConfig;
const uint32_t MAGIC_NUMBER = 0xCAFEBABE;
// IMU
LSM6DS3 imu(I2C_MODE, 0x6A);
float ax, ay, az, gx, gy, gz;
float roll=0, pitch=0, yaw=0;
unsigned long lastUpdate=0, lastSend=0;
const float alpha = 0.98f;
// ====== PDM MICROPHONE ======
#define PDM_BUFFER_SIZE 256
short pdmBuffer[PDM_BUFFER_SIZE];
volatile int pdmSamplesReady = 0;
volatile int16_t pdmPeak = 0; // Pic absolu du dernier buffer
void onPDMdata() {
int bytesAvailable = PDM.available();
PDM.read(pdmBuffer, bytesAvailable);
int samples = bytesAvailable / 2;
// Calculer le pic absolu dans ce buffer
int16_t peak = 0;
for (int i = 0; i < samples; i++) {
int16_t v = abs(pdmBuffer[i]);
if (v > peak) peak = v;
}
pdmPeak = peak;
pdmSamplesReady = 1;
}
// Valeur audio lissée pour debug
float audioSmoothed = 0;
// ====== DÉTECTION TIR ======
unsigned long lastShotTime = 0;
bool accelTrigger = false;
bool gyroTrigger = false;
bool audioTrigger = false;
unsigned long accelTriggerTime = 0;
unsigned long gyroTriggerTime = 0;
unsigned long audioTriggerTime = 0;
// ====== LED ======
LedState currentLedState = LED_BOOT;
unsigned long ledTimer = 0;
unsigned long shotFlashTimer = 0;
unsigned long connectionTime = 0;
bool ledBlinkState = false;
bool isAdvertising = false;
// ====== DEBUG ======
DebugMode debugMode = DEBUG_OFF;
unsigned long lastDebugSend = 0;
const uint16_t DEBUG_RATE = 50; // 20 Hz
// ====== UUIDs BLE ======
const char* SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E";
const char* IMU_CHAR_UUID = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E";
const char* SHOT_CHAR_UUID = "6E400004-B5A3-F393-E0A9-E50E24DCCA9E";
const char* DEBUG_CHAR_UUID = "6E400005-B5A3-F393-E0A9-E50E24DCCA9E";
const char* CONFIG_CHAR_UUID = "6E400006-B5A3-F393-E0A9-E50E24DCCA9E";
// ====== BLE ======
BLEService motionService(SERVICE_UUID);
BLECharacteristic imuChar (IMU_CHAR_UUID, BLERead|BLENotify, 12, true);
BLECharacteristic shotChar (SHOT_CHAR_UUID, BLERead|BLENotify, 1, true);
BLECharacteristic debugChar (DEBUG_CHAR_UUID, BLERead|BLENotify, 20, true);
BLECharacteristic configChar (CONFIG_CHAR_UUID, BLERead|BLEWrite, 32, true);
// ════════════════════════════════════════════════
// LED
// ════════════════════════════════════════════════
void setLED(uint8_t r, uint8_t g, uint8_t b) {
digitalWrite(LED_RED, r ? LOW : HIGH);
digitalWrite(LED_GREEN, g ? LOW : HIGH);
digitalWrite(LED_BLUE, b ? LOW : HIGH);
}
void changeLedState(LedState s) {
currentLedState = s;
ledTimer = millis();
ledBlinkState = false;
}
void updateLED() {
unsigned long now = millis();
if (currentLedState == LED_SHOT_FLASH) {
if (now - shotFlashTimer < 60) { setLED(255,0,0); return; }
else currentLedState = LED_CONNECTED;
}
switch (currentLedState) {
case LED_BOOT:
if (now-ledTimer>200){ ledTimer=now; ledBlinkState=!ledBlinkState;
setLED(ledBlinkState?128:0, 0, ledBlinkState?128:0); } break;
case LED_NO_PAIRING:
if (now-ledTimer>1000){ ledTimer=now; ledBlinkState=!ledBlinkState;
setLED(ledBlinkState?255:0, ledBlinkState?200:0, 0); } break;
case LED_ADVERTISING:
if (now-ledTimer>500){ ledTimer=now; ledBlinkState=!ledBlinkState;
setLED(0,0,ledBlinkState?255:0); } break;
case LED_CONNECTED: setLED(0,255,0); break;
case LED_REJECTED:
if (now-ledTimer>150){ ledTimer=now; ledBlinkState=!ledBlinkState;
setLED(ledBlinkState?255:0,0,0); } break;
case LED_ERROR: setLED(255,0,0); break;
default: break;
}
}
// ════════════════════════════════════════════════
// CONFIG
// ════════════════════════════════════════════════
void initDefaultConfig() {
shotConfig.accelThreshold = 2.5f;
shotConfig.gyroThreshold = 200.0f;
shotConfig.audioThreshold = 3000; // Seuil PDM (0-32767)
shotConfig.accelWindow = 20;
shotConfig.gyroWindow = 20;
shotConfig.audioWindow = 15;
shotConfig.minSensors = 2;
shotConfig.shotCooldown = 80;
shotConfig.useAccel = true;
shotConfig.useGyro = true;
shotConfig.useAudio = true;
}
void printConfig() {
Serial.println("\n╔══════════ CONFIG ══════════╗");
Serial.print("Accel : "); Serial.print(shotConfig.accelThreshold,1);
Serial.print(" G ["); Serial.print(shotConfig.useAccel?"ON":"OFF"); Serial.println("]");
Serial.print("Gyro : "); Serial.print(shotConfig.gyroThreshold,0);
Serial.print(" °/s ["); Serial.print(shotConfig.useGyro?"ON":"OFF"); Serial.println("]");
Serial.print("Audio : "); Serial.print(shotConfig.audioThreshold);
Serial.print(" PDM ["); Serial.print(shotConfig.useAudio?"ON":"OFF"); Serial.println("]");
Serial.print("MinSens: "); Serial.println(shotConfig.minSensors);
Serial.print("Cooldown: "); Serial.print(shotConfig.shotCooldown); Serial.println(" ms");
Serial.println("╚════════════════════════════╝\n");
}
// ════════════════════════════════════════════════
// FLASH
// ════════════════════════════════════════════════
void loadPairingData() {
memcpy(&pairing, (void*)FLASH_STORAGE_START, sizeof(PairingData));
if (pairing.magic == MAGIC_NUMBER) {
Serial.print("🔒 PC : "); Serial.println(pairing.authorizedMAC);
changeLedState(LED_ADVERTISING);
} else {
Serial.println("🔓 Aucun PC enregistré");
pairing.magic = 0;
memset(pairing.authorizedMAC, 0, sizeof(pairing.authorizedMAC));
changeLedState(LED_NO_PAIRING);
}
}
void savePairingData(const char* mac) {
pairing.magic = MAGIC_NUMBER;
strncpy(pairing.authorizedMAC, mac, 17);
pairing.authorizedMAC[17] = '\0';
NRF_NVMC->CONFIG = NVMC_CONFIG_WEN_Een; while(NRF_NVMC->READY==NVMC_READY_READY_Busy){}
NRF_NVMC->ERASEPAGE = FLASH_STORAGE_START; while(NRF_NVMC->READY==NVMC_READY_READY_Busy){}
NRF_NVMC->CONFIG = NVMC_CONFIG_WEN_Wen; while(NRF_NVMC->READY==NVMC_READY_READY_Busy){}
uint32_t* src=(uint32_t*)&pairing, *dst=(uint32_t*)FLASH_STORAGE_START;
for(int i=0;i<(int)(sizeof(PairingData)/4);i++){
dst[i]=src[i]; while(NRF_NVMC->READY==NVMC_READY_READY_Busy){}
}
NRF_NVMC->CONFIG = NVMC_CONFIG_WEN_Ren; while(NRF_NVMC->READY==NVMC_READY_READY_Busy){}
Serial.print("💾 "); Serial.println(pairing.authorizedMAC);
}
void resetPairing() {
pairing.magic = 0; memset(pairing.authorizedMAC,0,sizeof(pairing.authorizedMAC));
NRF_NVMC->CONFIG=NVMC_CONFIG_WEN_Een; while(NRF_NVMC->READY==NVMC_READY_READY_Busy){}
NRF_NVMC->ERASEPAGE=FLASH_STORAGE_START; while(NRF_NVMC->READY==NVMC_READY_READY_Busy){}
NRF_NVMC->CONFIG=NVMC_CONFIG_WEN_Ren; while(NRF_NVMC->READY==NVMC_READY_READY_Busy){}
Serial.println("🗑️ Reset"); changeLedState(LED_NO_PAIRING);
}
bool isAuthorized(const char* mac) {
if (pairing.magic != MAGIC_NUMBER) { savePairingData(mac); return true; }
return (strcmp(pairing.authorizedMAC, mac) == 0);
}
// ════════════════════════════════════════════════
// CALLBACKS BLE
// ════════════════════════════════════════════════
void onConnect(BLEDevice central) {
Serial.print("🔗 "); Serial.println(central.address());
if (!isAuthorized(central.address().c_str())) {
Serial.println("❌ REFUSÉ");
changeLedState(LED_REJECTED); delay(3000);
central.disconnect(); changeLedState(LED_ADVERTISING); return;
}
Serial.println("✅ CONNECTÉ");
changeLedState(LED_CONNECTED);
isAdvertising=false; lastUpdate=connectionTime=millis();
}
void onDisconnect(BLEDevice central) {
Serial.print("❌ DÉCO (");
Serial.print((millis()-connectionTime)/1000);
Serial.println("s)");
changeLedState(LED_ADVERTISING); isAdvertising=true;
}
void onConfigWrite(BLEDevice central, BLECharacteristic c) {
uint8_t data[32];
int len = c.readValue(data, 32);
if (len >= (int)sizeof(ShotConfig)) {
memcpy(&shotConfig, data, sizeof(ShotConfig));
Serial.println("⚙️ Config reçue"); printConfig();
}
}
// ════════════════════════════════════════════════
// DÉTECTION TIR
// ════════════════════════════════════════════════
bool checkWindow(unsigned long t, uint16_t w) {
return (millis()-t) <= w;
}
bool detectShot() {
unsigned long now = millis();
if ((now-lastShotTime) < shotConfig.shotCooldown) return false;
if (accelTrigger && !checkWindow(accelTriggerTime, shotConfig.accelWindow)) accelTrigger=false;
if (gyroTrigger && !checkWindow(gyroTriggerTime, shotConfig.gyroWindow)) gyroTrigger=false;
if (audioTrigger && !checkWindow(audioTriggerTime, shotConfig.audioWindow)) audioTrigger=false;
uint8_t n=0;
if (accelTrigger && shotConfig.useAccel) n++;
if (gyroTrigger && shotConfig.useGyro) n++;
if (audioTrigger && shotConfig.useAudio) n++;
if (n >= shotConfig.minSensors) {
accelTrigger=gyroTrigger=audioTrigger=false; return true;
}
return false;
}
// ════════════════════════════════════════════════
// DEBUG
// ════════════════════════════════════════════════
void sendDebugData(float accelMag, float gyroMag, uint16_t audioLevel) {
if (debugMode==DEBUG_OFF || !BLE.connected()) return;
unsigned long now=millis();
if ((now-lastDebugSend)<DEBUG_RATE) return;
lastDebugSend=now;
uint8_t buf[20];
buf[0]=(uint8_t)debugMode;
memcpy(&buf[1],&accelMag,4);
memcpy(&buf[5],&gyroMag,4);
memcpy(&buf[9],&audioLevel,2);
buf[11]=accelTrigger?1:0;
buf[12]=gyroTrigger?1:0;
buf[13]=audioTrigger?1:0;
debugChar.writeValue(buf,14);
}
void printDebugSerial(float accelMag, float gyroMag, uint16_t audioLevel) {
if (debugMode==DEBUG_OFF) return;
static unsigned long lp=0;
if ((millis()-lp)<100) return; lp=millis();
if (debugMode==DEBUG_RAW||debugMode==DEBUG_FULL) {
Serial.print("A:"); Serial.print(accelMag,2);
Serial.print(" G:"); Serial.print(gyroMag,1);
Serial.print(" M:"); Serial.print(audioLevel);
Serial.print(" | ");
}
if (debugMode==DEBUG_TRIGGERS||debugMode==DEBUG_FULL) {
Serial.print("[");
Serial.print(accelTrigger?"A":"-");
Serial.print(gyroTrigger?"G":"-");
Serial.print(audioTrigger?"M":"-");
Serial.print("]");
}
Serial.println();
}
// ════════════════════════════════════════════════
// COMMANDES SÉRIE
// ════════════════════════════════════════════════
void handleSerialCommand() {
if (!Serial.available()) return;
char cmd = Serial.read();
switch(cmd) {
case 'R': case 'r': resetPairing(); break;
case 'C': case 'c': printConfig(); break;
case 'D': case 'd':
debugMode=(DebugMode)((debugMode+1)%4);
Serial.print("Debug : ");
switch(debugMode){
case DEBUG_OFF: Serial.println("OFF"); break;
case DEBUG_RAW: Serial.println("RAW"); break;
case DEBUG_TRIGGERS: Serial.println("TRIGGERS"); break;
case DEBUG_FULL: Serial.println("FULL"); break;
} break;
case 'T': case 't':
Serial.println("💥 TEST TIR");
shotChar.writeValue((uint8_t)1);
shotFlashTimer=millis(); changeLedState(LED_SHOT_FLASH); break;
case '1': shotConfig.useAccel=!shotConfig.useAccel;
Serial.print("Accel:"); Serial.println(shotConfig.useAccel?"ON":"OFF"); break;
case '2': shotConfig.useGyro=!shotConfig.useGyro;
Serial.print("Gyro:"); Serial.println(shotConfig.useGyro?"ON":"OFF"); break;
case '3': shotConfig.useAudio=!shotConfig.useAudio;
Serial.print("Audio:"); Serial.println(shotConfig.useAudio?"ON":"OFF"); break;
case '+': shotConfig.accelThreshold+=0.1f;
Serial.print("Accel:"); Serial.println(shotConfig.accelThreshold,1); break;
case '-': shotConfig.accelThreshold=max(0.3f,shotConfig.accelThreshold-0.1f);
Serial.print("Accel:"); Serial.println(shotConfig.accelThreshold,1); break;
case 'G': shotConfig.gyroThreshold+=10.0f;
Serial.print("Gyro:"); Serial.println(shotConfig.gyroThreshold,0); break;
case 'g': shotConfig.gyroThreshold=max(20.0f,shotConfig.gyroThreshold-10.0f);
Serial.print("Gyro:"); Serial.println(shotConfig.gyroThreshold,0); break;
case 'M': shotConfig.audioThreshold+=200;
Serial.print("Audio PDM:"); Serial.println(shotConfig.audioThreshold); break;
case 'n': shotConfig.audioThreshold=max((uint16_t)200,
(uint16_t)(shotConfig.audioThreshold-200));
Serial.print("Audio PDM:"); Serial.println(shotConfig.audioThreshold); break;
case 'S': case 's':
shotConfig.minSensors++; if(shotConfig.minSensors>3) shotConfig.minSensors=1;
Serial.print("MinSensors:"); Serial.println(shotConfig.minSensors); break;
case 'H': case 'h':
Serial.println("\n╔═════ COMMANDES ═════╗");
Serial.println("R : Reset pairing");
Serial.println("C : Config");
Serial.println("D : Debug cycle");
Serial.println("T : Test tir");
Serial.println("1/2/3 : Toggle A/G/M");
Serial.println("+/- : Accel ±0.1G");
Serial.println("G/g : Gyro ±10°/s");
Serial.println("M/n : Audio PDM ±200");
Serial.println("S : Min capteurs");
Serial.println("╚═════════════════════╝\n"); break;
default: break;
}
}
// ════════════════════════════════════════════════
// SETUP
// ════════════════════════════════════════════════
void setup() {
Serial.begin(115200);
delay(2000);
Serial.println("\n╔══════════════════════════════════════════╗");
Serial.println("║ XIAO Airsoft Tracker Pro v3.2 ║");
Serial.println("║ Accel + Gyro + Micro PDM natif ║");
Serial.println("╚══════════════════════════════════════════╝\n");
pinMode(LED_RED, OUTPUT);
pinMode(LED_GREEN,OUTPUT);
pinMode(LED_BLUE, OUTPUT);
changeLedState(LED_BOOT);
initDefaultConfig();
loadPairingData();
// IMU
Serial.print("🔧 IMU... ");
if (imu.begin()!=0) {
Serial.println(""); changeLedState(LED_ERROR);
while(1){updateLED();delay(1);}
}
Serial.println("");
// PDM Microphone
Serial.print("🎙️ PDM Micro... ");
PDM.onReceive(onPDMdata);
// Mono, 16000 Hz (standard nRF52840)
if (!PDM.begin(1, 16000)) {
Serial.println("❌ PDM init failed");
// On continue sans micro plutôt que de bloquer
shotConfig.useAudio = false;
Serial.println(" Audio désactivé");
} else {
Serial.println("✅ (16kHz mono)");
PDM.setGain(20); // Gain réduit (0-80) — augmentez si signal trop faible
}
// BLE
Serial.print("📡 BLE... ");
if (!BLE.begin()) {
Serial.println(""); changeLedState(LED_ERROR);
while(1){updateLED();delay(1);}
}
Serial.println("");
delay(500);
BLE.setDeviceName("XIAO Airsoft Pro");
BLE.setLocalName("XIAO Airsoft Pro");
BLE.setConnectionInterval(0x0006, 0x0C80);
BLE.setAdvertisedService(motionService);
motionService.addCharacteristic(imuChar);
motionService.addCharacteristic(shotChar);
motionService.addCharacteristic(debugChar);
motionService.addCharacteristic(configChar);
BLE.addService(motionService);
uint8_t z[12]={0};
imuChar.writeValue(z,12);
shotChar.writeValue((uint8_t)0);
configChar.setEventHandler(BLEWritten, onConfigWrite);
BLE.setEventHandler(BLEConnected, onConnect);
BLE.setEventHandler(BLEDisconnected, onDisconnect);
BLE.advertise();
isAdvertising=true;
Serial.println("✅ Prêt !");
printConfig();
Serial.println("Tapez 'H' pour l'aide");
Serial.println("⚠️ Seuil audio PDM : 0-32767 (ajuster avec M/n)\n");
}
// ════════════════════════════════════════════════
// LOOP
// ════════════════════════════════════════════════
void loop() {
BLE.poll();
updateLED();
handleSerialCommand();
if (!BLE.connected()) return;
unsigned long now = millis();
// ─── Lecture IMU ───
ax = imu.readFloatAccelX();
ay = imu.readFloatAccelY();
az = imu.readFloatAccelZ();
gx = imu.readFloatGyroX() * DEG_TO_RAD;
gy = imu.readFloatGyroY() * DEG_TO_RAD;
gz = imu.readFloatGyroZ() * DEG_TO_RAD;
float accelMag = sqrt(ax*ax + ay*ay + az*az);
float gyroMag = sqrt(gx*gx + gy*gy + gz*gz) * RAD_TO_DEG;
// ─── Lecture PDM ───
// pdmPeak est mis à jour par l'ISR onPDMdata()
uint16_t audioLevel = (uint16_t)pdmPeak;
// Lissage pour affichage debug
audioSmoothed = 0.7f * audioSmoothed + 0.3f * audioLevel;
// ─── Triggers ───
unsigned long n = millis();
if (shotConfig.useAccel && accelMag>=shotConfig.accelThreshold && !accelTrigger) {
accelTrigger=true; accelTriggerTime=n; }
if (shotConfig.useGyro && gyroMag>=shotConfig.gyroThreshold && !gyroTrigger) {
gyroTrigger=true; gyroTriggerTime=n; }
if (shotConfig.useAudio && audioLevel>=shotConfig.audioThreshold && !audioTrigger) {
audioTrigger=true; audioTriggerTime=n; }
// ─── Détection tir ───
if (detectShot()) {
lastShotTime=now;
shotChar.writeValue((uint8_t)1);
shotFlashTimer=now; changeLedState(LED_SHOT_FLASH);
Serial.print("💥 TIR ! A:");Serial.print(accelMag,1);
Serial.print(" G:");Serial.print(gyroMag,0);
Serial.print(" M:");Serial.println(audioLevel);
}
// ─── Debug ───
sendDebugData(accelMag, gyroMag, audioLevel);
printDebugSerial(accelMag, gyroMag, audioLevel);
// ─── IMU BLE 10 Hz ───
if (now-lastSend>=100) {
lastSend=now;
float dt=(now-lastUpdate)/1000.0f; lastUpdate=now;
roll+=gx*dt; pitch+=gy*dt; yaw+=gz*dt;
float ar=atan2f(ay,az);
float ap=atan2f(-ax,sqrtf(ay*ay+az*az));
roll =alpha*roll +(1-alpha)*ar;
pitch=alpha*pitch+(1-alpha)*ap;
uint8_t payload[12];
memcpy(&payload[0],&roll,4);
memcpy(&payload[4],&pitch,4);
memcpy(&payload[8],&yaw,4);
imuChar.writeValue(payload,12);
}
}

View File

@ -0,0 +1,127 @@
"""
XIAO BLE Diagnostic Tool
Scan et test de connexion BLE
"""
import asyncio
from bleak import BleakScanner, BleakClient
# ⚠️ VÉRIFIEZ CES VALEURS ⚠️
DEVICE_ADDRESS = "00:a5:54:89:f1:ec" # ← Votre adresse MAC
DEVICE_NAME = "XIAO Airsoft Pro" # ← Nouveau nom dans le code v2
# UUIDs (version pro)
SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
IMU_CHAR_UUID = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
SHOT_CHAR_UUID = "6E400004-B5A3-F393-E0A9-E50E24DCCA9E"
DEBUG_CHAR_UUID = "6E400005-B5A3-F393-E0A9-E50E24DCCA9E"
CONFIG_CHAR_UUID = "6E400006-B5A3-F393-E0A9-E50E24DCCA9E"
async def scan_all():
"""Scan tous les appareils BLE visibles"""
print("\n🔍 Scan BLE en cours (10 secondes)...")
print(" Assurez-vous que le XIAO est allumé (LED bleue clignotante)\n")
devices = await BleakScanner.discover(timeout=10.0)
if not devices:
print("❌ Aucun appareil BLE trouvé !")
print(" → Vérifiez que le Bluetooth Windows est activé")
print(" → Vérifiez que le XIAO est allumé")
return []
print(f"📡 {len(devices)} appareil(s) trouvé(s) :\n")
for d in sorted(devices, key=lambda x: x.rssi, reverse=True):
marker = " ← XIAO !" if (d.name and "XIAO" in d.name) else ""
marker2 = " ← XIAO !" if d.address.lower() == DEVICE_ADDRESS.lower() else marker
print(f" [{d.rssi:4d} dBm] {d.address} | {d.name or '(sans nom)'}{marker2}")
return devices
async def test_connection():
"""Tente une connexion et liste les services"""
print(f"\n🔌 Tentative de connexion à {DEVICE_ADDRESS}...")
device = await BleakScanner.find_device_by_address(DEVICE_ADDRESS, timeout=10.0)
if not device:
print("❌ Appareil non trouvé à cette adresse !")
print(" → Vérifiez l'adresse MAC dans le Moniteur Série Arduino")
print(" → Ou lancez d'abord le scan pour voir l'adresse réelle")
return
print(f"✅ Appareil trouvé : {device.name} ({device.address})")
try:
async with BleakClient(device, timeout=10.0) as client:
print(f"✅ Connecté !\n")
print("📋 Services et caractéristiques disponibles :\n")
for service in client.services:
print(f" Service : {service.uuid}")
for char in service.characteristics:
props = ", ".join(char.properties)
known = ""
if char.uuid.upper() == IMU_CHAR_UUID.upper():
known = " ← IMU (Roll/Pitch/Yaw)"
elif char.uuid.upper() == SHOT_CHAR_UUID.upper():
known = " ← Shot Event"
elif char.uuid.upper() == DEBUG_CHAR_UUID.upper():
known = " ← Debug Data"
elif char.uuid.upper() == CONFIG_CHAR_UUID.upper():
known = " ← Configuration"
print(f" ├─ {char.uuid} [{props}]{known}")
print()
print("✅ Connexion OK - Toutes les caractéristiques sont accessibles !")
except Exception as e:
print(f"❌ Erreur de connexion : {e}")
print("\n Solutions possibles :")
print(" → Supprimez 'XIAO Airsoft Pro' dans les paramètres Bluetooth Windows")
print(" → Redémarrez le XIAO (bouton RESET)")
print(" → Relancez ce diagnostic")
async def main():
print("╔══════════════════════════════════════╗")
print("║ XIAO BLE Diagnostic Tool ║")
print("╚══════════════════════════════════════╝")
print(f"\nAdresse cible : {DEVICE_ADDRESS}")
print(f"Nom cible : {DEVICE_NAME}")
# Étape 1 : Scan
devices = await scan_all()
# Vérifier si notre appareil est visible
found = any(
d.address.lower() == DEVICE_ADDRESS.lower() or
(d.name and "XIAO" in d.name)
for d in devices
)
if not found:
print(f"\n⚠️ '{DEVICE_NAME}' non trouvé dans le scan !")
print(" → LED du XIAO : quelle couleur voyez-vous ?")
print(" 🔵 Bleu clignotant = OK, cherchez dans la liste ci-dessus")
print(" 🟡 Jaune = Aucun PC enregistré, essayez de vous connecter")
print(" 🔴 Rouge = Erreur hardware")
print(" Éteinte = XIAO pas alimenté\n")
# Chercher un XIAO par nom
xiao_devices = [d for d in devices if d.name and "XIAO" in d.name]
if xiao_devices:
print("🔎 XIAO trouvé avec un nom différent :")
for d in xiao_devices:
print(f" {d.address} | {d.name}")
print(f"\n ⚠️ Mettez à jour DEVICE_ADDRESS dans ce script et dans vos autres scripts !")
return
# Étape 2 : Test connexion
await test_connection()
print("\n📝 Si tout fonctionne ici, mettez à jour l'adresse dans :")
print(f" xiao_unreal_bridge.py → DEVICE_ADDRESS = \"{DEVICE_ADDRESS}\"")
print(f" xiao_calibration_tool.py → DEVICE_ADDRESS = \"{DEVICE_ADDRESS}\"")
if __name__ == "__main__":
asyncio.run(main())

View File

@ -0,0 +1,277 @@
"""
XIAO Airsoft Calibration Tool v3
- Rendu optimisé : set_data() sans redraw complet
- Fenêtre glissante fixe (pas de grossissement des buffers)
- Aucune latence même après des heures
"""
import asyncio
import struct
import threading
from collections import deque
from bleak import BleakClient, BleakScanner
DEVICE_NAME = "XIAO Airsoft Pro"
DEBUG_CHAR_UUID = "6E400005-B5A3-F393-E0A9-E50E24DCCA9E"
SHOT_CHAR_UUID = "6E400004-B5A3-F393-E0A9-E50E24DCCA9E"
# Fenêtre fixe : WINDOW_SIZE points affichés, jamais plus
WINDOW_SIZE = 200 # ~10s à 20Hz — ajustez si besoin
# Buffers circulaires de taille fixe
accel_buf = deque([0.0] * WINDOW_SIZE, maxlen=WINDOW_SIZE)
gyro_buf = deque([0.0] * WINDOW_SIZE, maxlen=WINDOW_SIZE)
audio_buf = deque([0] * WINDOW_SIZE, maxlen=WINDOW_SIZE)
accel_trig = deque([False] * WINDOW_SIZE, maxlen=WINDOW_SIZE)
gyro_trig = deque([False] * WINDOW_SIZE, maxlen=WINDOW_SIZE)
audio_trig = deque([False] * WINDOW_SIZE, maxlen=WINDOW_SIZE)
thresholds = {"accel": 2.5, "gyro": 200.0, "audio": 3000} # PDM : 0-32767
shot_count = 0
ble_status = "🔍 Connexion..."
ble_running = True
audio_max_global = 1000 # Tracks le max absolu jamais vu
import numpy as np
X = np.arange(WINDOW_SIZE) # axe X fixe, ne change jamais
# ─── BLE ────────────────────────────────────────────────
def debug_callback(sender, data):
global audio_max_global
if len(data) < 14:
return
accel_buf.append( struct.unpack('<f', data[1:5])[0] )
gyro_buf.append( struct.unpack('<f', data[5:9])[0] )
val = struct.unpack('<H', data[9:11])[0]
audio_buf.append(val)
if val > audio_max_global:
audio_max_global = val
accel_trig.append(bool(data[11]))
gyro_trig.append( bool(data[12]))
audio_trig.append(bool(data[13]))
def shot_callback(sender, data):
global shot_count
if data[0] == 1:
shot_count += 1
async def find_device():
found = None
def cb(device, adv):
nonlocal found
if device.name and DEVICE_NAME.lower() in device.name.lower():
found = device
scanner = BleakScanner(cb)
await scanner.start()
for _ in range(20):
if found: break
await asyncio.sleep(0.5)
await scanner.stop()
return found
async def ble_loop():
global ble_status, ble_running
while ble_running:
try:
ble_status = f"🔍 Recherche '{DEVICE_NAME}'..."
device = await find_device()
if not device:
ble_status = "⚠️ XIAO non trouvé — réessai..."
await asyncio.sleep(5)
continue
ble_status = f"📡 Connexion..."
async with BleakClient(device, timeout=15.0) as client:
await client.start_notify(DEBUG_CHAR_UUID, debug_callback)
await client.start_notify(SHOT_CHAR_UUID, shot_callback)
ble_status = f"{device.name} ({device.address})"
while client.is_connected and ble_running:
await asyncio.sleep(0.5)
ble_status = "❌ Déconnecté — reconnexion..."
except Exception as e:
ble_status = f"{str(e)[:50]}"
await asyncio.sleep(5)
def run_ble():
asyncio.run(ble_loop())
# ─── MATPLOTLIB optimisé ────────────────────────────────
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from matplotlib.animation import FuncAnimation
BG = '#1a1a2e'
PANEL = '#16213e'
TEXT = '#e0e0e0'
CA = '#4fc3f7' # accel
CG = '#81c784' # gyro
CM = '#ce93d8' # audio
CT = '#ef5350' # trigger / seuil
GRID = '#2a2a4a'
fig = plt.figure(figsize=(13, 9), facecolor=BG)
gs = gridspec.GridSpec(3, 1, hspace=0.5, top=0.92, bottom=0.06)
axes = [fig.add_subplot(gs[i]) for i in range(3)]
for ax in axes:
ax.set_facecolor(PANEL)
ax.tick_params(colors=TEXT, labelsize=8)
for sp in ax.spines.values():
sp.set_edgecolor('#444466')
ax.grid(True, alpha=0.2, color=GRID)
ax.set_xlim(0, WINDOW_SIZE - 1)
axes[0].set_ylabel("G", color=TEXT, fontsize=9)
axes[1].set_ylabel("°/s", color=TEXT, fontsize=9)
axes[2].set_ylabel("Niveau",color=TEXT, fontsize=9)
axes[2].set_xlabel("Échantillons (fenêtre glissante)",
color=TEXT, fontsize=8)
# 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)
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)
# Zones de trigger : rectangles pré-créés (un par point)
# On utilise une collection de spans pour les triggers
from matplotlib.patches import Rectangle
from matplotlib.collections import PatchCollection
# Titres dynamiques
titles = [
axes[0].set_title("", color=TEXT, fontsize=10, fontweight='bold', pad=5),
axes[1].set_title("", color=TEXT, fontsize=10, fontweight='bold', pad=5),
axes[2].set_title("", color=TEXT, fontsize=10, fontweight='bold', pad=5),
]
status_txt = fig.text(0.01, 0.97, "", color=TEXT, fontsize=9, va='top')
help_txt = fig.text(0.99, 0.97,
"Q/A Accel±0.1G W/S Gyro±10°/s E/D Audio±500 R Reset ESC Quitter",
color='#8888aa', fontsize=8, va='top', ha='right')
# Fonds de trigger (spans) — créés une fois, rendus invisibles par défaut
# On dessine juste une image de fond qu'on met à jour
trig_spans_a = [axes[0].axvspan(i, i+1, alpha=0, color=CT) for i in range(0, WINDOW_SIZE, 1)]
trig_spans_g = [axes[1].axvspan(i, i+1, alpha=0, color=CT) for i in range(0, WINDOW_SIZE, 1)]
trig_spans_m = [axes[2].axvspan(i, i+1, alpha=0, color=CT) for i in range(0, WINDOW_SIZE, 1)]
def update_spans(spans, trig_list):
"""Met à jour l'alpha des spans sans en créer de nouveaux"""
tl = list(trig_list)
for i, span in enumerate(spans):
span.set_alpha(0.25 if i < len(tl) and tl[i] else 0)
def update(frame):
# Snapshot des buffers (rapide)
a = np.array(accel_buf)
g = np.array(gyro_buf)
m = np.array(audio_buf)
# Mise à jour des données des lignes
line_a.set_ydata(a)
line_g.set_ydata(g)
line_m.set_ydata(m)
# Mise à jour des seuils
thr_a.set_ydata([thresholds["accel"], thresholds["accel"]])
thr_g.set_ydata([thresholds["gyro"], thresholds["gyro"]])
thr_m.set_ydata([thresholds["audio"], thresholds["audio"]])
# Ylim adaptatif
axes[0].set_ylim(0, max(thresholds["accel"] * 1.8, a.max() * 1.2, 2.0))
axes[1].set_ylim(0, max(thresholds["gyro"] * 1.8, g.max() * 1.2, 100))
# ylim audio basé sur le max GLOBAL jamais vu (jamais réduit automatiquement)
axes[2].set_ylim(0, max(audio_max_global * 1.2, thresholds["audio"] * 2.0, 1000))
# Triggers
update_spans(trig_spans_a, accel_trig)
update_spans(trig_spans_g, gyro_trig)
update_spans(trig_spans_m, audio_trig)
# Titres avec valeurs courantes
titles[0].set_text(
f"Accéléromètre "
f"val: {a[-1]:.2f} G "
f"seuil: {thresholds['accel']:.1f} G "
f"[Q=+0.1 A=-0.1]")
titles[1].set_text(
f"Gyroscope "
f"val: {g[-1]:.0f} °/s "
f"seuil: {thresholds['gyro']:.0f} °/s "
f"[W=+10 S=-10]")
titles[2].set_text(
f"Microphone PDM "
f"val: {m[-1]:.0f} "
f"seuil: {thresholds['audio']} "
f"[E=+500 D=-500]")
status_txt.set_text(
f"{ble_status} | 💥 Tirs détectés : {shot_count}")
return (line_a, line_g, line_m,
thr_a, thr_g, thr_m,
*titles, status_txt)
def on_key(event):
k = event.key
if k == 'q': thresholds["accel"] = round(thresholds["accel"] + 0.1, 1)
elif k == 'a': thresholds["accel"] = round(max(0.3, thresholds["accel"] - 0.1), 1)
elif k == 'w': thresholds["gyro"] = thresholds["gyro"] + 10
elif k == 's': thresholds["gyro"] = max(20, thresholds["gyro"] - 10)
elif k == 'e': thresholds["audio"] = thresholds["audio"] + 500
elif k == 'd': thresholds["audio"] = max(200, thresholds["audio"] - 500)
elif k == 'r':
global audio_max_global
audio_max_global = 1000
for b in (accel_buf, gyro_buf, audio_buf,
accel_trig, gyro_trig, audio_trig):
b.clear()
b.extend([0] * WINDOW_SIZE)
print("Courbes réinitialisées")
elif k == 'escape':
global ble_running
ble_running = False
plt.close('all')
# Affichage console
if k in ('q','a'): print(f"Accel seuil → {thresholds['accel']:.1f} G")
elif k in ('w','s'): print(f"Gyro seuil → {thresholds['gyro']:.0f} °/s")
elif k in ('e','d'): print(f"Audio seuil → {thresholds['audio']}")
def on_close(event):
global ble_running
ble_running = False
fig.canvas.mpl_connect('key_press_event', on_key)
fig.canvas.mpl_connect('close_event', on_close)
def main():
print("╔══════════════════════════════════════════════╗")
print("║ XIAO Airsoft Calibration Tool v3 ║")
print("║ Rendu optimisé — aucune latence ║")
print("╚══════════════════════════════════════════════╝")
print(f"\n🎯 Cible : '{DEVICE_NAME}'")
print("⚠️ Tapez 'D' dans le Moniteur Série Arduino")
print(" jusqu'à voir 'Debug : FULL'\n")
ble_thread = threading.Thread(target=run_ble, daemon=True)
ble_thread.start()
anim = FuncAnimation(
fig, update,
interval=80, # ~12 fps, largement suffisant
blit=False, # blit=True cause des bugs de spans
cache_frame_data=False
)
plt.show()
global ble_running
ble_running = False
if __name__ == "__main__":
main()