Vigi LED
Non connecté
Web Bluetooth n'est pas supporté dans ce navigateur.
Utilisez Chrome ou Edge sur Android, macOS, ou ChromeOS.
Firefox et Safari ne supportent pas Web Bluetooth.
Mode d'affichage
Couleurs de respiration
Actions
🌙 Activation nocturne
🌙 Activer les LEDs uniquement la nuit
Mode nocturne désactivé
Arduino Code XIAO ESP32C6 à flasher
BLE Service UUID
6E400001…
TX Char (Write)
6E400002…
RX Char (Notify)
6E400003…
LED Pin
D1 (GPIO 1)
/*
 * ═══════════════════════════════════════════════════════════
 *  XIAO ESP32C6 — Contrôleur LED NeoPixel avec BLE 5.0
 *  Compatible avec la page web LED Control
 * ═══════════════════════════════════════════════════════════
 *
 *  Bibliothèques requises (Gestionnaire de bibliothèques Arduino) :
 *    - Adafruit NeoPixel
 *    - ESP32 Arduino core (Expressif)
 *
 *  Câblage :
 *    LEDs NeoPixel  → D1 (GPIO1)
 *    Bouton MODE    → D2 (GPIO2) + GND
 *    Bouton BRIGHT  → D3 (GPIO3) + GND
 *    Batterie ADC   → A0 (GPIO0)
 *    Alimentation   → 3.3V ou 5V selon votre strip
 *
 *  Commandes BLE reçues :
 *    MODE:n          → mode 0-7, 10-13
 *                      0-3 : Respiration couleur pastel
 *                      4   : Arc-en-ciel
 *                      5   : Drapeau français
 *                      6   : Éteint
 *                      7   : Vague douce (bleu, cascade lente)
 *                      10  : Police (urgence)
 *                      11  : Veille pulsée (LED centrale seule)
 *                      12  : Fixe stable (couleur unie)
 *                      13  : K-2000 (balayage rouge avec traînée)
 *    BRI:nnn         → luminosité 80-255
 *    COL:n:r:g:b     → couleur pastel n (0-3), r/g/b 0-255
 *    OFF             → éteindre (mode 6)
 *    RESET           → reset aux valeurs par défaut
 *    STATUS          → répond avec l'état actuel
 *
 *  Réponses envoyées :
 *    OK:MODE:n
 *    OK:BRI:nnn
 *    OK:COL:n
 *    STATUS:MODE=n,BRI=nnn,...
 *    ERR:UNKNOWN:cmd
 * ═══════════════════════════════════════════════════════════
 */

#include <Adafruit_NeoPixel.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

// ─── Broches ──────────────────────────────────────────────
#define PIN_NEOPIXEL   1       // D1 sur XIAO ESP32C6
#define NUM_LEDS       28
#define LEDS_ACTIVE    9

#define BTN_MODE_PIN   2       // D2
#define BTN_BRIGHT_PIN 3       // D3
#define BAT_PIN        A0

// ─── BLE NUS (Nordic UART Service) ────────────────────────
#define SERVICE_UUID        "6E400001-B5BA-F393-E0A9-E50E24DCCA9E"
#define CHAR_UUID_RX        "6E400002-B5BA-F393-E0A9-E50E24DCCA9E"  // write (web → esp)
#define CHAR_UUID_TX        "6E400003-B5BA-F393-E0A9-E50E24DCCA9E"  // notify (esp → web)

// ─── Globals ──────────────────────────────────────────────
Adafruit_NeoPixel strip(NUM_LEDS, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800);

BLEServer*         pServer   = nullptr;
BLECharacteristic* pTxChar   = nullptr;
bool               bleConnected = false;
bool               oldConnected = false;

uint8_t  currentMode = 0;
uint8_t  brightness  = 180;
bool     brightUp    = true;

