J’ai décrit il y a quelques jours une course de LEDs open source OPEN LED RACE (OLR) qui remporte un succès à chaque présentation sur un salon ou une expo. Pour améliorer la lisibilité de la course pour les spectateurs (les participants sont tellement concentrés sur la course qu’ils ne seront sans doute pas concernés) j’ai utilisé la télémétrie fournie par OLR. Sur le port série on récupère les informations de début de course, du gagnant et les positions de chaque coureur. J’ai décidé de les envoyer sur une matrice 16×16 LEDs RGB pour un affichage bien visible.
Au sommaire :
Une afficheur 16×16 RGB pour la cours de LED Open Led Race
Le matériel
Matériel | Lien | Quantité |
Matrice RGB 16×16 LEDs | https://s.click.aliexpress.com/e/_Dc8CvwD | 1 |
Arduino Nano | https://fr.aliexpress.com/item/32892316815.html | 1 |
Plaque support Arduino Bornier à vis |
https://fr.aliexpress.com/item/32892316815.html | 1 |
Alimentation à découpage 5V/5A | https://s.click.aliexpress.com/e/_DeWGxNX | 1 |
Fils de liaison LEDs (3 pins) | https://s.click.aliexpress.com/e/_DlSsACp | 1 |
On est à moins de 20 euros si on commande sur Aliexpress.
Un souci
Le souci ? l’Arduino Nano Every sur lequel tourne OLR envoie les données de télémétrie sur le port USB. Il faut savoir que l’Arduino Nano Every dispose de 2 ports UART Série, contrairement à l’Arduino Nano de base qui n’a qu’un port série partagé entre TxD, RxD et le port USB comme on peut le voir sur le schéma du nano :
Attention : Au vu de ce schéma il faut bien noter que si vous essayez de mettre à jour le programme d’un Arduino Nano via le port USB pendant que le port série RxD est connecté… ça ne va pas fonctionner ! il faut débrancher RxD avant de mettre à jour via USB !!
Alors que sur l’Arduino Nano Every il existe 2 ports série comme le montre le schéma :
Ici le port série 0 en rouge est attribué à l’USB et le port Tx1 Rx1 est relié aux bornes TxD RxD de la carte… Donc on peut utiliser les deux ports série en même temps… Sauf que la télémétrie de OLR est envoyée sur le port Série 0 de l’Arduino Every, donc sur l’USB à destination du PC et que… il et impossible de connecter 2 Arduino via leur port USB (sans rentrer dans les détails). On doit passer par le port Série 1 sur un Arduino Every. Il faut donc modifier le programme OLR pour qu’il envoie aussi la télémétrie sur le port Série 1… ou utiliser un Arduino Nano classique.
Alors pourquoi un Arduino Nano Every me direz vous ? Parce qu’il peut gérer plusieurs bandeaux de LEDs de 5 mètres (il a plus de mémoire que le Nano : 6Ko au lieu de 2Ko) et que lorsque je monte un projet comme celui-ci j’aime bien anticiper sur d’éventuelles évolutions, même s’il n’y a pas d’évolution 😀 .
L’habillage
Pour habiller la matrice je n’ai pas réinventé le fil à couper l’eau chaude (un coucou à Alain B. qui est dans les étoiles 😉 ) puisque BromB a mis en ligne sur Thingiverse un ensemble qui me convenait parfaitement. Mais en fonction de vos besoins vous trouverez d’autres solutions. Vous pouvez télécharger les fichiers en cliquant sur ce lien.
On trouve cette structure dans laquelle la matrice de LEDs vient se poser, LEDs vers le haut. le plot central maintient la matrice en place et évite qu’elle ne se déforme. Des ouvertures sont prévues pour passer les fils.
Sur la matrice de LEDs, on va poser cette grille. Pour l’imprimer il faudra que votre imprimante 3D soit parfaitement réglée car le diffuseur ne possède qu’une couche ! On commence par une couche de blanc, puis les croisillons qui séparent les LEDs et évitent que la lumière ne diffuse entre les LEDs, sont noirs.
Un logiciel comme CURA permet par exemple de programmer un changement de filament après la première couche. (Extensions > Post traitement > Modifier le G-code).
Une fois cette grille imprimée correctement,on peut fermer l’arrière de la boîte avec cette plaque, également disponible sous forme de fichier STL.
La matrice en images
Sur cette dernière image on a les fils de la matrice de LED. en bas l’alimentation et l’entrée des données Din sur laquelle on envoie les données à afficher. au centre l’alimentation sur laquelle j’envoie le 5v de l’alimentation à découpage. En haut la sortie vers une autre matrice avec la prise 3 pins standard.
On peut alimenter la matrice en 5v entre les bornes 5v et GND de n’importe quel câble mais je préfère séparer et alimenter par le câble central. Pour les données, il faudra relier la pin Din à la sortie D6 de l’Arduino (ou une autre mais il faudra modifier le programme en conséquence) et ne pas oublier de relier une des bornes GND à la masse de l’Arduino pour assurer une masse commune.
Le programme
La documentation OLR en ligne donne toutes les informations sur ce qui est envoyé en télémétrie :
On voit que les données sont envoyées puis qu’un code hexadécimal 0x0A est envoyé, équivalent à \n ou Line Feed.
La phase de la course est indiquée par le code R. Pour nous on guette R4 pour démarrer le compte à rebours et R8 qui indique que la course est finie.
On reçoit ensuite la position de chaque voiture dans le tour en cours, et le nombre de tours réalisés. La charge batterie (100) n’est pas utilisée ici.
A la fin de la course, le vainqueur est désigné, ce qui nous permet d’afficher sa couleur sur toute la matrice de LEDs.
Les problèmes rencontrés
Lors des tests j’ai eu un souci avec la voiture verte. Dans OLR, le programme envoie la télémétrie vers le port USB et l’affichage sur le moniteur série est parfait on a bien la séquence de télémétrie
p1M2, 75,100
qui signifie que la voiture 1 est dans le tour 2 et a parcouru 75% du tour, que sa batterie est à 100%.
C’est le même buffer que j’envoie sur le port Serial 1 de l’Arduino Nano Every. Lorsque j’utilisais le bouton de la voiture rouge, la verte devenait p1,75,100 !
Le M2 ayant disparu, le décodage ne se faisait plus et l’affichage de la verte s’arrêtait.
Finalement j’ai résolu le problème de façon détournée en ajoutant un « T, » avant d’envoyer la télémétrie vers le port Serial1 et… ça a résolu le problème. NE cherchons pas, ça fonctionne et c’est le principal. Il suffit de découper la chaîne reçue différemment et c’est bon !
Le programme
Le programme Arduino est livré tel quel et sans garantie, mais il est copyleft et vous pouvez le modifier comme vous voulez. Il est destiné à tourner sur un Arduino Nano qui reçoit les données de télémétrie sur son port série.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 |
// Affichage du compe à Rebours // et de l'état de la course Open Led Race // On récupère les données envoyées en série par OLR // pour piloter une matrice de 16x16 LEDs #include //https://github.com/FastLED/FastLED #include //https://github.com/Jorgen-VikingGod/LEDMatrix #define DATA_PIN 6 // Sortie sur laquelle le Din de la matrice est connecté #define COLOR_ORDER GRB // Type de code à envoyer pour les WS2812B #define CHIPSET WS2812B // Type de LEDs du bandeau // initial matrix layout (to get led strip index by x/y) #define MATRIX_WIDTH 16 // Taille X de la matrice #define MATRIX_HEIGHT 16 // Taille Y de la matrice #define MATRIX_TYPE HORIZONTAL_ZIGZAG_MATRIX // Organisation des LEDs #define MATRIX_SIZE (MATRIX_WIDTH*MATRIX_HEIGHT) // Nombre total de LEDs #define NUMPIXELS MATRIX_SIZE // Nombre total de LEDs cLEDMatrix<MATRIX_WIDTH, MATRIX_HEIGHT, MATRIX_TYPE> leds; // Créer l'instance LEDMatrix #define pause 300 // Open Led Race envoie les données de télémétrie sur le port série // Plus d'infos : https://gitlab.com/open-led-race/olr-arduino/-/blob/master/doc/OLR_Protocol_Serial.pdf // Variables pour stocker les données de la course int numeroVoiture = 0; int toursComplets = 0; int pourcentageTour = 0; void setup() { Serial.begin(9600); FastLED.addLeds<CHIPSET, DATA_PIN, COLOR_ORDER>(leds[0], leds.Size()).setCorrection(TypicalSMD5050); FastLED.setCorrection(TypicalLEDStrip); FastLED.setBrightness(200); FastLED.clear(true); // on éteint toutes les LEDs } void loop() { if (Serial.available()) { String data = Serial.readStringUntil('\n'); Serial.print("\n"); Serial.print(data); Serial.print("\n"); if (data.startsWith("T")) { // Analyse des données sur les voitures et les tours traiterDonnees(data); } else if (data.startsWith("R4")) { // Compte à rebours avant la course afficherCompteARebours(); // Serial.print("C_A_R\n"); } else if (data.startsWith("w")) { // Course terminée, affichage du gagnant afficherGagnant(data); // Serial.print("WIN\n"); // afficherGagnant(data); } } } void traiterDonnees(String data) { String donnees = ""; donnees = data.substring(2); // Trouver l'index de 'p' dans la chaîne de caractères int indexp = donnees.indexOf("p"); // Trouver l'index de 'M' dans la chaîne de caractères int indexM = donnees.indexOf("M"); // Trouver l'index de la première virgule dans la chaîne de caractères int indexVirgule1 = donnees.indexOf(","); // Trouver l'index de la deuxième virgule dans la chaîne de caractères int indexVirgule2 = donnees.indexOf(",", indexVirgule1 + 1); // Extraire le numéro de voiture de la chaîne de caractères et le convertir en entier int numeroVoiture = donnees.substring(indexp + 1, indexM).toInt(); // Extraire le nombre de tours complets de la chaîne de caractères et le convertir en entier int toursComplets = donnees.substring(indexM + 1, indexVirgule1).toInt(); // Extraire le pourcentage du tour de la chaîne de caractères et le convertir en entier int pourcentageTour = donnees.substring(indexVirgule1 + 1, indexVirgule2).toInt(); Serial.print("Voiture : "); Serial.print(numeroVoiture); Serial.print("\n"); Serial.print("Tours effectués : "); Serial.print(toursComplets); Serial.print("\n"); Serial.print("Tour en cours : "); Serial.print(pourcentageTour); Serial.print("\n"); afficherInformationsCourse(numeroVoiture, toursComplets, pourcentageTour); } void afficherGagnant(String data) { // Afficher le gagnant sur la matrice LED if (data.startsWith("w1")) { // Voiture 1 gagne (rouge) leds.DrawFilledRectangle(0, 0, 15, 15, (CRGB::Red)); } else if (data.startsWith("w2")) { // Voiture 2 gagne (vert) leds.DrawFilledRectangle(0, 0, 15, 15, (CRGB::Green)); } else if (data.startsWith("w3")) { // Voiture 3 gagne (bleu) leds.DrawFilledRectangle(0, 0, 15, 15, (CRGB::Blue)); } else if (data.startsWith("w4")) { // Voiture 4 gagne (vert) leds.DrawFilledRectangle(0, 0, 15, 15, (CRGB::White)); } FastLED.show(); Serial.print(data); Serial.print("Le gagnant est : \n"); delay(pause); } void afficherCompteARebours() { // Afficher 3 leds.DrawFilledRectangle(0, 0, 15, 15, (CRGB::Gray)); leds.DrawFilledRectangle(5, 12, 11, 4, (CRGB::Orange)); leds.DrawLine(6, 13, 10, 13, (CRGB::Black)); leds.DrawPixel(5, 12, (CRGB::Black)); leds.DrawPixel(11, 12, (CRGB::Black)); leds.DrawLine(4, 11, 4, 8, (CRGB::Black)); leds.DrawLine(12, 11, 12, 3, (CRGB::Black)); leds.DrawLine(5, 7, 6, 7, (CRGB::Black)); leds.DrawLine(4, 6, 4, 3, (CRGB::Black)); leds.DrawPixel(5, 2, (CRGB::Black)); leds.DrawPixel(11, 2, (CRGB::Black)); leds.DrawLine(6, 1, 10, 1, (CRGB::Black)); leds.DrawFilledRectangle(5, 9, 6, 8, (CRGB::Maroon)); leds.DrawFilledRectangle(7, 10, 7, 9, (CRGB::Maroon)); leds.DrawPixel(8, 10, (CRGB::Maroon)); leds.DrawPixel(5, 4, (CRGB::Maroon)); leds.DrawLine(5, 3, 11, 3, (CRGB::Maroon)); leds.DrawLine(6, 2, 10, 2, (CRGB::Maroon)); leds.DrawLine(10, 8, 11, 8, (CRGB::Maroon)); leds.DrawLine(7, 6, 8, 6, (CRGB::Maroon)); FastLED.show(); delay(2000); // Afficher 2 leds.DrawFilledRectangle(0, 0, 15, 15, (CRGB::Gray)); leds.DrawFilledRectangle(5, 12, 11, 4, (CRGB::Orange)); leds.DrawLine(6, 13, 10, 13, (CRGB::Black)); leds.DrawPixel(5, 12, (CRGB::Black)); leds.DrawPixel(11, 12, (CRGB::Black)); leds.DrawLine(4, 11, 4, 8, (CRGB::Black)); leds.DrawLine(12, 11, 12, 2, (CRGB::Black)); leds.DrawPixel(5, 7, (CRGB::Black)); leds.DrawLine(4, 6, 4, 2, (CRGB::Black)); leds.DrawLine(5, 1, 11, 1, (CRGB::Black)); leds.DrawFilledRectangle(5, 9, 6, 8, (CRGB::Maroon)); leds.DrawFilledRectangle(7, 10, 7, 9, (CRGB::Maroon)); leds.DrawPixel(8, 10, (CRGB::Maroon)); leds.DrawLine(5, 3, 11, 3, (CRGB::Maroon)); leds.DrawFilledRectangle(5, 3, 11, 2, (CRGB::Maroon)); leds.DrawLine(10, 7, 11, 7, (CRGB::Maroon)); leds.DrawLine(9, 6, 11, 6, (CRGB::Maroon)); FastLED.show(); delay(2000); // Afficher 1 leds.DrawFilledRectangle(0, 0, 15, 15, (CRGB::Gray)); leds.DrawFilledRectangle(7, 12, 11, 4, (CRGB::Orange)); leds.DrawLine(8, 13, 10, 13, (CRGB::Black)); leds.DrawPixel(7, 12, (CRGB::Black)); leds.DrawLine(11, 12, 11, 2, (CRGB::Black)); leds.DrawLine(6, 11, 6, 8, (CRGB::Black)); leds.DrawLine(8, 1, 10, 1, (CRGB::Black)); leds.DrawLine(7, 7, 7, 2, (CRGB::Black)); leds.DrawFilledRectangle(8, 3, 10, 2, (CRGB::Maroon)); leds.DrawLine(7, 9, 7, 8, (CRGB::Maroon)); FastLED.show(); delay(2000); // Allumer GO! en vert leds.DrawFilledRectangle(0, 0, 15, 15, (CRGB::Yellow)); leds.DrawFilledRectangle(1, 11, 2, 3, (CRGB::Black)); leds.DrawLine(2, 12, 5, 12, (CRGB::Black)); leds.DrawLine(5, 11, 6, 11, (CRGB::Black)); leds.DrawLine(2, 12, 5, 12, (CRGB::Black)); leds.DrawLine(5, 11, 6, 11, (CRGB::Black)); leds.DrawFilledRectangle(5, 6, 6, 3, (CRGB::Black)); leds.DrawLine(3, 2, 4, 2, (CRGB::Black)); leds.DrawPixel(6, 2, (CRGB::Black)); leds.DrawPixel(4, 6, (CRGB::Black)); leds.DrawFilledRectangle(8, 11, 9, 3, (CRGB::Black)); leds.DrawFilledRectangle(11, 11, 12, 3, (CRGB::Black)); leds.DrawLine(9, 12, 11, 12, (CRGB::Black)); leds.DrawLine(9, 2, 11, 2, (CRGB::Black)); leds.DrawLine(14, 12, 14, 4, (CRGB::Black)); leds.DrawPixel(14, 2, (CRGB::Black)); FastLED.show(); delay(1000); FastLED.clear(true); // on éteint toutes les LEDs } void afficherInformationsCourse(int numeroVoiture, int toursComplets, int pourcentageTour) { // Afficher les informations sur la matrice LED // Déterminer la couleur de la voiture en fonction du numéro // position y du rectangle représentant la voiture // 1 = Rouge, 2 = Vert, 3 = Bleu, 4 = Blanc CRGB color; // Couleur du tour en cours CRGB color2; // Couleur du nombre de tours finis int y=0; // Déterminer le nombre de LED à allumer en fonction du pourcentage du tour // on allume les 12 premières LED pour 100% du tour int n = map(pourcentageTour, 0, 100, 0, 12); Serial.print("n vaut : "); Serial.print(n); Serial.print("\n"); // Traitement pour chaque voiture switch (numeroVoiture) { // Voiture Rouge case 1: color = CRGB::DarkRed; color2 = CRGB::Red; y = 12; break; // Voiture Verte case 2: color = CRGB::DarkGreen; color2 = CRGB::Green; y = 8; break; // Voiture Bleue case 3: color = CRGB::DarkBlue; color2 = CRGB::Blue; y = 4; break; // Voiture Blanche case 4: color = CRGB::Grey; color2 = CRGB::White; y = 0; break; default: color = CRGB::Black; break; } // Au départ on reçoit p1M1,0,100 // Quand on est dans le premier tour on reçoit p1 // Si le pourcentage est à 0 et le nombre de tour à UN // on n'allume aucune LED pour cette voiture if ((pourcentageTour == 0 ) && (toursComplets == 1)) { // Eteindre le rectangle complet leds.DrawFilledRectangle(0, y, 15, y+3, (CRGB::Black)); Serial.print("Aucune LED ======================= \n"); FastLED.show(); } // Si le nombre de tours est à UN on n'affiche que la position dans ce tour if (toursComplets == 1 && (pourcentageTour != 0)) { // Eteindre le rectangle complet leds.DrawFilledRectangle(0, y, 15, y+3, (CRGB::Black)); // Allumer les LED correspondant au % de tour effectué leds.DrawFilledRectangle(0, y, n, y+3, color); Serial.print("Que la position dans le tour ==== \n"); FastLED.show(); } // Si le nombre de tours est supérieur à 1 if (toursComplets >1) { // Eteindre le rectangle complet de la voiture, y compris le nombre de tours leds.DrawFilledRectangle(0, y, 15, y+3, (CRGB::Black)); // Allumer les LED correspondant au % de tour effectué leds.DrawFilledRectangle(0, y, n, y+3, color); // Allumer le nombre de LEDs correspondant au nombre de tours complets leds.DrawFilledRectangle(12, y, 10 + toursComplets, y+3, color2); Serial.print("Position + Nombre de tours ===== \n"); FastLED.show(); } } |
Vous pouvez aussi le télécharger en cliquant sur ce lien. J’ai laissé les print qui ont servi aux tests, vous pourrez les enlever pour alléger le programme.
Modifications de Open LED Race
Ajout de sorties vers Serial1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
... void setup() { // Sortie vers la matrice Serial1.begin(9600); Serial.begin(115200); randomSeed( analogRead(A6) + analogRead(A7) ); controller_setup( ); param_load( &tck.cfg ); ... void activate_demo_mode(void){ race.demo_mode = true; race.demo_mode_on_received = false; // reset flag set_controllers_mode(race.numcars, DEMO_MODE ) ; race.winner=0; // force a fake winner (used in Status=Complete by draw_winner()) race.phase = COMPLETE; sprintf(txbuff, "%c%d%c", 'M', 1 , EOL ); serialCommand.sendCommand(txbuff); // Sortie vers la matrice Serial1.print(txbuff); ... void exit_demo_mode(void){ race.demo_mode = false; race.demo_mode_off_received = false; // reset flag set_controllers_mode(race.numcars, DIGITAL_MODE ) ; race.winner=0; // force a fake winner (used in Status=Complete by draw_winner()) race.phase = COMPLETE; sprintf(txbuff, "%c%d%c", 'M', 0 , EOL ); serialCommand.sendCommand(txbuff); // Sortie vers la matrice Serial1.print(txbuff); ... void send_phase( int phase ) { sprintf(txbuff, "R%d%c",phase,EOL); serialCommand.sendCommand(txbuff); // Sortie vers la matrice Serial1.print(txbuff); ... if ( car->st == CAR_FINISH ){ car->trackID = NOT_TRACK; sprintf( txbuff, "w%d%c", caridx + 1, EOL ); serialCommand.sendCommand(txbuff); // Sortie vers la matrice Serial1.print(txbuff); ... for( int i = 0; i < race.numcars; ++i ) { int const rpos = get_relative_position( &cars[i] ); sprintf( txbuff, "p%d%s%d,%d,%d%c", i + 1, tracksID[cars[i].trackID], cars[i].nlap, rpos,(int)cars[i].battery, EOL ); serialCommand.sendCommand(txbuff); // Sortie vers la matrice Serial1.print("T,"); Serial1.print(txbuff); |
J’ai ajouté une sortie vers le port Série 1 de l’Arduino Nano Every et j’ai mis « T, » devant la télémétrie des voitures pour réduire le défaut que j’avais sur la voiture verte qui ne s’affichait pas. Maintenant c’est de temps en temps la blanche qui n’affiche rien…??? à suivre
En fait pour la verte au lieu de p2M1,50,100 je recevais p2,50,100 (uniquement après avoir utilisé le bouton rouge !) ce qui fausse le décodage et bloque l’affichage de la couleur verte. apparemment j’aurais le même souci avec la blanche ?
Vidéo
Conclusion
Une belle interface pour Open Led Race. J’ai encore quelques soucis avec la transmission des données. Je suis descendu à 9600 bits/s mais de temps en temps il y a des pertes de données, je vais continuer à chercher la cause et améliorer à mesure. (mettre des ferrites sur le c^ble série, modifier le programme pour vérifier le longueur de la chaine reçue…).
Le programme dans github : https://github.com/framboise314/Display-for-Open-LED-Race
Sources
https://www.thingiverse.com/thing:5746531
http://electroniqueamateur.blogspot.com/2020/05/matrice-de-leds-rgb-16-x-16-ws2812b-et.html
Ping : Un afficheur pour la course de LEDs Open LED Race