Add arduino and python code
This commit is contained in:
parent
00f44bf81b
commit
d30569e6a1
537
Arduino/xiao_airsoft_pro/xiao_airsoft_pro.ino
Normal file
537
Arduino/xiao_airsoft_pro/xiao_airsoft_pro.ino
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
127
Python/Xiao_BLE_tools/xiao_ble_diagnostic.py
Normal file
127
Python/Xiao_BLE_tools/xiao_ble_diagnostic.py
Normal 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())
|
||||||
277
Python/Xiao_BLE_tools/xiao_calibration_tool.py
Normal file
277
Python/Xiao_BLE_tools/xiao_calibration_tool.py
Normal 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()
|
||||||
Loading…
x
Reference in New Issue
Block a user