// Couleurs pastel modifiables
uint8_t pastelR[4] = {  4,  35, 100, 180 };
uint8_t pastelG[4] = { 85,  80,  60,  40 };
uint8_t pastelB[4] = { 42, 160, 120,  20 };

unsigned long tempsAppuiMode   = 0;
unsigned long tempsAppuiBright = 0;
bool boutonModeAppuye   = false;
bool boutonBrightAppuye = false;

// Buffer BLE entrant
String bleBuffer = "";
bool   cmdPending = false;
String pendingCmd = "";

// ─── BLE Callbacks ────────────────────────────────────────
class ServerCallbacks : public BLEServerCallbacks {
  void onConnect(BLEServer* s) override {
    bleConnected = true;
    Serial.println("BLE: client connecté");
  }
  void onDisconnect(BLEServer* s) override {
    bleConnected = false;
    Serial.println("BLE: client déconnecté");
  }
};

class RxCallbacks : public BLECharacteristicCallbacks {
  void onWrite(BLECharacteristic* c) override {
    String val = c->getValue().c_str();
    val.trim();
    if (val.length() > 0) {
      pendingCmd = val;
      cmdPending = true;
    }
  }
};

// ─── BLE Send ─────────────────────────────────────────────
void bleSend(String msg) {
  if (pTxChar && bleConnected) {
    pTxChar->setValue(msg.c_str());
    pTxChar->notify();
  }
  Serial.println("BLE OUT: " + msg);
}

// ─── Parser de commandes ──────────────────────────────────
void parseCmd(String cmd) {
  cmd.trim();
  Serial.println("CMD: " + cmd);

  if (cmd.startsWith("MODE:")) {
    int m = cmd.substring(5).toInt();
    if (m >= 0 && (m <= 12 || m == 10)) {
      currentMode = m;
      bleSend("OK:MODE:" + String(currentMode));
    }

  } else if (cmd.startsWith("BRI:")) {
    int b = cmd.substring(4).toInt();
    if (b >= 80 && b <= 255) {
      brightness = b;
      strip.setBrightness(brightness);
      bleSend("OK:BRI:" + String(brightness));
    }

  } else if (cmd.startsWith("COL:")) {
    // Format: COL:n:r:g:b
    int p1 = cmd.indexOf(':', 4);
    int p2 = cmd.indexOf(':', p1 + 1);
    int p3 = cmd.indexOf(':', p2 + 1);
    if (p1 > 0 && p2 > 0 && p3 > 0) {
      int idx = cmd.substring(4,     p1).toInt();
      int r   = cmd.substring(p1+1,  p2).toInt();
      int g   = cmd.substring(p2+1,  p3).toInt();
      int b   = cmd.substring(p3+1).toInt();
      if (idx >= 0 && idx <= 3) {
        pastelR[idx] = constrain(r, 0, 255);
        pastelG[idx] = constrain(g, 0, 255);
        pastelB[idx] = constrain(b, 0, 255);
        bleSend("OK:COL:" + String(idx));
      }
    }

  } else if (cmd == "OFF") {
    currentMode = 6;
    strip.clear(); strip.show();
    bleSend("OK:OFF");

  } else if (cmd == "RESET") {
    currentMode = 0;
    brightness  = 180;
    pastelR[0] =   4; pastelG[0] = 85;  pastelB[0] = 42;
    pastelR[1] =  35; pastelG[1] = 80;  pastelB[1] = 160;
    pastelR[2] = 100; pastelG[2] = 60;  pastelB[2] = 120;
    pastelR[3] = 180; pastelG[3] = 40;  pastelB[3] = 20;
    strip.setBrightness(brightness);
    bleSend("OK:RESET");

  } else if (cmd == "STATUS") {
    String s = "STATUS:MODE=" + String(currentMode)
             + ",BRI=" + String(brightness)
             + ",R0=" + String(pastelR[0]) + ",G0=" + String(pastelG[0]) + ",B0=" + String(pastelB[0])
             + ",R1=" + String(pastelR[1]) + ",G1=" + String(pastelG[1]) + ",B1=" + String(pastelB[1])
             + ",R2=" + String(pastelR[2]) + ",G2=" + String(pastelG[2]) + ",B2=" + String(pastelB[2])
             + ",R3=" + String(pastelR[3]) + ",G3=" + String(pastelG[3]) + ",B3=" + String(pastelB[3]);
    bleSend(s);

  } else {
    bleSend("ERR:UNKNOWN:" + cmd);
  }
}

