// ───────────────────────────────────────────────────────────────────────────── // ESP32-C3 SuperMini — I2C Slave + WS2812B Ring + DFR0299 MP3 // ───────────────────────────────────────────────────────────────────────────── // I2C Slave : SDA=GPIO8, SCL=GPIO9, addr=0x42 // Ring : GPIO6, 120 LEDs (ring1=32, ring2=40, ring3=48) // DFR0299 : Serial (GPIO20=RX, GPIO21=TX) // SD card : dossier MP3/, fichiers nommés 0001.mp3 .. 9999.mp3 // Ring layout : ring1=32, ring2=40, ring3=48 // ───────────────────────────────────────────────────────────────────────────── #include #include #include // ── Pins ───────────────────────────────────────────────────────────────────── #define PIN_SDA 8 #define PIN_SCL 9 #define I2C_ADDR 0x42 #define PIN_RING 6 #define NUM_LEDS 120 // ── Ring layout ─────────────────────────────────────────────────────────────── #define RING1_START 0 // 32 LEDs #define RING2_START 32 // 40 LEDs #define RING3_START 72 // 48 LEDs // Ring milieu CO2 : LEDs 32-71, éteindre 32-35 et 68-71 #define CO2_START 36 #define CO2_LEDS 32 // LEDs actives pour CO2 // ── Commandes I2C ───────────────────────────────────────────────────────────── #define CMD_CO2 0x01 #define CMD_GYRO 0x02 #define CMD_BRIGHTNESS 0x03 #define CMD_VOLUME 0x04 #define CMD_PLAY 0x05 // ── Numéros de fichiers MP3 (dossier MP3/, nommés XXXX.mp3) ────────────────── #define SND_STARTUP 1004 #define SND_VERY_HIGH 1001 #define SND_HIGH 1002 #define SND_MEDIUM 1003 #define SND_LOW 1005 // à adapter selon vos fichiers #define SND_INFO 1006 #define SND_CO2_400 1101 #define SND_CO2_600 1102 #define SND_CO2_1000 1103 #define SND_CO2_1500 1104 // ── Couleurs criticité ──────────────────────────────────────────────────────── // index : 0=non défini, 1=P1 RED, 2=P2 ORANGE, 3=P3 YELLOW, 4=P4 GREEN, 5=P5 BLUE const uint8_t CRIT_R[] = {0, 255, 255, 255, 0, 0}; const uint8_t CRIT_G[] = {0, 0, 165, 255, 128, 0}; const uint8_t CRIT_B[] = {0, 0, 0, 0, 0, 255}; // ── Objets ──────────────────────────────────────────────────────────────────── Adafruit_NeoPixel ring(NUM_LEDS, PIN_RING, NEO_GRB + NEO_KHZ800); DFRobotDFPlayerMini mp3; // ── État ────────────────────────────────────────────────────────────────────── #define MODE_CO2 1 #define MODE_GYRO 2 int currentMode = MODE_CO2; uint8_t currentCrit = 1; uint16_t currentCO2 = 400; uint8_t brightness = 20; uint8_t mp3Volume = 25; // Gyrophare #define TRAIL_LEN 6 // longueur de la traînée float gyroAngle = 0.0f; // angle en degrés [0..360[ unsigned long lastGyroTime = 0; const float GYRO_SPEED = 2.0f; // tours/sec // Standby MP3 unsigned long lastPlayTime = 0; bool waitingStandby = false; const unsigned long STANDBY_DELAY = 5000; // ── Buffer I2C ──────────────────────────────────────────────────────────────── volatile uint8_t i2cBuf[4]; volatile uint8_t i2cLen = 0; volatile bool i2cReady = false; // ── MP3 helpers ─────────────────────────────────────────────────────────────── void mp3_setVolume(uint8_t vol) { mp3.start(); delay(200); mp3.volume(vol); delay(50); mp3.sleep(); } void mp3_play(int fileNum) { mp3.start(); delay(200); mp3.volume(mp3Volume); delay(50); mp3.playMp3Folder(fileNum); lastPlayTime = millis(); waitingStandby = true; } // Convertit le numéro de fichier reçu via I2C en numéro MP3 réel int toMp3File(uint8_t file) { switch (file) { case 0: return SND_STARTUP; case 1: return SND_VERY_HIGH; case 2: return SND_HIGH; case 3: return SND_MEDIUM; case 4: return SND_LOW; case 5: return SND_INFO; case 101: return SND_CO2_400; case 102: return SND_CO2_600; case 103: return SND_CO2_1000; case 104: return SND_CO2_1500; default: return SND_STARTUP; } } // ── I2C callbacks ───────────────────────────────────────────────────────────── void onReceive(int numBytes) { i2cLen = 0; while (Wire.available() && i2cLen < 4) { i2cBuf[i2cLen++] = Wire.read(); } i2cReady = true; } // ── Ring : mode CO2 ─────────────────────────────────────────────────────────── void ring_showCO2(uint16_t co2) { ring.clear(); if (co2 < 400) co2 = 400; if (co2 > 2000) co2 = 2000; uint8_t ledsToLight = map(co2, 400, 2000, 0, CO2_LEDS); for (uint8_t i = 0; i < ledsToLight; i++) { float ratio = (float)i / (float)(CO2_LEDS - 1); uint8_t r, g; if (ratio < 0.5f) { r = (uint8_t)(255 * ratio * 2.0f); g = 255; } else { r = 255; g = (uint8_t)(255 * (1.0f - (ratio - 0.5f) * 2.0f)); } ring.setPixelColor(CO2_START + i, ring.Color(r, g, 0)); } ring.show(); } // ── Ring : mode gyrophare ───────────────────────────────────────────────────── void ring_drawSpots(int ringStart, int ringSize) { uint8_t r = CRIT_R[currentCrit]; uint8_t g = CRIT_G[currentCrit]; uint8_t b = CRIT_B[currentCrit]; // 2 spots espacés de 180° float spotAngles[2] = { gyroAngle, fmod(gyroAngle + 180.0f, 360.0f) }; for (int src = 0; src < 2; src++) { // Position de la tête du spot sur ce ring float headF = (spotAngles[src] / 360.0f) * ringSize; int head = (int)headF % ringSize; // Tête + traînée for (int t = 0; t < TRAIL_LEN; t++) { int ledIdx = (head - t + ringSize) % ringSize; // Intensité décroissante quadratique float intensity = (float)(TRAIL_LEN - t) / (float)TRAIL_LEN; intensity = intensity * intensity; uint8_t cr = (uint8_t)(r * intensity); uint8_t cg = (uint8_t)(g * intensity); uint8_t cb = (uint8_t)(b * intensity); // Additionner avec couleur existante (2 spots peuvent se croiser) uint32_t existing = ring.getPixelColor(ringStart + ledIdx); cr = (uint8_t)min(255, (int)cr + (int)((existing >> 16) & 0xFF)); cg = (uint8_t)min(255, (int)cg + (int)((existing >> 8) & 0xFF)); cb = (uint8_t)min(255, (int)cb + (int)( existing & 0xFF)); ring.setPixelColor(ringStart + ledIdx, ring.Color(cr, cg, cb)); } } } void ring_updateGyro() { // Avancer l'angle selon le temps écoulé unsigned long now = millis(); float elapsed = (now - lastGyroTime) / 1000.0f; lastGyroTime = now; gyroAngle += elapsed * GYRO_SPEED * 360.0f; if (gyroAngle >= 360.0f) gyroAngle -= 360.0f; ring.clear(); ring_drawSpots(RING1_START, 32); ring_drawSpots(RING2_START, 40); ring_drawSpots(RING3_START, 48); ring.show(); } // ── Traitement commandes I2C ────────────────────────────────────────────────── void processI2C() { if (!i2cReady) return; i2cReady = false; uint8_t cmd = i2cBuf[0]; switch (cmd) { case CMD_CO2: { uint16_t co2 = ((uint16_t)i2cBuf[1] << 8) | i2cBuf[2]; currentCO2 = co2; currentMode = MODE_CO2; ring_showCO2(co2); break; } case CMD_GYRO: { uint8_t crit = i2cBuf[1]; if (crit >= 1 && crit <= 5) currentCrit = crit; currentMode = MODE_GYRO; break; } case CMD_BRIGHTNESS: { brightness = i2cBuf[1]; ring.setBrightness(brightness); ring.show(); break; } case CMD_VOLUME: { mp3Volume = i2cBuf[1]; if (mp3Volume > 30) mp3Volume = 30; mp3_setVolume(mp3Volume); break; } case CMD_PLAY: { mp3_play(toMp3File(i2cBuf[1])); break; } } } // ── Setup ───────────────────────────────────────────────────────────────────── void setup() { // I2C slave Wire.begin(I2C_ADDR, PIN_SDA, PIN_SCL); Wire.onReceive(onReceive); // Ring — séquence de démarrage (5 couleurs de criticité) ring.begin(); ring.setBrightness(brightness); ring.clear(); ring.show(); // Séquence de démarrage : gyrophare avec changement de couleur 5→1→5 const int seq[] = {5, 4, 3, 2, 1, 1, 2, 3, 4, 5}; lastGyroTime = millis(); unsigned long seqTimer = millis(); int seqIdx = 0; currentMode = MODE_GYRO; while (seqIdx < 10) { currentCrit = seq[seqIdx]; ring_updateGyro(); delay(20); // ~50 fps if (millis() - seqTimer >= 400) { seqIdx++; seqTimer = millis(); } } currentMode = MODE_CO2; ring.clear(); ring.show(); // MP3 Serial.begin(9600); delay(2000); mp3.begin(Serial, true); delay(500); mp3.outputDevice(DFPLAYER_DEVICE_SD); delay(200); mp3.volume(mp3Volume); delay(200); mp3.playMp3Folder(SND_STARTUP); delay(11000); // attendre fin du son de démarrage mp3.sleep(); // standby } // ── Loop ────────────────────────────────────────────────────────────────────── void loop() { processI2C(); // Repasser en standby après la fin du son if (waitingStandby && millis() - lastPlayTime >= STANDBY_DELAY) { waitingStandby = false; mp3.sleep(); } if (currentMode == MODE_GYRO) { ring_updateGyro(); delay(20); // ~50 fps } }