Table of Contents
Hardware
Composants externe
La carte Arduino Q ne suffit pas pour afficher les alertes, quelques composant sont ajoutés:
- 3 affichages TM1637 à 4 digits pour l'affichage des valeurs du capteur CO2
- 2 affichages de 32x8x8 pixels (MAX7219) mis en série
Modification affichages MAX7219
Un affichage ressemble à ceci
Chaque module est câblé ainsi
Si on place les 2 modules en série, il y a un problème car on peut voir que les connexions par défaut (en rouge) sont en trop. Et il manque la liaison entre le dernier module de la ligne du bas et le premier de la ligne du haut.
Ce qui est voulu c'est ceci:
Il faut donc couper cette piste au dos du module:
Et relier souder les 2 modules ensembles
ESP32-C3 SuperMini
Aucune librairie pour commander les WS2812B depuis un Arduino Q est disponible pour l'instant. Pour pouvoir utiliser ces leds, j'ai ajouté un ESP32-C3 Supermini sur le port I2C de l'Arduino Q afin de piloter les leds “à distance”.
Le sketch de cet ESP32-C3 SuperMini est le suivant:
- sketch.ino
// ───────────────────────────────────────────────────────────────────────────── // 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 <Wire.h> #include <Adafruit_NeoPixel.h> #include <DFRobotDFPlayerMini.h> // ── 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 } }
DFR0299
Le DFR0299 ou “DFPlayer - Mini MP3 Player” est un petit lecteur de MP3. Les fichiers doivent être placés sur une carte SD et sont lus à la demande.
Les fichiers utilisés sont:
| Fichier | Événement |
|---|---|
| Démarrage | |
| Ticket VERY_HIGH | |
| Ticket HIGH | |
| Ticket MEDIUM | |
| Ticket LOW | |
| Ticket INFORMATION | |
| CO₂ 400-600 | |
| CO₂ 600-1000 | |
| CO₂ 1000-1500 | |
| CO₂ 1500-2000 |