// ─── Batterie ─────────────────────────────────────────────
bool batterieCritique() {
  long sum = 0;
  for (int i = 0; i < 16; i++) { sum += analogRead(BAT_PIN); delay(1); }
  return (sum >> 4) < 325;
}

void alerteEtExtinction() {
  for (int i = 0; i < 6; i++) {
    strip.fill(0xFF0000); strip.show(); delay(250);
    strip.clear();        strip.show(); delay(250);
  }
  esp_deep_sleep_start();
}

// ─── Effets LED ───────────────────────────────────────────
void feedbackLimite(bool estMax) {
  uint32_t c = estMax ? 0x00FF00 : 0x0000FF;
  for (int i = 0; i < 3; i++) {
    strip.fill(c, 0, LEDS_ACTIVE); strip.show(); delay(60);
    strip.clear(); strip.show(); delay(40);
  }
}

void k2000(uint32_t c) {
  for (int r = 0; r < 2; r++) {
    for (int i = 0; i < LEDS_ACTIVE; i++) {
      strip.clear(); strip.setPixelColor(i, c);
      if (i > 0) strip.setPixelColor(i-1, c >> 1);
      strip.show(); delay(35);
    }
    for (int i = LEDS_ACTIVE-1; i >= 0; i--) {
      strip.clear(); strip.setPixelColor(i, c);
      if (i < LEDS_ACTIVE-1) strip.setPixelColor(i+1, c >> 1);
      strip.show(); delay(35);
    }
  }
}

void policeMode() {
  static uint8_t step = 0;
  static unsigned long last = 0;
  if (millis() - last < 50) return;
  last = millis();
  strip.clear();
  if (step < 8)       { if (step % 2 == 0) strip.fill(0xFF0000); }
  else if (step < 16) { if ((step-8) % 2 == 0) strip.fill(0x0000FF); }
  strip.show();
  if (++step >= 18) { step = 0; delay(380); }
}

void drapeauFrancais() {
  static float phase = 0.0f;
  phase += 0.07f;
  if (phase > TWO_PI) phase -= TWO_PI;
  for (int i = 0; i < LEDS_ACTIVE; i++) {
    float pos  = (float)i / (LEDS_ACTIVE - 1);
    float wave = sinf(pos * PI * 3.5f + phase) * 0.35f + 0.65f;
    uint8_t w  = (uint8_t)(255 * wave);
    uint32_t col;
    if      (pos < 0.333f) col = strip.Color(0, 0, w);
    else if (pos < 0.666f) col = strip.Color(w, w, w);
    else                   col = strip.Color(w, 0, 0);
    strip.setPixelColor(i, col);
  }
  strip.show(); delay(30);
}

void respiration() {
  static unsigned long start = millis();
  unsigned long t = millis() - start;
  uint8_t bri = 10;

  if      (t < 2500)  bri = map(t, 0, 2500, 10, 255);
  else if (t < 8500)  bri = 255;
  else if (t < 11000) bri = map(t - 8500, 0, 2500, 255, 10);
  else { start = millis(); return; }

  uint8_t idx = (currentMode <= 3) ? currentMode : 0;
  uint8_t r = (uint32_t)pastelR[idx] * bri / 255 * brightness / 255;
  uint8_t g = (uint32_t)pastelG[idx] * bri / 255 * brightness / 255;
  uint8_t b = (uint32_t)pastelB[idx] * bri / 255 * brightness / 255;
  strip.fill(strip.Color(r, g, b), 0, LEDS_ACTIVE);
  strip.show();
}

void rainbow() {
  static uint16_t j = 0;
  for (uint8_t i = 0; i < LEDS_ACTIVE; i++) {
    strip.setPixelColor(i, strip.ColorHSV((j + i * 8192UL) % 65536, 255, brightness));
  }
  strip.show();
  j = (j + 256) % 65536;
  delay(20);
}

// ─── Nouveaux effets ──────────────────────────────────────

// Mode 7 : Vague douce bleue
void vagueFluide() {
  static unsigned long t0 = millis();
  float t = (millis() - t0) / 1000.0f;
  for (int i = 0; i < LEDS_ACTIVE; i++) {
    float phase = ((float)i / (LEDS_ACTIVE-1)) * TWO_PI - t * 0.8f;
    float bri = sinf(phase) * 0.4f + 0.6f;
    uint8_t r = (uint8_t)(20  * bri * brightness / 255);
    uint8_t g = (uint8_t)(80  * bri * brightness / 255);
    uint8_t b = (uint8_t)(160 * bri * brightness / 255);
    strip.setPixelColor(i, strip.Color(r, g, b));
  }
  strip.show();
  delay(20);
}

// Mode 13 : K-2000 rouge — balayage avec traînée
void k2000Rouge() {
  static int  pos  = 0;
  static int  dir  = 1;
  static unsigned long last = 0;
  if (millis() - last < 55) return;
  last = millis();
  strip.clear();
  // Traînée dégradée
  for (int i = 0; i < LEDS_ACTIVE; i++) {
    int dist = abs(i - pos);
    uint8_t r = 0;
    if      (dist == 0) r = 255;
    else if (dist == 1) r = 100;
    else if (dist == 2) r = 30;
    strip.setPixelColor(i, strip.Color(r * brightness / 255, 0, 0));
  }
  strip.show();
  pos += dir;
  if (pos >= LEDS_ACTIVE - 1) { pos = LEDS_ACTIVE - 1; dir = -1; }
  if (pos <= 0)                { pos = 0;                dir =  1; }
}

// Mode 11 : Veille pulsée (LED centrale uniquement)
void veilleDiscrete() {
  static unsigned long t0 = millis();
  float t    = (float)((millis() - t0) % 4000) / 4000.0f;
  float bri  = (sinf(t * TWO_PI - HALF_PI) + 1.0f) / 2.0f;
  float sc   = bri * 0.35f + 0.02f;
  strip.clear();
  int mid = LEDS_ACTIVE / 2;
  uint8_t r = (uint8_t)(40  * sc * brightness / 255);
  uint8_t g = (uint8_t)(130 * sc * brightness / 255);
  uint8_t b = (uint8_t)(80  * sc * brightness / 255);
  strip.setPixelColor(mid, strip.Color(r, g, b));
  if (mid > 0) strip.setPixelColor(mid-1, strip.Color(r/4, g/4, b/4));
  if (mid < LEDS_ACTIVE-1) strip.setPixelColor(mid+1, strip.Color(r/4, g/4, b/4));
  strip.show();
  delay(20);
}

// Mode 12 : Couleur fixe stable
void fixeStable() {
  uint8_t r = (uint32_t)pastelR[0] * brightness / 255;
  uint8_t g = (uint32_t)pastelG[0] * brightness / 255;
  uint8_t b = (uint32_t)pastelB[0] * brightness / 255;
  strip.fill(strip.Color(r, g, b), 0, LEDS_ACTIVE);
  strip.show();
  delay(100);
}
void setup() {
  Serial.begin(115200);

  pinMode(BTN_MODE_PIN,   INPUT_PULLUP);
  pinMode(BTN_BRIGHT_PIN, INPUT_PULLUP);

  strip.begin();
  strip.setBrightness(brightness);
  strip.clear();
  strip.show();

  // Initialisation BLE
  BLEDevice::init("LED-Controller");
  pServer = BLEDevice::createServer();
  pServer->setCallbacks(new ServerCallbacks());

  BLEService* pService = pServer->createService(SERVICE_UUID);

  // Caractéristique RX (web écrit ici)
  BLECharacteristic* pRxChar = pService->createCharacteristic(
    CHAR_UUID_RX,
    BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR
  );
  pRxChar->setCallbacks(new RxCallbacks());

  // Caractéristique TX (notif vers web)
  pTxChar = pService->createCharacteristic(
    CHAR_UUID_TX,
    BLECharacteristic::PROPERTY_NOTIFY
  );
  pTxChar->addDescriptor(new BLE2902());

  pService->start();

  BLEAdvertising* adv = BLEDevice::getAdvertising();
  adv->addServiceUUID(SERVICE_UUID);
  adv->setScanResponse(true);
  BLEDevice::startAdvertising();

  Serial.println("BLE démarré: LED-Controller");
}

// ─── LOOP ────────────────────────────────────────────────
void loop() {
  // Reconnexion auto
  if (!bleConnected && oldConnected) {
    delay(300);
    pServer->startAdvertising();
    oldConnected = false;
  }
  if (bleConnected && !oldConnected) {
    oldConnected = true;
  }

  // Traiter commande BLE
  if (cmdPending) {
    cmdPending = false;
    parseCmd(pendingCmd);
  }

  if (batterieCritique()) alerteEtExtinction();

  // Boutons physiques
  bool btnMode   = digitalRead(BTN_MODE_PIN);
  bool btnBright = digitalRead(BTN_BRIGHT_PIN);

  if (btnMode == LOW && !boutonModeAppuye) {
    boutonModeAppuye = true; tempsAppuiMode = millis();
  }
  if (btnMode == LOW && boutonModeAppuye) {
    if (millis() - tempsAppuiMode > 2000) {
      currentMode = (currentMode == 10) ? 0 : 10;
      while (digitalRead(BTN_MODE_PIN) == LOW) delay(10);
      boutonModeAppuye = false;
    }
  }
  if (btnMode == HIGH && boutonModeAppuye) {
    if (millis() - tempsAppuiMode < 1800 && currentMode < 10)
      currentMode = (currentMode + 1) % 7;
    boutonModeAppuye = false;
  }

  if (btnBright == LOW && !boutonBrightAppuye) {
    boutonBrightAppuye = true; tempsAppuiBright = millis();
  }
  if (btnBright == LOW && boutonBrightAppuye) {
    static unsigned long lastAdj = 0;
    if (millis() - tempsAppuiBright > 400 && millis() - lastAdj > 65) {
      lastAdj = millis();
      bool limite = false;
      if (brightUp) {
        if (brightness <= 248) brightness += 7; else { brightness = 255; limite = true; }
      } else {
        if (brightness >= 87)  brightness -= 7; else { brightness = 80;  limite = true; }
      }
      strip.setBrightness(brightness);
      if (limite) feedbackLimite(brightUp);
    }
  }
  if (btnBright == HIGH && boutonBrightAppuye) {
    if (millis() - tempsAppuiBright > 500) brightUp = !brightUp;
    boutonBrightAppuye = false;
  }

  // Rendu LED
  if      (currentMode <= 3)  respiration();
  else if (currentMode == 4)  rainbow();
  else if (currentMode == 5)  drapeauFrancais();
  else if (currentMode == 6)  { strip.clear(); strip.show(); }
  else if (currentMode == 7)  vagueFluide();
  else if (currentMode == 10) policeMode();
  else if (currentMode == 11) veilleDiscrete();
  else if (currentMode == 12) fixeStable();
  else if (currentMode == 13) k2000Rouge();
}